Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions app/src/main/java/com/hmh/hamyeonham/StartActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.hmh.hamyeonham.common.dialog.OneButtonCommonDialog
import com.hmh.hamyeonham.common.view.viewBinding
import com.hmh.hamyeonham.databinding.ActivitySampleBinding
import com.hmh.hamyeonham.feature.login.LoginActivity
Expand All @@ -27,18 +26,11 @@ class StartActivity : AppCompatActivity() {

private fun initLottieSplash() {
binding.splashLottieAppLogo.playAnimation()
OneButtonCommonDialog
.newInstance(
title = "서비스를 개선하고 있어요",
description = "서비스를 개선하고 있어요 더 나은 모습으로 만나요",
iconRes = null,
confirmButtonText = "확인",
)
.setConfirmButtonClickListener {
finish()
binding.splashLottieAppLogo.addAnimatorUpdateListener {
if (it.animatedFraction == 1.0f) {
navigateToLogin()
}
.showAllowingStateLoss(supportFragmentManager)

}
Comment on lines +26 to +30
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve animation completion detection and cleanup.

The current implementation has a few potential issues:

  1. Floating point precision: Using exact equality (== 1.0f) for floating point comparison can be unreliable. Animation fraction might not reach exactly 1.0f due to precision issues.

  2. Missing cleanup: The animator listener is not removed, which could potentially cause memory leaks.

Apply this diff to improve the implementation:

 private fun initLottieSplash() {
     binding.splashLottieAppLogo.playAnimation()
-    binding.splashLottieAppLogo.addAnimatorUpdateListener {
-        if (it.animatedFraction == 1.0f) {
+    binding.splashLottieAppLogo.addAnimatorUpdateListener { animator ->
+        if (animator.animatedFraction >= 0.99f) {
+            binding.splashLottieAppLogo.removeAllUpdateListeners()
             navigateToLogin()
         }
     }
 }
📝 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
binding.splashLottieAppLogo.addAnimatorUpdateListener {
if (it.animatedFraction == 1.0f) {
navigateToLogin()
}
.showAllowingStateLoss(supportFragmentManager)
}
private fun initLottieSplash() {
binding.splashLottieAppLogo.playAnimation()
binding.splashLottieAppLogo.addAnimatorUpdateListener { animator ->
if (animator.animatedFraction >= 0.99f) {
binding.splashLottieAppLogo.removeAllUpdateListeners()
navigateToLogin()
}
}
}
🤖 Prompt for AI Agents
In app/src/main/java/com/hmh/hamyeonham/StartActivity.kt around lines 29 to 33,
replace the exact floating point equality check on animatedFraction with a
threshold comparison (e.g., check if animatedFraction is greater than or equal
to 0.99f) to handle precision issues. Additionally, after navigating to login,
remove the animator update listener to prevent potential memory leaks by
unregistering the listener from the animation.

}

private fun navigateToLogin() {
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/layout/activity_sample.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@
app:lottie_autoPlay="true"
app:lottie_loop="false"
app:lottie_rawRes="@raw/splash_logo_white"
app:lottie_speed="1" />
app:lottie_speed="1.5" />

</androidx.constraintlayout.widget.ConstraintLayout>
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.hmh.hamyeonham.common.context

import android.Manifest
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.Point
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ package com.hmh.hamyeonham.core.database

import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.hmh.hamyeonham.core.database.dao.ChallengeDao
import com.hmh.hamyeonham.core.database.dao.DeletedGoalsDao
import com.hmh.hamyeonham.core.database.dao.LockDao
import com.hmh.hamyeonham.core.database.dao.UsageGoalsDao
import com.hmh.hamyeonham.core.database.dao.UsageTotalGoalDao
import com.hmh.hamyeonham.core.database.dao.UserAuthDao
import com.hmh.hamyeonham.core.database.dao.UserProfileDao
import com.hmh.hamyeonham.core.database.entity.UserAuth
import com.hmh.hamyeonham.core.database.entity.UserProfile
import com.hmh.hamyeonham.core.database.model.DailyChallengeEntity
import com.hmh.hamyeonham.core.database.model.DeletedGoalWithUsageEntity
import com.hmh.hamyeonham.core.database.model.DeletedUsageEntity
Expand All @@ -26,9 +32,11 @@ import kotlinx.coroutines.launch
DailyChallengeEntity::class,
DeletedGoalWithUsageEntity::class,
DeletedUsageEntity::class,
LockWithDateEntity::class
LockWithDateEntity::class,
UserAuth::class,
UserProfile::class
],
version = 1,
version = 2,
exportSchema = false
)
abstract class HMHRoomDatabase : RoomDatabase() {
Expand All @@ -37,6 +45,8 @@ abstract class HMHRoomDatabase : RoomDatabase() {
abstract fun challengeDao(): ChallengeDao
abstract fun deletedGoalsDao(): DeletedGoalsDao
abstract fun lockDao(): LockDao
abstract fun userAuthDao(): UserAuthDao
abstract fun userProfileDao(): UserProfileDao

@OptIn(DelicateCoroutinesApi::class)
fun deleteAll() {
Expand All @@ -46,7 +56,45 @@ abstract class HMHRoomDatabase : RoomDatabase() {
challengeDao().deleteAll()
deletedGoalsDao().deleteAll()
lockDao().deleteAll()
userAuthDao().clearUserAuth()
userProfileDao().clearUserProfile()
}
}

companion object {
// 버전 1에서 버전 2로 마이그레이션
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// UserAuth 테이블 생성
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `user_auth` (
`id` INTEGER NOT NULL,
`userId` INTEGER NOT NULL,
`providerType` TEXT NOT NULL,
`isLoggedIn` INTEGER NOT NULL,
`lastLoginTimestamp` INTEGER NOT NULL,
PRIMARY KEY(`id`)
)
"""
)

// UserProfile 테이블 생성
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `user_profile` (
`userId` INTEGER NOT NULL,
`nickname` TEXT,
`profileImageUrl` TEXT,
`email` TEXT,
`ageRange` TEXT,
`gender` TEXT,
`updatedAt` INTEGER NOT NULL,
PRIMARY KEY(`userId`)
)
"""
)
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.hmh.hamyeonham.core.database.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.hmh.hamyeonham.core.database.entity.UserAuth
import kotlinx.coroutines.flow.Flow

@Dao
interface UserAuthDao {
@Query("SELECT * FROM user_auth WHERE id = 1")
suspend fun getUserAuth(): UserAuth?

@Query("SELECT * FROM user_auth WHERE id = 1")
fun observeUserAuth(): Flow<UserAuth?>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrUpdateUserAuth(userAuth: UserAuth)

@Query("UPDATE user_auth SET isLoggedIn = :isLoggedIn WHERE id = 1")
suspend fun updateLoginStatus(isLoggedIn: Boolean)

@Query("DELETE FROM user_auth")
suspend fun clearUserAuth()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.hmh.hamyeonham.core.database.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.hmh.hamyeonham.core.database.entity.UserProfile
import kotlinx.coroutines.flow.Flow

@Dao
interface UserProfileDao {
@Query("SELECT * FROM user_profile WHERE userId = :userId")
suspend fun getUserProfile(userId: Long): UserProfile?

@Query("SELECT * FROM user_profile WHERE userId = :userId")
fun observeUserProfile(userId: Long): Flow<UserProfile?>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrUpdateUserProfile(userProfile: UserProfile)

@Query("DELETE FROM user_profile")
suspend fun clearUserProfile()
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,16 @@ object DaoModule {
fun providesLockDao(
database: HMHRoomDatabase,
) = database.lockDao()

@Provides
@Singleton
fun providesUserAuthDao(
database: HMHRoomDatabase,
) = database.userAuthDao()

@Provides
@Singleton
fun providesUserProfileDao(
database: HMHRoomDatabase,
) = database.userProfileDao()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.hmh.hamyeonham.core.database.di

import android.content.Context
import androidx.room.Room
import com.hmh.hamyeonham.core.database.BuildConfig
import com.hmh.hamyeonham.core.database.HMHRoomDatabase
import dagger.Module
import dagger.Provides
Expand All @@ -17,9 +18,13 @@ object DatabaseModule {
@Singleton
fun providesHMHDatabase(
@ApplicationContext context: Context,
): HMHRoomDatabase = Room.databaseBuilder(
context,
HMHRoomDatabase::class.java,
"hmh-android-database",
).build()
): HMHRoomDatabase {
val builder = Room.databaseBuilder(
context,
HMHRoomDatabase::class.java,
"hmh-android-database"
)
builder.addMigrations(HMHRoomDatabase.MIGRATION_1_2)
return builder.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.hmh.hamyeonham.core.database.entity

import androidx.room.Entity
import androidx.room.PrimaryKey

/**
* 사용자 인증 정보를 저장하는 엔티티
* 토큰은 SDK에서 관리하므로 저장하지 않음
*/
@Entity(tableName = "user_auth")
data class UserAuth(
@PrimaryKey
val id: Int = 1, // 항상 같은 ID 사용 (싱글 인스턴스)
val userId: Long = -1,
val providerType: String = "", // "KAKAO", "GOOGLE" 등 AuthProvider의 name
val isLoggedIn: Boolean = false,
val lastLoginTimestamp: Long = 0
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider alternatives to the singleton pattern and improve type safety.

The entity design has a few areas for improvement:

  1. Singleton with fixed ID: Using id: Int = 1 to enforce a single instance is unconventional for Room. Consider using a repository-level singleton pattern instead.

  2. String-based provider type: The providerType field would benefit from an enum for better type safety:

+enum class AuthProvider {
+    KAKAO, GOOGLE, NONE;
+    
+    companion object {
+        fun fromString(value: String): AuthProvider = 
+            values().find { it.name == value } ?: NONE
+    }
+}

 data class UserAuth(
     @PrimaryKey
     val id: Int = 1,
     val userId: Long = -1,
-    val providerType: String = "", // "KAKAO", "GOOGLE" 등 AuthProvider의 name
+    val providerType: String = AuthProvider.NONE.name,
     val isLoggedIn: Boolean = false,
     val lastLoginTimestamp: Long = 0
 )
🤖 Prompt for AI Agents
In
core/database/src/main/java/com/hmh/hamyeonham/core/database/entity/UserAuth.kt
around lines 10 to 18, avoid using a fixed ID value to enforce a singleton in
the Room entity; instead, remove the default fixed ID and handle singleton
behavior at the repository or service layer. Also, replace the String type of
the providerType field with an enum class representing the authentication
providers to improve type safety and prevent invalid values.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.hmh.hamyeonham.core.database.entity

import androidx.room.Entity
import androidx.room.PrimaryKey

/**
* 사용자 프로필 정보를 저장하는 엔티티
*/
@Entity(tableName = "user_profile")
data class UserProfile(
@PrimaryKey
val userId: Long,
val nickname: String? = null,
val profileImageUrl: String? = null,
val email: String? = null,
val ageRange: String? = null,
val gender: String? = null,
val updatedAt: Long = System.currentTimeMillis()
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider moving timestamp generation to repository layer.

The entity design is mostly well-structured, but there's one concern with the default timestamp:

 data class UserProfile(
     @PrimaryKey
     val userId: Long,
     val nickname: String? = null,
     val profileImageUrl: String? = null,
     val email: String? = null,
     val ageRange: String? = null,
     val gender: String? = null,
-    val updatedAt: Long = System.currentTimeMillis()
+    val updatedAt: Long
 )

Rationale: Using System.currentTimeMillis() as a default value in the entity can cause issues with:

  • Unit testing (timestamps become unpredictable)
  • Data consistency (multiple instances created at different times)
  • Database operations (updates vs inserts might get different timestamps)

Consider generating the timestamp in the repository or DAO layer where you have better control over when it's set.

📝 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
@Entity(tableName = "user_profile")
data class UserProfile(
@PrimaryKey
val userId: Long,
val nickname: String? = null,
val profileImageUrl: String? = null,
val email: String? = null,
val ageRange: String? = null,
val gender: String? = null,
val updatedAt: Long = System.currentTimeMillis()
)
@Entity(tableName = "user_profile")
data class UserProfile(
@PrimaryKey
val userId: Long,
val nickname: String? = null,
val profileImageUrl: String? = null,
val email: String? = null,
val ageRange: String? = null,
val gender: String? = null,
val updatedAt: Long
)
🤖 Prompt for AI Agents
In
core/database/src/main/java/com/hmh/hamyeonham/core/database/entity/UserProfile.kt
lines 9 to 19, the default timestamp for updatedAt is set using
System.currentTimeMillis() in the entity, which can cause issues with testing
and data consistency. Remove the default value from the entity and instead set
the updatedAt timestamp explicitly in the repository or DAO layer when creating
or updating UserProfile instances to ensure better control and predictability.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.hmh.hamyeonham.core.network.auth.authenticator
import android.content.Context
import com.hmh.hamyeonham.common.navigation.NavigationProvider
import com.hmh.hamyeonham.core.database.manger.DatabaseManager
import com.hmh.hamyeonham.core.network.auth.datastore.network.HMHNetworkPreference
import com.hmh.hamyeonham.core.network.auth.datastore.network.UserPreference
import com.jakewharton.processphoenix.ProcessPhoenix
import com.kakao.sdk.user.UserApiClient
import dagger.hilt.android.qualifiers.ApplicationContext
Expand All @@ -18,7 +18,7 @@ import kotlin.coroutines.resume
@Singleton
class AuthenticatorUtil @Inject constructor(
@ApplicationContext private val context: Context,
private val dataStore: HMHNetworkPreference,
private val dataStore: UserPreference,
private val databaseManager: DatabaseManager,
private val navigationProvider: NavigationProvider,
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.hmh.hamyeonham.core.network.auth.authenticator

import com.hmh.hamyeonham.core.network.auth.api.RefreshService
import com.hmh.hamyeonham.core.network.auth.datastore.network.HMHNetworkPreference
import com.hmh.hamyeonham.core.network.auth.datastore.network.UserPreference
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
Expand All @@ -16,7 +16,7 @@ import javax.inject.Singleton

@Singleton
class HMHAuthenticator @Inject constructor(
private val dataStore: HMHNetworkPreference,
private val dataStore: UserPreference,
private val api: RefreshService,
private val authenticatorUtil: AuthenticatorUtil
) : Authenticator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class DefaultHMHNetworkPreference @Inject constructor(
class DefaultUserPreference @Inject constructor(
private val preferences: SharedPreferences,
) : HMHNetworkPreference {
) : UserPreference {
override var accessToken: String
get() = preferences.getString("access_token", "").orEmpty()
set(value) {
Expand All @@ -30,11 +30,11 @@ class DefaultHMHNetworkPreference @Inject constructor(
putString("user_name", value)
}
}
override var userId: Int
get() = preferences.getInt("user_id", -1)
override var userId: Long
get() = preferences.getLong("user_id", -1)
set(value) {
preferences.edit(commit = true) {
putInt("user_id", value)
putLong("user_id", value)
}
}
Comment on lines +33 to 39
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

Verify migration impact for existing users.

The userId type change from Int to Long is significant and requires careful consideration for existing users who may have Int values stored in SharedPreferences.


🏁 Script executed:

#!/bin/bash
# Description: Search for any migration or compatibility handling for the userId type change
# Expected: Find migration logic or compatibility handling for existing Int userId values

echo "Searching for userId migration or compatibility handling..."
rg -A 5 -B 5 "userId.*migration|migration.*userId|user_id.*int|int.*user_id" --type kt

echo "Searching for any SharedPreferences migration patterns..."
rg -A 10 "SharedPreferences.*migration|migration.*SharedPreferences" --type kt

echo "Checking for userId type usage patterns..."
rg "userId.*Int|Int.*userId" --type kt

Length of output: 640


🏁 Script executed:

#!/bin/bash
# Verify if any legacy Int-based API for “user_id” still exists or if migration code is present

echo "🔍 Searching for legacy getInt usage for 'user_id'..."
rg -n 'getInt\("user_id"' -g '*.kt'

echo "🔍 Searching for legacy putInt usage for 'user_id'..."
rg -n 'putInt\("user_id"' -g '*.kt'

echo "🔍 Searching for migration/convert patterns in Kotlin sources..."
rg -n -E 'migrate|migration|convert|upgrade' -g '*.kt'

Length of output: 914


Implement migration for legacy Int-based userId in SharedPreferences

No compatibility or migration logic was found for existing users who have user_id stored as an Int. Calling getLong("user_id") on an Int-backed preference will throw a ClassCastException at runtime. Please add fallback handling to read the old Int value and re-save it as a Long.

• File: core/network/src/main/java/com/hmh/hamyeonham/core/network/auth/datastore/network/DefaultUserPreference.kt, lines 33–39
• Add a try/catch around getLong (or migrate on initialization) to preserve existing data

Suggested diff:

 override var userId: Long
-    get() = preferences.getLong("user_id", -1)
+    get() = try {
+        preferences.getLong("user_id", -1)
+    } catch (e: ClassCastException) {
+        // Migrate legacy Int value to Long
+        val intValue = preferences.getInt("user_id", -1)
+        preferences.edit(commit = true) {
+            putLong("user_id", intValue.toLong())
+        }
+        intValue.toLong()
+    }
     set(value) {
         preferences.edit(commit = true) {
             putLong("user_id", value)
         }
     }
📝 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
override var userId: Long
get() = preferences.getLong("user_id", -1)
set(value) {
preferences.edit(commit = true) {
putInt("user_id", value)
putLong("user_id", value)
}
}
override var userId: Long
get() = try {
preferences.getLong("user_id", -1)
} catch (e: ClassCastException) {
// Migrate legacy Int value to Long
val intValue = preferences.getInt("user_id", -1)
preferences.edit(commit = true) {
putLong("user_id", intValue.toLong())
}
intValue.toLong()
}
set(value) {
preferences.edit(commit = true) {
putLong("user_id", value)
}
}
🤖 Prompt for AI Agents
In
core/network/src/main/java/com/hmh/hamyeonham/core/network/auth/datastore/network/DefaultUserPreference.kt
lines 33-39, the userId getter directly calls getLong on preferences which can
throw ClassCastException if the stored user_id is an Int. To fix this, wrap the
getLong call in a try/catch block; in the catch, read the user_id as an Int,
convert it to Long, save it back as Long, and return the Long value. This
migration ensures backward compatibility with legacy Int-based user_id values.

override var autoLoginConfigured: Boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.hmh.hamyeonham.core.network.auth.datastore.network

interface HMHNetworkPreference {
interface UserPreference {
var accessToken: String
var refreshToken: String
var userName: String
var userId: Int
var userId: Long
var autoLoginConfigured: Boolean
fun clear()
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.hmh.hamyeonham.core.network.auth.interceptor

import com.hmh.hamyeonham.core.network.auth.datastore.network.HMHNetworkPreference
import com.hmh.hamyeonham.core.network.auth.datastore.network.UserPreference
import kotlinx.datetime.TimeZone
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject

class HeaderInterceptor @Inject constructor(
private val dataStore: HMHNetworkPreference
private val dataStore: UserPreference
) : Interceptor {
private val encodedToken: String
get() = "Bearer ${dataStore.accessToken}"
Expand Down

This file was deleted.

Loading