diff --git a/.github/workflows/android-pull-request-ci.yml b/.github/workflows/android-pull-request-ci.yml
index 8854d504..af693824 100644
--- a/.github/workflows/android-pull-request-ci.yml
+++ b/.github/workflows/android-pull-request-ci.yml
@@ -44,10 +44,10 @@ jobs:
echo "TEMP_TOKEN=\"TEMP_TOKEN\"" >> local.properties
- name: Access Kakao KAKAO_NATIVE_APP_KEY
- env:
- KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }}
- run: |
- echo "KAKAO_NATIVE_APP_KEY=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties
+ env:
+ KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }}
+ run: |
+ echo "KAKAO_NATIVE_APP_KEY=\"$KAKAO_NATIVE_APP_KEY\"" >> local.properties
- name: Grant execute permission for gradlew
run: chmod +x gradlew
diff --git a/Near/app/build.gradle.kts b/Near/app/build.gradle.kts
index 6ab76de2..0284dce0 100644
--- a/Near/app/build.gradle.kts
+++ b/Near/app/build.gradle.kts
@@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.hilt.application)
alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.kotlin.parcelize)
}
android {
@@ -98,6 +99,9 @@ dependencies {
// Kakao Module
implementation(libs.v2.all)
+
+ // Splash Screen API
+ implementation(libs.androidx.core.splashscreen)
}
fun getProperty(propertyKey: String): String = gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
diff --git a/Near/app/src/main/AndroidManifest.xml b/Near/app/src/main/AndroidManifest.xml
index b0ae0a68..09d49b30 100644
--- a/Near/app/src/main/AndroidManifest.xml
+++ b/Near/app/src/main/AndroidManifest.xml
@@ -9,7 +9,7 @@
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
- android:icon="@mipmap/ic_launcher"
+ android:icon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
@@ -20,25 +20,28 @@
-
+
+
-
+
+
-
+
+ android:theme="@style/Theme.Near.Splash">
diff --git a/Near/app/src/main/java/com/alarmy/near/data/di/DataStoreModule.kt b/Near/app/src/main/java/com/alarmy/near/data/di/DataStoreModule.kt
index 5f73bfb6..12c320b2 100644
--- a/Near/app/src/main/java/com/alarmy/near/data/di/DataStoreModule.kt
+++ b/Near/app/src/main/java/com/alarmy/near/data/di/DataStoreModule.kt
@@ -9,17 +9,35 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
+import javax.inject.Qualifier
import javax.inject.Singleton
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class AuthDataStore
+
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class OnboardingDataStore
+
// DataStore 확장 프로퍼티
-private val Context.dataStore: DataStore by preferencesDataStore(name = "auth_preferences")
+private val Context.authDataStore: DataStore by preferencesDataStore(name = "auth_preferences")
+private val Context.onboardingDataStore: DataStore by preferencesDataStore(name = "onboarding_preferences")
@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {
@Provides
@Singleton
- fun provideDataStore(
+ @AuthDataStore
+ fun provideAuthDataStore(
+ @ApplicationContext context: Context,
+ ): DataStore = context.authDataStore
+
+ @Provides
+ @Singleton
+ @OnboardingDataStore
+ fun provideOnboardingDataStore(
@ApplicationContext context: Context,
- ): DataStore = context.dataStore
+ ): DataStore = context.onboardingDataStore
}
diff --git a/Near/app/src/main/java/com/alarmy/near/data/di/RepositoryModule.kt b/Near/app/src/main/java/com/alarmy/near/data/di/RepositoryModule.kt
index 5d73c37f..50d1b230 100644
--- a/Near/app/src/main/java/com/alarmy/near/data/di/RepositoryModule.kt
+++ b/Near/app/src/main/java/com/alarmy/near/data/di/RepositoryModule.kt
@@ -6,6 +6,8 @@ import com.alarmy.near.data.repository.DefaultFriendRepository
import com.alarmy.near.data.repository.ExampleRepository
import com.alarmy.near.data.repository.ExampleRepositoryImpl
import com.alarmy.near.data.repository.FriendRepository
+import com.alarmy.near.data.repository.OnBoardingRepository
+import com.alarmy.near.data.repository.OnBoardingRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -26,4 +28,8 @@ interface RepositoryModule {
@Binds
@Singleton
abstract fun bindAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository
+
+ @Binds
+ @Singleton
+ abstract fun bindOnBoardingRepository(onBoardingRepositoryImpl: OnBoardingRepositoryImpl): OnBoardingRepository
}
diff --git a/Near/app/src/main/java/com/alarmy/near/data/local/datastore/OnboardingPreferences.kt b/Near/app/src/main/java/com/alarmy/near/data/local/datastore/OnboardingPreferences.kt
new file mode 100644
index 00000000..fe7f5841
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/data/local/datastore/OnboardingPreferences.kt
@@ -0,0 +1,48 @@
+package com.alarmy.near.data.local.datastore
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import com.alarmy.near.data.di.OnboardingDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * 온보딩 완료 상태를 관리하는 DataStore
+ * 최초 접속 여부를 확인하여 온보딩 화면 표시 여부를 결정
+ */
+@Singleton
+class OnboardingPreferences @Inject constructor(
+ @OnboardingDataStore private val dataStore: DataStore,
+) {
+ companion object {
+ private val ONBOARDING_COMPLETED_KEY = booleanPreferencesKey("onboarding_completed")
+ }
+
+ /**
+ * 온보딩 완료 여부를 확인하는 Flow
+ * true: 온보딩 완료됨, false: 온보딩 미완료
+ */
+ val isOnboardingCompleted: Flow = dataStore.data.map { preferences ->
+ preferences[ONBOARDING_COMPLETED_KEY] ?: false
+ }
+
+ /**
+ * 온보딩 완료 상태를 저장
+ */
+ suspend fun setOnboardingCompleted(completed: Boolean) {
+ dataStore.edit { preferences ->
+ preferences[ONBOARDING_COMPLETED_KEY] = completed
+ }
+ }
+
+ /**
+ * 온보딩 완료 상태를 true로 설정
+ */
+ suspend fun markOnboardingAsCompleted() {
+ setOnboardingCompleted(true)
+ }
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/data/local/datastore/TokenPreferences.kt b/Near/app/src/main/java/com/alarmy/near/data/local/datastore/TokenPreferences.kt
index 13f5eaa0..81c5362b 100644
--- a/Near/app/src/main/java/com/alarmy/near/data/local/datastore/TokenPreferences.kt
+++ b/Near/app/src/main/java/com/alarmy/near/data/local/datastore/TokenPreferences.kt
@@ -5,9 +5,9 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
+import com.alarmy.near.data.di.AuthDataStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
-
import javax.inject.Inject
import javax.inject.Singleton
@@ -16,11 +16,9 @@ import javax.inject.Singleton
* 액세스 토큰과 리프레시 토큰의 저장, 조회, 삭제를 담당
*/
@Singleton
-class TokenPreferences
- @Inject
- constructor(
- private val dataStore: DataStore,
- ) {
+class TokenPreferences @Inject constructor(
+ @AuthDataStore private val dataStore: DataStore,
+) {
private val accessTokenKey = stringPreferencesKey("access_token")
private val refreshTokenKey = stringPreferencesKey("refresh_token")
private val expiresAtKey = longPreferencesKey("expires_at")
diff --git a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt
index fcfc70c5..68203c95 100644
--- a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt
+++ b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt
@@ -1,21 +1,65 @@
package com.alarmy.near.data.mapper
+import com.alarmy.near.model.Anniversary
import com.alarmy.near.model.ContactFrequency
-import com.alarmy.near.model.FriendSummary
+import com.alarmy.near.model.DayOfWeek
+import com.alarmy.near.model.Friend
+import com.alarmy.near.model.Relation
+import com.alarmy.near.model.ReminderInterval
+import com.alarmy.near.network.request.AnniversaryRequest
+import com.alarmy.near.network.request.ContactFrequencyRequest
+import com.alarmy.near.network.request.FriendRequest
+import com.alarmy.near.network.response.AnniversaryEntity
+import com.alarmy.near.network.response.ContactFrequencyEntity
import com.alarmy.near.network.response.FriendEntity
-fun FriendEntity.toModel(): FriendSummary =
- FriendSummary(
- id = friendId,
+fun FriendEntity.toModel(): Friend =
+ Friend(
+ friendId = friendId,
+ imageUrl = imageUrl,
+ relation = Relation.valueOf(relation),
name = name,
- profileImageUrl = imageUrl,
- lastContactedAt = lastContactAt,
- isContacted = true,
- contactFrequency =
- when (checkRate) {
- in 0..29 -> ContactFrequency.LOW
- in 30..69 -> ContactFrequency.MIDDLE
- in 70..100 -> ContactFrequency.HIGH
- else -> ContactFrequency.LOW
- },
- )
+ contactFrequency = contactFrequency.toModel(),
+ birthday = birthday,
+ anniversaryList = anniversaryList.map { it.toModel() },
+ memo = memo,
+ phone = phone,
+ lastContactAt = lastContactAt,
+ )
+
+fun ContactFrequencyEntity.toModel(): ContactFrequency =
+ ContactFrequency(
+ reminderInterval = ReminderInterval.valueOf(contactWeek),
+ dayOfWeek = DayOfWeek.valueOf(dayOfWeek),
+ )
+
+fun AnniversaryEntity.toModel(): Anniversary =
+ Anniversary(
+ id = id,
+ title = title,
+ date = date,
+ )
+
+fun Friend.toRequest(): FriendRequest =
+ FriendRequest(
+ name = name,
+ relation = relation.toString(),
+ contactFrequency = contactFrequency.toRequest(),
+ birthday = birthday,
+ anniversaryList = anniversaryList.map { it.toRequest() },
+ memo = memo,
+ phone = phone,
+ )
+
+fun ContactFrequency.toRequest(): ContactFrequencyRequest =
+ ContactFrequencyRequest(
+ contactWeek = reminderInterval.toString(),
+ dayOfWeek = dayOfWeek.toString(),
+ )
+
+fun Anniversary.toRequest(): AnniversaryRequest =
+ AnniversaryRequest(
+ id = id,
+ title = title,
+ date = date,
+ )
diff --git a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendRecordMapper.kt b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendRecordMapper.kt
new file mode 100644
index 00000000..ce99b806
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendRecordMapper.kt
@@ -0,0 +1,20 @@
+package com.alarmy.near.data.mapper
+
+import com.alarmy.near.model.FriendRecord
+import com.alarmy.near.network.response.FriendRecordEntity
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+fun FriendRecordEntity.toModel(): FriendRecord =
+ FriendRecord(
+ isChecked = isChecked,
+ createdAt = createdAt.toShortDate(),
+ )
+
+private fun String.toShortDate(): String {
+ val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+ val outputFormatter = DateTimeFormatter.ofPattern("yy.MM.dd")
+
+ val dateTime = LocalDateTime.parse(this, inputFormatter)
+ return dateTime.format(outputFormatter)
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendSummaryMapper.kt b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendSummaryMapper.kt
new file mode 100644
index 00000000..c1811712
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendSummaryMapper.kt
@@ -0,0 +1,25 @@
+package com.alarmy.near.data.mapper
+
+import com.alarmy.near.model.friendsummary.ContactFrequencyLevel
+import com.alarmy.near.model.friendsummary.FriendSummary
+import com.alarmy.near.network.response.FriendSummaryEntity
+
+private val CONTACT_FREQUENCY_LOW_RANGE = 0..29
+private val CONTACT_FREQUENCY_MIDDLE_RANGE = 30..69
+private val CONTACT_FREQUENCY_HIGH_RANGE = 70..100
+
+fun FriendSummaryEntity.toModel(): FriendSummary =
+ FriendSummary(
+ id = friendId,
+ name = name,
+ profileImageUrl = imageUrl,
+ lastContactedAt = lastContactAt,
+ isContacted = true,
+ contactFrequencyLevel =
+ when (checkRate) {
+ in CONTACT_FREQUENCY_LOW_RANGE -> ContactFrequencyLevel.LOW
+ in CONTACT_FREQUENCY_MIDDLE_RANGE -> ContactFrequencyLevel.MIDDLE
+ in CONTACT_FREQUENCY_HIGH_RANGE -> ContactFrequencyLevel.HIGH
+ else -> ContactFrequencyLevel.LOW
+ },
+ )
diff --git a/Near/app/src/main/java/com/alarmy/near/data/repository/DefaultFriendRepository.kt b/Near/app/src/main/java/com/alarmy/near/data/repository/DefaultFriendRepository.kt
index 61ae63e7..7651da94 100644
--- a/Near/app/src/main/java/com/alarmy/near/data/repository/DefaultFriendRepository.kt
+++ b/Near/app/src/main/java/com/alarmy/near/data/repository/DefaultFriendRepository.kt
@@ -1,7 +1,10 @@
package com.alarmy.near.data.repository
import com.alarmy.near.data.mapper.toModel
-import com.alarmy.near.model.FriendSummary
+import com.alarmy.near.data.mapper.toRequest
+import com.alarmy.near.model.Friend
+import com.alarmy.near.model.FriendRecord
+import com.alarmy.near.model.friendsummary.FriendSummary
import com.alarmy.near.model.monthly.MonthlyFriend
import com.alarmy.near.network.service.FriendService
import kotlinx.coroutines.flow.Flow
@@ -30,4 +33,34 @@ class DefaultFriendRepository
},
)
}
+
+ override fun fetchFriendById(friendId: String): Flow =
+ flow {
+ emit(friendService.fetchFriendById(friendId).toModel())
+ }
+
+ override fun updateFriend(
+ friendId: String,
+ friend: Friend,
+ ): Flow =
+ flow {
+ emit(friendService.updateFriend(friendId, friend.toRequest()).toModel())
+ }
+
+ override fun deleteFriend(friendId: String): Flow =
+ flow {
+ friendService.deleteFriend(friendId)
+ emit(Unit)
+ }
+
+ override fun fetchFriendRecord(friendId: String): Flow> =
+ flow {
+ emit(friendService.fetchFriendRecord(friendId).map { it.toModel() })
+ }
+
+ override fun recordContact(friendId: String): Flow =
+ flow {
+ val response = friendService.recordContact(friendId)
+ emit(response.message) // CommonMessageEntity.message 라고 가정
+ }
}
diff --git a/Near/app/src/main/java/com/alarmy/near/data/repository/FriendRepository.kt b/Near/app/src/main/java/com/alarmy/near/data/repository/FriendRepository.kt
index 425654fe..81e6761b 100644
--- a/Near/app/src/main/java/com/alarmy/near/data/repository/FriendRepository.kt
+++ b/Near/app/src/main/java/com/alarmy/near/data/repository/FriendRepository.kt
@@ -1,6 +1,8 @@
package com.alarmy.near.data.repository
-import com.alarmy.near.model.FriendSummary
+import com.alarmy.near.model.Friend
+import com.alarmy.near.model.FriendRecord
+import com.alarmy.near.model.friendsummary.FriendSummary
import com.alarmy.near.model.monthly.MonthlyFriend
import kotlinx.coroutines.flow.Flow
@@ -8,4 +10,17 @@ interface FriendRepository {
fun fetchFriends(): Flow>
fun fetchMonthlyFriends(): Flow>
+
+ fun fetchFriendById(friendId: String): Flow
+
+ fun updateFriend(
+ friendId: String,
+ friend: Friend,
+ ): Flow
+
+ fun deleteFriend(friendId: String): Flow
+
+ fun fetchFriendRecord(friendId: String): Flow>
+
+ fun recordContact(friendId: String): Flow
}
diff --git a/Near/app/src/main/java/com/alarmy/near/data/repository/OnBoardingRepository.kt b/Near/app/src/main/java/com/alarmy/near/data/repository/OnBoardingRepository.kt
new file mode 100644
index 00000000..c71f8c37
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/data/repository/OnBoardingRepository.kt
@@ -0,0 +1,37 @@
+package com.alarmy.near.data.repository
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * 온보딩 관련 데이터를 관리하는 Repository 인터페이스
+ * 온보딩 완료 상태의 저장, 조회, 확인 기능을 제공
+ */
+interface OnBoardingRepository {
+
+ /**
+ * 온보딩 완료 여부를 확인하는 Flow
+ * true: 온보딩 완료됨, false: 온보딩 미완료
+ */
+ fun observeOnboardingStatus(): Flow
+
+ /**
+ * 온보딩 완료 상태를 저장
+ * completed: 온보딩 완료 여부
+ */
+ suspend fun setOnboardingCompleted(completed: Boolean)
+
+ /**
+ * 온보딩을 완료로 표시
+ */
+ suspend fun markOnboardingAsCompleted()
+
+ /**
+ * 온보딩 완료 상태를 확인
+ */
+ suspend fun isOnboardingCompleted(): Boolean
+
+ /**
+ * 온보딩 상태를 초기화 (테스트용 또는 재온보딩용)
+ */
+ suspend fun resetOnboardingStatus()
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/data/repository/OnBoardingRepositoryImpl.kt b/Near/app/src/main/java/com/alarmy/near/data/repository/OnBoardingRepositoryImpl.kt
new file mode 100644
index 00000000..02ae619e
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/data/repository/OnBoardingRepositoryImpl.kt
@@ -0,0 +1,37 @@
+package com.alarmy.near.data.repository
+
+import com.alarmy.near.data.local.datastore.OnboardingPreferences
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * OnBoardingRepository의 구현체
+ * OnboardingPreferences를 사용하여 온보딩 상태를 관리
+ */
+@Singleton
+class OnBoardingRepositoryImpl @Inject constructor(
+ private val onboardingPreferences: OnboardingPreferences,
+) : OnBoardingRepository {
+
+ override fun observeOnboardingStatus(): Flow {
+ return onboardingPreferences.isOnboardingCompleted
+ }
+
+ override suspend fun setOnboardingCompleted(completed: Boolean) {
+ onboardingPreferences.setOnboardingCompleted(completed)
+ }
+
+ override suspend fun markOnboardingAsCompleted() {
+ onboardingPreferences.markOnboardingAsCompleted()
+ }
+
+ override suspend fun isOnboardingCompleted(): Boolean {
+ return onboardingPreferences.isOnboardingCompleted.first()
+ }
+
+ override suspend fun resetOnboardingStatus() {
+ onboardingPreferences.setOnboardingCompleted(false)
+ }
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/model/ContactFrequency.kt b/Near/app/src/main/java/com/alarmy/near/model/ContactFrequency.kt
deleted file mode 100644
index dfcea4a5..00000000
--- a/Near/app/src/main/java/com/alarmy/near/model/ContactFrequency.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.alarmy.near.model
-
-enum class ContactFrequency {
- LOW,
- MIDDLE,
- HIGH,
-}
diff --git a/Near/app/src/main/java/com/alarmy/near/model/Friend.kt b/Near/app/src/main/java/com/alarmy/near/model/Friend.kt
new file mode 100644
index 00000000..b37b23a6
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/model/Friend.kt
@@ -0,0 +1,63 @@
+package com.alarmy.near.model
+
+import android.os.Parcelable
+import androidx.annotation.StringRes
+import com.alarmy.near.R
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+import java.util.Locale
+
+@Parcelize
+@Serializable
+data class Friend(
+ val friendId: String,
+ val imageUrl: String?,
+ val relation: Relation,
+ val name: String,
+ val contactFrequency: ContactFrequency,
+ val birthday: String?,
+ val anniversaryList: List,
+ val memo: String?,
+ val phone: String?,
+ val lastContactAt: String?, // "2025-07-16"
+) : Parcelable {
+ val isContactedToday: Boolean
+ get() = lastContactAt?.isToday() ?: false
+
+ private fun String.isToday(): Boolean {
+ val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.KOREA)
+ val targetDate = LocalDate.parse(this, formatter)
+ val today = LocalDate.now()
+ return targetDate == today
+ }
+}
+
+@Serializable
+@Parcelize
+data class ContactFrequency(
+ val reminderInterval: ReminderInterval,
+ val dayOfWeek: DayOfWeek,
+) : Parcelable
+
+@Serializable
+@Parcelize
+data class Anniversary(
+ val id: Int? = null,
+ val title: String,
+ val date: String? = null,
+) : Parcelable
+
+@Serializable
+enum class DayOfWeek(
+ @param:StringRes val resId: Int,
+) {
+ MONDAY(R.string.day_of_week_monday),
+ TUESDAY(R.string.day_of_week_tuesday),
+ WEDNESDAY(R.string.day_of_week_wednesday),
+ THURSDAY(R.string.day_of_week_thursday),
+ FRIDAY(R.string.day_of_week_friday),
+ SATURDAY(R.string.day_of_week_saturday),
+ SUNDAY(R.string.day_of_week_sunday),
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/model/FriendRecord.kt b/Near/app/src/main/java/com/alarmy/near/model/FriendRecord.kt
new file mode 100644
index 00000000..2d6a7459
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/model/FriendRecord.kt
@@ -0,0 +1,6 @@
+package com.alarmy.near.model
+
+data class FriendRecord(
+ val isChecked: Boolean,
+ val createdAt: String, // ex) 25.11.12
+)
diff --git a/Near/app/src/main/java/com/alarmy/near/model/Relation.kt b/Near/app/src/main/java/com/alarmy/near/model/Relation.kt
index e01a7a52..1c84a7ba 100644
--- a/Near/app/src/main/java/com/alarmy/near/model/Relation.kt
+++ b/Near/app/src/main/java/com/alarmy/near/model/Relation.kt
@@ -1,7 +1,12 @@
package com.alarmy.near.model
-enum class Relation {
- FRIEND,
- FAMILY,
- ACQUAINTANCE,
+import androidx.annotation.StringRes
+import com.alarmy.near.R
+
+enum class Relation(
+ @param:StringRes val resId: Int,
+) {
+ FRIEND(R.string.relation_friend),
+ FAMILY(R.string.relation_family),
+ ACQUAINTANCE(R.string.relation_acquaintance),
}
diff --git a/Near/app/src/main/java/com/alarmy/near/model/ReminderInterval.kt b/Near/app/src/main/java/com/alarmy/near/model/ReminderInterval.kt
index 0d39369d..5bbd80f8 100644
--- a/Near/app/src/main/java/com/alarmy/near/model/ReminderInterval.kt
+++ b/Near/app/src/main/java/com/alarmy/near/model/ReminderInterval.kt
@@ -6,9 +6,9 @@ import com.alarmy.near.R
enum class ReminderInterval(
@param:StringRes val labelRes: Int,
) {
- DAILY(R.string.reminder_interval_daily), // 매일
- WEEKLY(R.string.reminder_interval_weekly), // 매주
- BIWEEKLY(R.string.reminder_interval_biweekly), // 2주
- MONTHLY(R.string.reminder_interval_monthly), // 매달
- SEMIANNUAL(R.string.reminder_interval_semiannual), // 6개월
+ EVERY_DAY(R.string.reminder_interval_daily),
+ EVERY_WEEK(R.string.reminder_interval_weekly),
+ EVERY_TWO_WEEK(R.string.reminder_interval_biweekly),
+ EVERY_MONTH(R.string.reminder_interval_monthly),
+ EVERY_SIX_MONTH(R.string.reminder_interval_semiannual),
}
diff --git a/Near/app/src/main/java/com/alarmy/near/model/friendsummary/ContactFrequencyLevel.kt b/Near/app/src/main/java/com/alarmy/near/model/friendsummary/ContactFrequencyLevel.kt
new file mode 100644
index 00000000..2da7665a
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/model/friendsummary/ContactFrequencyLevel.kt
@@ -0,0 +1,7 @@
+package com.alarmy.near.model.friendsummary
+
+enum class ContactFrequencyLevel {
+ LOW,
+ MIDDLE,
+ HIGH,
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/model/FriendSummary.kt b/Near/app/src/main/java/com/alarmy/near/model/friendsummary/FriendSummary.kt
similarity index 69%
rename from Near/app/src/main/java/com/alarmy/near/model/FriendSummary.kt
rename to Near/app/src/main/java/com/alarmy/near/model/friendsummary/FriendSummary.kt
index 49249003..7d5799e3 100644
--- a/Near/app/src/main/java/com/alarmy/near/model/FriendSummary.kt
+++ b/Near/app/src/main/java/com/alarmy/near/model/friendsummary/FriendSummary.kt
@@ -1,4 +1,4 @@
-package com.alarmy.near.model
+package com.alarmy.near.model.friendsummary
import androidx.compose.runtime.Immutable
@@ -9,5 +9,5 @@ data class FriendSummary(
val profileImageUrl: String?,
val lastContactedAt: String?,
val isContacted: Boolean,
- val contactFrequency: ContactFrequency,
+ val contactFrequencyLevel: ContactFrequencyLevel,
)
diff --git a/Near/app/src/main/java/com/alarmy/near/model/monthly/MonthlyFriendType.kt b/Near/app/src/main/java/com/alarmy/near/model/monthly/MonthlyFriendType.kt
index 553d6939..0efc9fb0 100644
--- a/Near/app/src/main/java/com/alarmy/near/model/monthly/MonthlyFriendType.kt
+++ b/Near/app/src/main/java/com/alarmy/near/model/monthly/MonthlyFriendType.kt
@@ -3,6 +3,7 @@ package com.alarmy.near.model.monthly
import androidx.annotation.DrawableRes
import com.alarmy.near.R
+// TODO Drawable Res UI-Layer 이동
enum class MonthlyFriendType(
@param:DrawableRes val imageSrc: Int,
) {
diff --git a/Near/app/src/main/java/com/alarmy/near/network/auth/TestTokenInterceptor.kt b/Near/app/src/main/java/com/alarmy/near/network/auth/TestTokenInterceptor.kt
new file mode 100644
index 00000000..f1b24c8f
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/network/auth/TestTokenInterceptor.kt
@@ -0,0 +1,21 @@
+package com.alarmy.near.network.auth
+
+import com.alarmy.near.BuildConfig
+import okhttp3.Interceptor
+import okhttp3.Response
+import javax.inject.Inject
+
+class TestTokenInterceptor
+ @Inject
+ constructor() : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+ return chain.proceed(
+ request =
+ request
+ .newBuilder()
+ .addHeader("Authorization", "Bearer ${BuildConfig.TEMP_TOKEN}")
+ .build(),
+ )
+ }
+ }
diff --git a/Near/app/src/main/java/com/alarmy/near/network/di/NetworkModule.kt b/Near/app/src/main/java/com/alarmy/near/network/di/NetworkModule.kt
index e6663ac9..2b3938d7 100644
--- a/Near/app/src/main/java/com/alarmy/near/network/di/NetworkModule.kt
+++ b/Near/app/src/main/java/com/alarmy/near/network/di/NetworkModule.kt
@@ -1,6 +1,7 @@
package com.alarmy.near.network.di
import com.alarmy.near.BuildConfig
+import com.alarmy.near.network.auth.TestTokenInterceptor
import com.alarmy.near.network.auth.TokenAuthenticator
import com.alarmy.near.network.auth.TokenInterceptor
import dagger.Module
@@ -34,12 +35,14 @@ object NetworkModule {
loggingInterceptor: HttpLoggingInterceptor,
tokenInterceptor: TokenInterceptor,
tokenAuthenticator: TokenAuthenticator,
+ testTokenInterceptor: TestTokenInterceptor,
): OkHttpClient =
OkHttpClient
.Builder()
.addInterceptor(loggingInterceptor)
- .addInterceptor(tokenInterceptor)
- .authenticator(tokenAuthenticator)
+ .addInterceptor(testTokenInterceptor)
+// .addInterceptor(tokenInterceptor)
+// .authenticator(tokenAuthenticator)
.build()
@Provides
diff --git a/Near/app/src/main/java/com/alarmy/near/network/request/FriendRequest.kt b/Near/app/src/main/java/com/alarmy/near/network/request/FriendRequest.kt
new file mode 100644
index 00000000..fc9878e2
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/network/request/FriendRequest.kt
@@ -0,0 +1,27 @@
+package com.alarmy.near.network.request
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class FriendRequest(
+ val name: String,
+ val relation: String,
+ val contactFrequency: ContactFrequencyRequest,
+ val birthday: String?,
+ val anniversaryList: List,
+ val memo: String?,
+ val phone: String?,
+)
+
+@Serializable
+data class ContactFrequencyRequest(
+ val contactWeek: String,
+ val dayOfWeek: String,
+)
+
+@Serializable
+data class AnniversaryRequest(
+ val id: Int? = null,
+ val title: String,
+ val date: String? = null,
+)
diff --git a/Near/app/src/main/java/com/alarmy/near/network/response/CommonMessageEntity.kt b/Near/app/src/main/java/com/alarmy/near/network/response/CommonMessageEntity.kt
new file mode 100644
index 00000000..6566e2bf
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/network/response/CommonMessageEntity.kt
@@ -0,0 +1,8 @@
+package com.alarmy.near.network.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CommonMessageEntity(
+ val message: String,
+)
diff --git a/Near/app/src/main/java/com/alarmy/near/network/response/FriendEntity.kt b/Near/app/src/main/java/com/alarmy/near/network/response/FriendEntity.kt
index 7ec3ab3d..dcea2b5c 100644
--- a/Near/app/src/main/java/com/alarmy/near/network/response/FriendEntity.kt
+++ b/Near/app/src/main/java/com/alarmy/near/network/response/FriendEntity.kt
@@ -1,15 +1,31 @@
package com.alarmy.near.network.response
+import com.alarmy.near.model.DayOfWeek
import kotlinx.serialization.Serializable
@Serializable
data class FriendEntity(
val friendId: String,
- val position: Int,
- val source: String,
+ val imageUrl: String?,
+ val relation: String,
val name: String,
- val imageUrl: String? = null,
- val fileName: String? = null,
- val checkRate: Int,
- val lastContactAt: String? = null,
+ val contactFrequency: ContactFrequencyEntity,
+ val birthday: String?,
+ val anniversaryList: List,
+ val memo: String?,
+ val phone: String?,
+ val lastContactAt: String?,
+)
+
+@Serializable
+data class ContactFrequencyEntity(
+ val contactWeek: String,
+ val dayOfWeek: String,
+)
+
+@Serializable
+data class AnniversaryEntity(
+ val id: Int,
+ val title: String,
+ val date: String,
)
diff --git a/Near/app/src/main/java/com/alarmy/near/network/response/FriendRecordEntity.kt b/Near/app/src/main/java/com/alarmy/near/network/response/FriendRecordEntity.kt
new file mode 100644
index 00000000..577349c4
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/network/response/FriendRecordEntity.kt
@@ -0,0 +1,9 @@
+package com.alarmy.near.network.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class FriendRecordEntity(
+ val isChecked: Boolean,
+ val createdAt: String,
+)
diff --git a/Near/app/src/main/java/com/alarmy/near/network/response/FriendSummaryEntity.kt b/Near/app/src/main/java/com/alarmy/near/network/response/FriendSummaryEntity.kt
new file mode 100644
index 00000000..f6a6c55b
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/network/response/FriendSummaryEntity.kt
@@ -0,0 +1,15 @@
+package com.alarmy.near.network.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class FriendSummaryEntity(
+ val friendId: String,
+ val position: Int,
+ val source: String,
+ val name: String,
+ val imageUrl: String? = null,
+ val fileName: String? = null,
+ val checkRate: Int,
+ val lastContactAt: String? = null,
+)
diff --git a/Near/app/src/main/java/com/alarmy/near/network/service/FriendService.kt b/Near/app/src/main/java/com/alarmy/near/network/service/FriendService.kt
index 0beca5dc..6aafb65d 100644
--- a/Near/app/src/main/java/com/alarmy/near/network/service/FriendService.kt
+++ b/Near/app/src/main/java/com/alarmy/near/network/service/FriendService.kt
@@ -1,13 +1,48 @@
package com.alarmy.near.network.service
+import com.alarmy.near.network.request.FriendRequest
+import com.alarmy.near.network.response.CommonMessageEntity
import com.alarmy.near.network.response.FriendEntity
+import com.alarmy.near.network.response.FriendRecordEntity
+import com.alarmy.near.network.response.FriendSummaryEntity
import com.alarmy.near.network.response.MonthlyFriendEntity
+import retrofit2.http.Body
+import retrofit2.http.DELETE
import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.PUT
+import retrofit2.http.Path
interface FriendService {
@GET("/friend/list")
- suspend fun fetchFriends(): List
+ suspend fun fetchFriends(): List
@GET("/friend/monthly")
suspend fun fetchMonthlyFriends(): List
+
+ @GET("/friend/{friendId}")
+ suspend fun fetchFriendById(
+ @Path("friendId") friendId: String,
+ ): FriendEntity
+
+ @PUT("/friend/{friendId}")
+ suspend fun updateFriend(
+ @Path("friendId") friendId: String,
+ @Body friendRequest: FriendRequest,
+ ): FriendEntity
+
+ @DELETE("/friend/{friendId}")
+ suspend fun deleteFriend(
+ @Path("friendId") friendId: String,
+ )
+
+ @GET("/friend/record/{friendId}")
+ suspend fun fetchFriendRecord(
+ @Path("friendId") friendId: String,
+ ): List
+
+ @POST("/friend/record/{friendId}")
+ suspend fun recordContact(
+ @Path("friendId") friendId: String,
+ ): CommonMessageEntity
}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt
index 95e1a2d5..b9376bb5 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt
@@ -12,17 +12,23 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
@@ -31,12 +37,16 @@ import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -45,252 +55,452 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.alarmy.near.R
+import com.alarmy.near.model.ContactFrequency
+import com.alarmy.near.model.DayOfWeek
+import com.alarmy.near.model.Friend
+import com.alarmy.near.model.FriendRecord
+import com.alarmy.near.model.Relation
+import com.alarmy.near.model.ReminderInterval
import com.alarmy.near.presentation.feature.friendprofile.component.CallButton
import com.alarmy.near.presentation.feature.friendprofile.component.MessageButton
+import com.alarmy.near.presentation.feature.friendprofile.uistate.FriendProfileUIEvent
+import com.alarmy.near.presentation.feature.friendprofile.uistate.FriendShipRecordState
+import com.alarmy.near.presentation.feature.friendprofile.uistate.FriendState
+import com.alarmy.near.presentation.ui.component.NearFrame
import com.alarmy.near.presentation.ui.component.appbar.NearTopAppbar
import com.alarmy.near.presentation.ui.component.button.NearSolidTypeButton
+import com.alarmy.near.presentation.ui.component.dropdown.NearDropdownMenu
+import com.alarmy.near.presentation.ui.component.dropdown.NearDropdownMenuItem
import com.alarmy.near.presentation.ui.extension.onNoRippleClick
import com.alarmy.near.presentation.ui.theme.NearTheme
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
@Composable
fun FriendProfileRoute(
+ viewModel: FriendProfileViewModel = hiltViewModel(),
onShowErrorSnackBar: (throwable: Throwable?) -> Unit,
onClickBackButton: () -> Unit = {},
+ onEditFriendInfo: (Friend) -> Unit = {},
+ onClickCallButton: (phoneNumber: String) -> Unit = {},
+ onClickMessageButton: (phoneNumber: String) -> Unit = {},
+ onDeleteFriendSuccess: (friendId: String) -> Unit = {},
) {
+ val friendState = viewModel.friendFlow.collectAsStateWithLifecycle()
+ val friendShipRecordState = viewModel.friendShipRecordStateFlow.collectAsStateWithLifecycle()
+ val recordSuccessDialogState = remember { mutableStateOf(false) }
+ LaunchedEffect(viewModel.uiEvent) {
+ launch {
+ viewModel.uiEvent.collect { event ->
+ when (event) {
+ is FriendProfileUIEvent.NetworkError -> {
+ onShowErrorSnackBar(IllegalStateException("네트워크 에러가 발생했습니다."))
+ }
+
+ is FriendProfileUIEvent.DeleteFriendSuccess -> {
+ onDeleteFriendSuccess(event.friendId)
+ }
+
+ is FriendProfileUIEvent.RecordFriendShipSuccess -> {
+ recordSuccessDialogState.value = true
+ }
+ }
+ }
+ }
+ }
FriendProfileScreen(
+ friendState = friendState.value,
+ friendShipRecordState = friendShipRecordState.value,
+ recordSuccessDialogState = recordSuccessDialogState.value,
onClickBackButton = onClickBackButton,
+ onEditFriendInfo = onEditFriendInfo,
+ onClickCallButton = onClickCallButton,
+ onClickMessageButton = onClickMessageButton,
+ onRecordFriendShip = viewModel::onRecordFriendShip,
+ onDeleteFriend = viewModel::onDeleteFriend,
+ onDismissRecordSuccessDialog = {
+ recordSuccessDialogState.value = false
+ },
)
}
@Composable
fun FriendProfileScreen(
modifier: Modifier = Modifier,
+ friendState: FriendState,
+ friendShipRecordState: FriendShipRecordState,
+ recordSuccessDialogState: Boolean = false,
onClickBackButton: () -> Unit = {},
+ onEditFriendInfo: (Friend) -> Unit = {},
+ onClickCallButton: (phoneNumber: String) -> Unit = {},
+ onClickMessageButton: (phoneNumber: String) -> Unit = {},
+ onRecordFriendShip: (friendId: String) -> Unit = {},
+ onDeleteFriend: (friendId: String) -> Unit = {},
+ onDismissRecordSuccessDialog: () -> Unit = {},
) {
- // TODO Home 머지시 상단 패딩 + status 색상 변경
val currentTabPosition = remember { mutableIntStateOf(0) }
- Box(modifier = modifier.padding(bottom = 24.dp)) {
- Column(
- modifier =
- Modifier
- .align(Alignment.TopStart)
- .fillMaxSize()
- .background(NearTheme.colors.WHITE_FFFFFF),
- ) {
- NearTopAppbar(
- title = "프로필 상세",
- onClickBackButton = onClickBackButton,
- menuButton = {
- Image(
- modifier = Modifier.onNoRippleClick(onClick = {}).padding(end = 20.dp),
- painter = painterResource(R.drawable.ic_32_menu),
- contentDescription = stringResource(R.string.common_menu_button_description),
- )
- },
- )
- Spacer(modifier = Modifier.height(18.dp))
- Row(
- modifier =
- Modifier
- .fillMaxWidth()
- .padding(horizontal = 32.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Box(
- modifier =
- Modifier,
- ) {
- Image(
- modifier = Modifier.align(Alignment.Center),
- painter = painterResource(R.drawable.img_80_user1),
- contentDescription = null,
- )
- Image(
+ val dropdownState = remember { mutableStateOf(false) }
+
+ NearFrame(modifier = modifier) {
+ Box {
+ when (friendState) {
+ is FriendState.Success -> {
+ val friend = friendState.friend
+ Column(
modifier =
Modifier
- .align(Alignment.TopEnd)
- .offset(x = 2.dp, y = (-2).dp),
- painter = painterResource(R.drawable.ic_visual_24_emoji_100),
- contentDescription = null,
- )
- }
- Spacer(modifier = Modifier.width(24.dp))
- Column {
- Text(
- modifier = Modifier.widthIn(max = 145.dp),
- text = "일이삼사오육칠",
- style = NearTheme.typography.B1_16_BOLD,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- )
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = "3월 22일 더 가까워졌어요",
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.BLUE01_5AA2E9,
- )
- }
- }
- Spacer(modifier = Modifier.height(24.dp))
- Row(
- modifier =
- Modifier
- .fillMaxWidth()
- .padding(horizontal = 20.dp),
- ) {
- CallButton(modifier = Modifier.weight(1f), onClick = {})
- Spacer(modifier = Modifier.width(7.dp))
- MessageButton(Modifier.weight(1f), onClick = {})
- }
- Spacer(modifier = Modifier.height(24.dp))
- TabRow(
- modifier =
- Modifier
- .padding(horizontal = 25.dp)
- .width(170.dp),
- containerColor = NearTheme.colors.WHITE_FFFFFF,
- selectedTabIndex = 0,
- divider = {},
- indicator = {
- TabRowDefaults.SecondaryIndicator(
+ .align(Alignment.TopStart)
+ .fillMaxSize()
+ .background(NearTheme.colors.WHITE_FFFFFF),
+ ) {
+ if (recordSuccessDialogState) {
+ LaunchedEffect(true) {
+ if (recordSuccessDialogState) {
+ delay(2000L)
+ onDismissRecordSuccessDialog()
+ }
+ }
+ Dialog(onDismissRequest = onDismissRecordSuccessDialog) {
+ Column(
+ modifier =
+ Modifier
+ .width(255.dp)
+ .height(186.dp)
+ .background(
+ color = NearTheme.colors.WHITE_FFFFFF,
+ shape = RoundedCornerShape(16.dp),
+ ),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ painterResource(R.drawable.img_100_character_success),
+ contentDescription = "",
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ stringResource(R.string.friend_profile_info_contact_success_text),
+ style = NearTheme.typography.B1_16_BOLD,
+ color = Color(0xff222222),
+ )
+ }
+ }
+ }
+ NearTopAppbar(
+ title = stringResource(R.string.friend_profile_title),
+ onClickBackButton = onClickBackButton,
+ menuButton = {
+ Column(modifier = Modifier.padding(end = 20.dp)) {
+ Image(
+ modifier =
+ Modifier
+ .onNoRippleClick(onClick = {
+ dropdownState.value = true
+ }),
+ painter = painterResource(R.drawable.ic_32_menu),
+ contentDescription = stringResource(R.string.common_menu_button_description),
+ )
+ NearDropdownMenu(
+ expanded = dropdownState.value,
+ onDismissRequest = { dropdownState.value = false },
+ ) {
+ NearDropdownMenuItem(
+ onClick = {
+ onEditFriendInfo(friend)
+ dropdownState.value = false
+ },
+ text = stringResource(R.string.friend_profile_info_edit)
+ )
+ NearDropdownMenuItem(
+ onClick = {
+ onDeleteFriend(friend.friendId)
+ dropdownState.value = false
+ },
+ text = stringResource(R.string.friend_profile_info_delete)
+ )
+ }
+ }
+ },
+ )
+
+ Spacer(modifier = Modifier.height(18.dp))
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 32.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Box(
+ modifier =
+ Modifier,
+ ) {
+ Image(
+ modifier = Modifier.align(Alignment.Center),
+ painter = painterResource(R.drawable.img_80_user1),
+ contentDescription = null,
+ )
+ Image(
+ modifier =
+ Modifier
+ .align(Alignment.TopEnd)
+ .offset(x = 2.dp, y = (-2).dp),
+ painter = painterResource(R.drawable.ic_visual_24_emoji_0),
+ contentDescription = null,
+ )
+ }
+ Spacer(modifier = Modifier.width(24.dp))
+ Column {
+ Text(
+ modifier = Modifier.widthIn(max = 145.dp),
+ text = friend.name,
+ style = NearTheme.typography.B1_16_BOLD,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+ if (friend.lastContactAt != null) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text =
+ stringResource(
+ R.string.friend_profile_last_contact_date_format,
+ friend.lastContactAt.lastContactFormat(),
+ ),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.BLUE01_5AA2E9,
+ )
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(24.dp))
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ ) {
+ CallButton(
+ modifier = Modifier.weight(1f),
+ enabled = !friend.phone.isNullOrBlank(),
+ onClick = {
+ friend.phone?.let {
+ onClickCallButton(friend.phone)
+ }
+ },
+ )
+ Spacer(modifier = Modifier.width(7.dp))
+ MessageButton(
+ Modifier.weight(1f),
+ enabled = !friend.phone.isNullOrBlank(),
+ onClick = {
+ friend.phone?.let {
+ onClickMessageButton(friend.phone)
+ }
+ },
+ )
+ }
+ Spacer(modifier = Modifier.height(24.dp))
+ TabRow(
+ modifier =
+ Modifier
+ .padding(horizontal = 25.dp)
+ .width(170.dp),
+ containerColor = NearTheme.colors.WHITE_FFFFFF,
+ selectedTabIndex = 0,
+ divider = {},
+ indicator = {
+ TabRowDefaults.SecondaryIndicator(
+ modifier =
+ Modifier
+ .customTabIndicatorOffset(
+ it[currentTabPosition.intValue],
+ 80.dp,
+ ), // 넓이, 애니메이션 지정
+ // 모양 지정
+ height = 3.dp,
+ color = NearTheme.colors.BLUE01_5AA2E9,
+ )
+ },
+ ) {
+ Tab(
+ modifier =
+ Modifier
+ .width(85.dp)
+ .height(50.dp),
+ selected = true,
+ onClick = {
+ currentTabPosition.intValue = 0
+ },
+ ) {
+ if (currentTabPosition.intValue == 0) {
+ Text(
+ text = stringResource(R.string.friend_profile_tab_text_profile),
+ style = NearTheme.typography.B2_14_BOLD,
+ color = NearTheme.colors.BLACK_1A1A1A,
+ )
+ } else {
+ Text(
+ text = stringResource(R.string.friend_profile_tab_text_profile),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.GRAY02_B7B7B7,
+ )
+ }
+ }
+ Tab(
+ modifier =
+ Modifier
+ .width(85.dp)
+ .height(50.dp),
+ selected = true,
+ onClick = {
+ currentTabPosition.intValue = 1
+ },
+ ) {
+ if (currentTabPosition.intValue == 1) {
+ Text(
+ text = stringResource(R.string.friend_profile_tab_text_record),
+ style = NearTheme.typography.B2_14_BOLD,
+ color = NearTheme.colors.BLACK_1A1A1A,
+ )
+ } else {
+ Text(
+ text = stringResource(R.string.friend_profile_tab_text_record),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.GRAY02_B7B7B7,
+ )
+ }
+ }
+ }
+ HorizontalDivider(thickness = 1.dp, color = NearTheme.colors.GRAY03_EBEBEB)
+ if (currentTabPosition.intValue == 0) {
+ ProfileTab(friend = friend)
+ } else {
+ RecordTab(friendShipRecordState = friendShipRecordState)
+ }
+ Spacer(modifier = Modifier.height(60.dp))
+ }
+ NearSolidTypeButton(
modifier =
Modifier
- .customTabIndicatorOffset(
- it[currentTabPosition.intValue],
- 80.dp,
- ), // 넓이, 애니메이션 지정
- // 모양 지정
- height = 3.dp,
- color = NearTheme.colors.BLUE01_5AA2E9,
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp)
+ .align(Alignment.BottomCenter),
+ contentPadding = PaddingValues(vertical = 17.dp),
+ enabled = friend.isContactedToday.not(),
+ onClick = { onRecordFriendShip(friend.friendId) },
+ text = stringResource(R.string.friend_profile_record_button_text),
)
- },
- ) {
- Tab(
- modifier =
- Modifier
- .width(85.dp)
- .height(50.dp),
- selected = true,
- onClick = {
- currentTabPosition.intValue = 0
- },
- ) {
- if (currentTabPosition.intValue == 0) {
- Text(
- text = stringResource(R.string.friend_profile_tab_text_profile),
- style = NearTheme.typography.B2_14_BOLD,
- color = NearTheme.colors.BLACK_1A1A1A,
- )
- } else {
- Text(
- text = stringResource(R.string.friend_profile_tab_text_profile),
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.GRAY02_B7B7B7,
- )
+ }
+
+ is FriendState.Loading -> {
+ Box(modifier = Modifier.fillMaxSize()) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
- Tab(
- modifier =
- Modifier
- .width(85.dp)
- .height(50.dp),
- selected = true,
- onClick = {
- currentTabPosition.intValue = 1
- },
- ) {
- if (currentTabPosition.intValue == 1) {
- Text(
- text = stringResource(R.string.friend_profile_tab_text_record),
- style = NearTheme.typography.B2_14_BOLD,
- color = NearTheme.colors.BLACK_1A1A1A,
- )
- } else {
+
+ is FriendState.Error -> {
+ Box(modifier = Modifier.fillMaxSize()) {
Text(
- text = stringResource(R.string.friend_profile_tab_text_record),
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.GRAY02_B7B7B7,
+ modifier = Modifier.align(Alignment.Center),
+ text = "프로필 정보를 불러오는데 실패했습니다.",
)
}
}
}
- HorizontalDivider(thickness = 1.dp, color = NearTheme.colors.GRAY03_EBEBEB)
- if (currentTabPosition.intValue == 0) {
- ProfileTab()
- } else {
- RecordTab()
- }
}
- NearSolidTypeButton(
- modifier =
- Modifier
- .fillMaxWidth()
- .padding(horizontal = 20.dp)
- .align(Alignment.BottomCenter),
- contentPadding = PaddingValues(vertical = 17.dp),
- enabled = true,
- onClick = {},
- text = stringResource(R.string.friend_profile_record_button_text),
- )
}
}
@Composable
-private fun ProfileTab(modifier: Modifier = Modifier) {
+private fun ProfileTab(
+ modifier: Modifier = Modifier,
+ friend: Friend,
+) {
Column(modifier = modifier) {
Spacer(modifier = Modifier.height(32.dp))
ProfileDetailInfo(
category = stringResource(R.string.friend_profile_info_category_relation),
- content = "친구",
+ content = stringResource(friend.relation.resId),
)
Spacer(modifier = Modifier.height(16.dp))
ProfileDetailInfo(
category = stringResource(R.string.friend_profile_info_category_term_of_contact),
- content = "2주",
+ content = stringResource(friend.contactFrequency.reminderInterval.labelRes),
)
Spacer(modifier = Modifier.height(16.dp))
ProfileDetailInfo(
category = stringResource(R.string.friend_profile_info_category_birthday),
- content = "1996.03.21",
+ content = friend.birthday?.replace("-", ".") ?: "-",
)
Spacer(modifier = Modifier.height(16.dp))
ProfileDetailInfo(
category = stringResource(R.string.friend_profile_info_category_anniversary),
- content = "결혼기념일 (2020.06.24)",
+ content =
+ friend.anniversaryList.joinToString(" ") {
+ "${it.title} (${it.date})"
+ },
)
Spacer(modifier = Modifier.height(16.dp))
ProfileMemoInfo(
- content = null,
+ content = friend.memo,
)
}
}
@Composable
-private fun RecordTab(modifier: Modifier = Modifier) {
+private fun RecordTab(
+ modifier: Modifier = Modifier,
+ friendShipRecordState: FriendShipRecordState,
+) {
Column(modifier = modifier.padding(horizontal = 24.dp)) {
Spacer(modifier = Modifier.height(24.dp))
Text(
- "챙김 기록",
+ stringResource(R.string.friend_profile_info_record_title_text),
style = NearTheme.typography.B2_14_BOLD,
color = NearTheme.colors.BLACK_1A1A1A,
)
- Spacer(modifier = Modifier.height(13.dp))
-
- LazyVerticalGrid(
- GridCells.Fixed(3),
- verticalArrangement = Arrangement.spacedBy(24.dp),
- contentPadding = PaddingValues(bottom = 60.dp),
- ) {
- items(15) {
- RecordItem()
+ if (friendShipRecordState.isEmpty) {
+ Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
+ Spacer(modifier = Modifier.height(60.dp))
+ Image(painterResource(R.drawable.img_100_character_empty), contentDescription = null)
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ stringResource(R.string.friend_profile_info_empty_contact_friend),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.GRAY01_888888,
+ )
+ }
+ } else {
+ Spacer(modifier = Modifier.height(13.dp))
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(3),
+ verticalArrangement = Arrangement.spacedBy(24.dp),
+ contentPadding = PaddingValues(bottom = 60.dp),
+ ) {
+ items(friendShipRecordState.records.size) {
+ RecordItem(
+ friendRecord = friendShipRecordState.records[friendShipRecordState.records.size - 1 - it],
+ index =
+ friendShipRecordState.records.size - it,
+ )
+ }
}
}
}
}
@Composable
-private fun RecordItem(modifier: Modifier = Modifier) {
+private fun RecordItem(
+ modifier: Modifier = Modifier,
+ index: Int,
+ friendRecord: FriendRecord,
+) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
@@ -320,7 +530,7 @@ private fun RecordItem(modifier: Modifier = Modifier) {
contentDescription = null,
)
Text(
- "11번째 챙김",
+ stringResource(R.string.friend_profile_info_contact_record_text, index),
style = NearTheme.typography.B2_14_MEDIUM,
color = NearTheme.colors.BLUE01_5AA2E9,
)
@@ -328,7 +538,7 @@ private fun RecordItem(modifier: Modifier = Modifier) {
}
Spacer(modifier = Modifier.height(8.dp))
Text(
- "25.03.20",
+ friendRecord.createdAt,
style = NearTheme.typography.B2_14_MEDIUM,
color = NearTheme.colors.GRAY01_888888,
)
@@ -455,10 +665,48 @@ fun Modifier.customTabIndicatorOffset(
.width(currentTabWidth)
}
+private fun String.lastContactFormat(): String {
+ val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
+ val outputFormatter = DateTimeFormatter.ofPattern("M월 d일")
+
+ val date = LocalDate.parse(this, inputFormatter)
+ return date.format(outputFormatter)
+}
+
@Preview(showBackground = true)
@Composable
fun FriendProfileScreenPreview() {
NearTheme {
- FriendProfileScreen()
+ FriendProfileScreen(
+ friendState =
+ FriendState.Success(
+ Friend(
+ friendId = "adfaggasf",
+ imageUrl = "",
+ relation = Relation.FRIEND,
+ name = "",
+ contactFrequency =
+ ContactFrequency(
+ reminderInterval = ReminderInterval.EVERY_TWO_WEEK,
+ dayOfWeek = DayOfWeek.THURSDAY,
+ ),
+ birthday = "1998-11-13",
+ anniversaryList = listOf(),
+ memo = "",
+ phone = "",
+ lastContactAt = "",
+ ),
+ ),
+ friendShipRecordState =
+ FriendShipRecordState(
+ records =
+ List(5) {
+ FriendRecord(
+ isChecked = true,
+ createdAt = "2023-11-1$it",
+ )
+ },
+ ),
+ )
}
}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileViewModel.kt
new file mode 100644
index 00000000..141e4594
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileViewModel.kt
@@ -0,0 +1,148 @@
+package com.alarmy.near.presentation.feature.friendprofile
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
+import com.alarmy.near.data.repository.FriendRepository
+import com.alarmy.near.model.Friend
+import com.alarmy.near.model.FriendRecord
+import com.alarmy.near.presentation.feature.friendprofile.navigation.RouteFriendProfile
+import com.alarmy.near.presentation.feature.friendprofile.uistate.FriendProfileUIEvent
+import com.alarmy.near.presentation.feature.friendprofile.uistate.FriendShipRecordState
+import com.alarmy.near.presentation.feature.friendprofile.uistate.FriendState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+import java.util.Locale
+import javax.inject.Inject
+
+@HiltViewModel
+class FriendProfileViewModel
+ @Inject
+ constructor(
+ savedStateHandle: SavedStateHandle,
+ private val friendRepository: FriendRepository,
+ ) : ViewModel() {
+ private val friendId: String =
+ savedStateHandle.toRoute().friendId
+ private val _uiEvent = Channel()
+ val uiEvent = _uiEvent.receiveAsFlow()
+ private val _friendFlow: MutableStateFlow = MutableStateFlow(FriendState.Loading)
+ val friendFlow: StateFlow = _friendFlow.asStateFlow()
+
+ private val _friendShipRecordStateFlow: MutableStateFlow =
+ MutableStateFlow(FriendShipRecordState(isLoading = true))
+ val friendShipRecordStateFlow: StateFlow =
+ _friendShipRecordStateFlow.asStateFlow()
+
+ init {
+ fetchFriend()
+ fetchFriendShipRecord()
+ }
+
+ fun fetchFriend() {
+ friendRepository
+ .fetchFriendById(friendId)
+ .onEach { friend ->
+ _friendFlow.value = FriendState.Success(friend)
+ }.catch { error ->
+ _friendFlow.value = FriendState.Error("데이터를 가져오는데 실패했습니다.")
+ _uiEvent.send(FriendProfileUIEvent.NetworkError) // UI에서 단발성 이벤트로도 쓸 수 있음
+ }.launchIn(viewModelScope)
+ }
+
+ fun fetchFriendShipRecord() {
+ friendRepository
+ .fetchFriendRecord(friendId)
+ .onEach { records ->
+ _friendShipRecordStateFlow.update {
+ it.copy(
+ records = records.filter { record -> record.isChecked },
+ isEmpty = records.isEmpty(),
+ isLoading = false,
+ )
+ }
+ }.catch { error ->
+ _friendShipRecordStateFlow.update {
+ it.copy(
+ isEmpty = true,
+ isLoading = false,
+ )
+ }
+ _uiEvent.send(FriendProfileUIEvent.NetworkError) // UI에서 단발성 이벤트로도 쓸 수 있음
+ }.launchIn(viewModelScope)
+ }
+
+ fun onDeleteFriend(friendId: String) {
+ friendRepository
+ .deleteFriend(friendId)
+ .onEach {
+ _uiEvent.send(FriendProfileUIEvent.DeleteFriendSuccess(friendId))
+ // event
+ }.catch { error ->
+ _uiEvent.send(FriendProfileUIEvent.NetworkError) // UI에서 단발성 이벤트로도 쓸 수 있음
+ }.launchIn(viewModelScope)
+ }
+
+ fun onRecordFriendShip(friendId: String) {
+ friendRepository
+ .recordContact(friendId) // 내 현재 시간 가져와서
+ .onEach { _ ->
+ _uiEvent.send(FriendProfileUIEvent.RecordFriendShipSuccess)
+ _friendShipRecordStateFlow.update { recordState ->
+ recordState.copy(
+ records =
+ listOf(
+ FriendRecord(
+ isChecked = true,
+ createdAt = getTodayShortFormat(),
+ ),
+ ) + (recordState.records),
+ )
+ }
+ if (friendFlow.value is FriendState.Success) {
+ _friendFlow.update {
+ (it as FriendState.Success).copy(
+ friend =
+ it.friend.copy(
+ lastContactAt = getTodayDashFormat(),
+ ),
+ )
+ }
+ }
+
+ // event
+ }.catch { error ->
+ _uiEvent.send(FriendProfileUIEvent.NetworkError) // UI에서 단발성 이벤트로도 쓸 수 있음
+ }.launchIn(viewModelScope)
+ }
+
+ private fun getTodayShortFormat(): String {
+ val today = LocalDate.now()
+ val formatter = DateTimeFormatter.ofPattern("yy.MM.dd", Locale.KOREA)
+ return today.format(formatter)
+ }
+
+ private fun getTodayDashFormat(): String {
+ val today = LocalDate.now()
+ val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.KOREA)
+ return today.format(formatter)
+ }
+
+ fun updateFriend(friend: Friend) {
+ if (_friendFlow.value is FriendState.Success) {
+ _friendFlow.update { (it as FriendState.Success).copy(friend = friend) }
+ }
+ }
+ }
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/navigation/FriendProfileNavigation.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/navigation/FriendProfileNavigation.kt
index c3b4608a..d22a48d3 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/navigation/FriendProfileNavigation.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/navigation/FriendProfileNavigation.kt
@@ -1,16 +1,32 @@
package com.alarmy.near.presentation.feature.friendprofile.navigation
+import android.os.Build
+import android.os.Parcelable
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
+import androidx.navigation.NavType
import androidx.navigation.compose.composable
+import androidx.navigation.toRoute
+import androidx.savedstate.SavedState
+import com.alarmy.near.model.Friend
import com.alarmy.near.presentation.feature.friendprofile.FriendProfileRoute
+import com.alarmy.near.presentation.feature.friendprofile.FriendProfileViewModel
+import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.FRIEND_PROFILE_EDIT_COMPLETE_KEY
+import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.FriendType
+import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.RouteFriendProfileEditor
+import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import kotlin.reflect.KType
+import kotlin.reflect.typeOf
+@Parcelize
@Serializable
data class RouteFriendProfile(
val friendId: String,
-)
+) : Parcelable
fun NavController.navigateToFriendProfile(
friendId: String,
@@ -22,11 +38,23 @@ fun NavController.navigateToFriendProfile(
fun NavGraphBuilder.friendProfileNavGraph(
onShowErrorSnackBar: (throwable: Throwable?) -> Unit,
onClickBackButton: () -> Unit,
+ onEditFriendInfo: (Friend) -> Unit = {},
+ onClickCallButton: (phoneNumber: String) -> Unit = {},
+ onClickMessageButton: (phoneNumber: String) -> Unit = {},
) {
composable { backStackEntry ->
+ val viewModel: FriendProfileViewModel = hiltViewModel()
+ val friend = backStackEntry.savedStateHandle.get(FRIEND_PROFILE_EDIT_COMPLETE_KEY)
+ friend?.let {
+ viewModel.updateFriend(it)
+ }
FriendProfileRoute(
+ viewModel = viewModel,
onShowErrorSnackBar = onShowErrorSnackBar,
onClickBackButton = onClickBackButton,
+ onEditFriendInfo = onEditFriendInfo,
+ onClickCallButton = onClickCallButton,
+ onClickMessageButton = onClickMessageButton,
)
}
}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/uistate/FriendProfileUIEvent.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/uistate/FriendProfileUIEvent.kt
new file mode 100644
index 00000000..87133fef
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/uistate/FriendProfileUIEvent.kt
@@ -0,0 +1,11 @@
+package com.alarmy.near.presentation.feature.friendprofile.uistate
+
+sealed interface FriendProfileUIEvent {
+ data object NetworkError : FriendProfileUIEvent
+
+ data class DeleteFriendSuccess(
+ val friendId: String,
+ ) : FriendProfileUIEvent
+
+ data object RecordFriendShipSuccess : FriendProfileUIEvent
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/uistate/FriendShipRecordState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/uistate/FriendShipRecordState.kt
new file mode 100644
index 00000000..8fa62e68
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/uistate/FriendShipRecordState.kt
@@ -0,0 +1,9 @@
+package com.alarmy.near.presentation.feature.friendprofile.uistate
+
+import com.alarmy.near.model.FriendRecord
+
+data class FriendShipRecordState(
+ val records: List = emptyList(),
+ val isEmpty: Boolean = true,
+ val isLoading: Boolean = false,
+)
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/uistate/FriendState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/uistate/FriendState.kt
new file mode 100644
index 00000000..b9b089a7
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/uistate/FriendState.kt
@@ -0,0 +1,15 @@
+package com.alarmy.near.presentation.feature.friendprofile.uistate
+
+import com.alarmy.near.model.Friend
+
+sealed interface FriendState {
+ object Loading : FriendState
+
+ data class Success(
+ val friend: Friend,
+ ) : FriendState
+
+ data class Error(
+ val errorMessage: String,
+ ) : FriendState
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt
index fab2cd0f..cdcf3714 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
@@ -22,10 +23,12 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -36,23 +39,83 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.alarmy.near.R
+import com.alarmy.near.model.ContactFrequency
+import com.alarmy.near.model.DayOfWeek
+import com.alarmy.near.model.Friend
+import com.alarmy.near.model.Relation
+import com.alarmy.near.model.ReminderInterval
import com.alarmy.near.presentation.feature.friendprofileedittor.component.NearDatePicker
import com.alarmy.near.presentation.feature.friendprofileedittor.component.ReminderIntervalBottomSheet
+import com.alarmy.near.presentation.feature.friendprofileedittor.dialog.EditorExitDialog
+import com.alarmy.near.presentation.feature.friendprofileedittor.uistate.FriendProfileEditorUIEvent
+import com.alarmy.near.presentation.feature.friendprofileedittor.uistate.FriendProfileEditorUIState
+import com.alarmy.near.presentation.ui.component.NearFrame
import com.alarmy.near.presentation.ui.component.appbar.NearTopAppbar
import com.alarmy.near.presentation.ui.component.radiobutton.NearSmallRadioButton
import com.alarmy.near.presentation.ui.component.textfield.NearLimitedTextField
import com.alarmy.near.presentation.ui.component.textfield.NearTextField
import com.alarmy.near.presentation.ui.extension.onNoRippleClick
import com.alarmy.near.presentation.ui.theme.NearTheme
+import kotlinx.coroutines.launch
@Composable
fun FriendProfileEditorRoute(
+ viewModel: FriendProfileEditorViewModel = hiltViewModel(),
onShowErrorSnackBar: (throwable: Throwable?) -> Unit,
onClickBackButton: () -> Unit = {},
+ onSuccessEdit: (Friend) -> Unit = {},
) {
+ val friendProfileEditorUIState = viewModel.uiState.collectAsStateWithLifecycle()
+ val warningDialogState = remember { mutableStateOf(false) }
+ val context = LocalContext.current
+
+ LaunchedEffect(viewModel.uiEvent) {
+ launch {
+ viewModel.uiEvent.collect { event ->
+ when (event) {
+ FriendProfileEditorUIEvent.WarningExit -> {
+ warningDialogState.value = true
+ }
+
+ FriendProfileEditorUIEvent.Exit -> {
+ warningDialogState.value = false
+ onClickBackButton()
+ }
+
+ is FriendProfileEditorUIEvent.FriendProfileEditFailure -> {
+ onShowErrorSnackBar(event.throwable)
+ }
+
+ FriendProfileEditorUIEvent.FriendProfileEditNetworkError -> {
+ onShowErrorSnackBar(IllegalStateException(context.getString(R.string.network_error_message)))
+ }
+
+ is FriendProfileEditorUIEvent.FriendProfileEditSuccess -> {
+ onSuccessEdit(event.friend)
+ }
+ }
+ }
+ }
+ }
FriendProfileEditorScreen(
- onClickBackButton = onClickBackButton,
+ friendProfileEditorUIState = friendProfileEditorUIState.value,
+ dialogState = warningDialogState.value,
+ onClickBackButton = viewModel::onExit,
+ onNameChanged = viewModel::onNameChanged,
+ onRelationChanged = viewModel::onRelationChanged,
+ onReminderIntervalChanged = viewModel::onRemindIntervalChanged,
+ onBirthdayChanged = viewModel::onBirthdayChanged,
+ onAnniversaryNameChange = viewModel::onAnniversaryTitleChanged,
+ onAnniversaryDateSelected = viewModel::onAnniversaryDateChanged,
+ onRemoveAnniversary = viewModel::onRemoveAnniversary,
+ onAddAnniversary = viewModel::onAddAnniversary,
+ onMemoChanged = viewModel::onMemoChanged,
+ onSubmit = viewModel::onSubmit,
+ onEditorExit = onClickBackButton,
+ onCloseDialog = { warningDialogState.value = false },
)
}
@@ -60,317 +123,108 @@ fun FriendProfileEditorRoute(
@Composable
fun FriendProfileEditorScreen(
modifier: Modifier = Modifier,
+ dialogState: Boolean = false,
+ friendProfileEditorUIState: FriendProfileEditorUIState,
onClickBackButton: () -> Unit = {},
+ onNameChanged: (String) -> Unit = {},
+ onRelationChanged: (Relation) -> Unit = {},
+ onReminderIntervalChanged: (ReminderInterval) -> Unit = {},
+ onBirthdayChanged: (Long) -> Unit = {},
+ onAnniversaryNameChange: (index: Int, name: String) -> Unit = { _, _ -> },
+ onAnniversaryDateSelected: (index: Int, dataTimeMillis: Long) -> Unit = { _, _ -> },
+ onRemoveAnniversary: (index: Int) -> Unit = { _ -> },
+ onAddAnniversary: () -> Unit = {},
+ onMemoChanged: (String) -> Unit = {},
+ onSubmit: () -> Unit = {},
+ onEditorExit: () -> Unit = {},
+ onCloseDialog: () -> Unit = {},
) {
- val isErrorMessageVisible = remember { mutableStateOf(false) }
- val density = LocalDensity.current
- val statusBarHeightDp = with(density) { WindowInsets.statusBars.getTop(density).toDp() }
val showBottomSheet = remember { mutableStateOf(false) }
if (showBottomSheet.value) {
ReminderIntervalBottomSheet(onDismissRequest = {
showBottomSheet.value = false
+ }, onSelectReminderInterval = {
+ onReminderIntervalChanged(it)
+ showBottomSheet.value = false
})
}
- Column(
- modifier =
- modifier
- .fillMaxSize()
- .background(NearTheme.colors.WHITE_FFFFFF),
- ) {
- Spacer(modifier = Modifier.padding(top = statusBarHeightDp))
- NearTopAppbar(
- modifier = Modifier.padding(end = 24.dp),
- title = "",
- onClickBackButton = onClickBackButton,
- menuButton = {
- Text(
- text = "완료",
- style = NearTheme.typography.B1_16_BOLD,
- color = NearTheme.colors.BLACK_1A1A1A,
- )
+ if (dialogState) {
+ EditorExitDialog(
+ onDismissRequest = {
+ onCloseDialog()
+ },
+ onConfirm = {
+ onEditorExit()
},
)
- Spacer(modifier = Modifier.height(16.dp))
- Column(
- modifier =
- Modifier
- .fillMaxWidth()
- .padding(start = 24.dp, end = 20.dp),
- ) {
- Row(
- modifier =
- Modifier
- .fillMaxWidth(),
- ) {
- Text(
- modifier = Modifier.padding(top = 16.dp),
- text =
- buildAnnotatedString {
- append("이름")
- withStyle(
- style =
- SpanStyle(
- color = NearTheme.colors.BLUE01_5AA2E9,
- ),
- ) {
- append("*")
- }
- },
- textAlign = TextAlign.Center,
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.GRAY01_888888,
- )
- Spacer(modifier = Modifier.width(55.dp))
- NearTextField(
- modifier = Modifier.weight(1f),
- value = "가나다",
- onValueChange = {
- },
- )
- }
- if (isErrorMessageVisible.value) {
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- "이름을 입력해주세요.",
- style = NearTheme.typography.FC_12_MEDIUM,
- color = NearTheme.colors.NEGATIVE_F04E4E,
- )
- }
- Spacer(modifier = Modifier.height(32.dp))
- Row(
- modifier =
- Modifier
- .fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- "관계",
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.GRAY01_888888,
- )
- Spacer(modifier = Modifier.width(72.dp))
- Row(
- modifier =
- Modifier
- .weight(1f)
- .padding(end = 35.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- NearSmallRadioButton(
- selected = false,
- onClick = {},
- )
- Spacer(modifier = Modifier.width(8.dp))
- Text(
- text = stringResource(R.string.friend_profile_editor_relation_freind),
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.BLACK_1A1A1A,
- )
- }
- Row(verticalAlignment = Alignment.CenterVertically) {
- NearSmallRadioButton(
- selected = false,
- onClick = {},
- )
- Spacer(modifier = Modifier.width(8.dp))
- Text(
- text = stringResource(R.string.friend_profile_editor_relation_family),
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.BLACK_1A1A1A,
- )
- }
- Row(verticalAlignment = Alignment.CenterVertically) {
- NearSmallRadioButton(
- selected = false,
- onClick = {},
- )
- Spacer(modifier = Modifier.width(8.dp))
- Text(
- text = stringResource(R.string.friend_profile_editor_relation_acquaintance),
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.BLACK_1A1A1A,
- )
- }
- }
- }
- Spacer(modifier = Modifier.height(33.dp))
- Row(
- modifier =
- Modifier
- .fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- "연락 주기",
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.GRAY01_888888,
- )
- Spacer(modifier = Modifier.width(35.dp))
- Surface(
- modifier =
- modifier
- .weight(1f)
- .onNoRippleClick({
- showBottomSheet.value = true
- }),
- shape = RoundedCornerShape(12.dp),
- border =
- BorderStroke(
- width = 1.dp,
- color = NearTheme.colors.GRAY03_EBEBEB,
- ),
- color = NearTheme.colors.WHITE_FFFFFF,
- ) {
- Row(
- modifier =
- Modifier.padding(
- start = 16.dp,
- end = 12.dp,
- top = 14.dp,
- bottom = 14.dp,
- ),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
- ) {
+ }
+ NearFrame(modifier = modifier) {
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ item {
+ NearTopAppbar(
+ modifier = Modifier.padding(end = 24.dp),
+ title = "",
+ onClickBackButton = onClickBackButton,
+ menuButton = {
Text(
- "2주 (수요일 마다)",
- style = NearTheme.typography.B2_14_MEDIUM,
+ modifier =
+ Modifier.onNoRippleClick(onClick = {
+ onSubmit()
+ }),
+ text = stringResource(R.string.friend_profile_editor_edit_complete_text),
+ style = NearTheme.typography.B1_16_BOLD,
color = NearTheme.colors.BLACK_1A1A1A,
)
- Image(
- painter = painterResource(id = R.drawable.ic_24_down),
- contentDescription = null,
- )
- }
- }
- }
- Spacer(modifier = Modifier.height(16.dp))
- Row(
- modifier =
- Modifier
- .fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- val birthdayDatePickerState = remember { mutableStateOf(false) }
- val datePickerState =
- rememberDatePickerState()
- if (birthdayDatePickerState.value) {
- NearDatePicker(
- datePickerState = datePickerState,
- onDismiss = { birthdayDatePickerState.value = false },
- onDateSelected = {},
- )
- }
- Text(
- "생일",
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.GRAY01_888888,
+ },
)
- Spacer(modifier = Modifier.width(62.dp))
- Surface(
+ Spacer(modifier = Modifier.height(16.dp))
+ Column(
modifier =
- modifier
- .weight(1f),
- shape = RoundedCornerShape(12.dp),
- border =
- BorderStroke(
- width = 1.dp,
- color = NearTheme.colors.GRAY03_EBEBEB,
- ),
- color = NearTheme.colors.WHITE_FFFFFF,
+ Modifier
+ .fillMaxWidth()
+ .padding(start = 24.dp, end = 20.dp),
) {
Row(
modifier =
Modifier
- .padding(
- start = 16.dp,
- end = 12.dp,
- top = 14.dp,
- bottom = 14.dp,
- ).onNoRippleClick({
- birthdayDatePickerState.value = true
- }),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
+ .fillMaxWidth(),
) {
Text(
- "2020.06.24",
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.BLACK_1A1A1A,
- )
- Image(
- painter = painterResource(id = R.drawable.ic_24_down),
- contentDescription = null,
- )
- }
- }
- }
- Spacer(modifier = Modifier.height(32.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- ) {
- Text(
- text = "기념일",
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.GRAY01_888888,
- )
- Text(
- modifier = Modifier.onNoRippleClick(onClick = {}),
- text = "추가하기",
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.BLUE01_5AA2E9,
- )
- }
- Spacer(modifier = Modifier.height(16.dp))
- }
- LazyColumn(
- modifier = Modifier.background(color = NearTheme.colors.BG02_F4F9FD),
- contentPadding =
- PaddingValues(
- top = 20.dp,
- bottom = 32.dp,
- start = 24.dp,
- end = 20.dp,
- ),
- verticalArrangement = Arrangement.spacedBy(32.dp),
- ) {
- item {
- Column {
- val anniversaryDatePickerState = remember { mutableStateOf(false) }
- val datePickerState =
- rememberDatePickerState()
- if (anniversaryDatePickerState.value) {
- NearDatePicker(
- datePickerState = datePickerState,
- onDismiss = { anniversaryDatePickerState.value = false },
- onDateSelected = {},
- )
- }
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = "기념일 이름",
+ modifier = Modifier.padding(top = 16.dp),
+ text =
+ buildAnnotatedString {
+ append(stringResource(R.string.friend_profile_editor_name))
+ withStyle(
+ style =
+ SpanStyle(
+ color = NearTheme.colors.BLUE01_5AA2E9,
+ ),
+ ) {
+ append("*")
+ }
+ },
+ textAlign = TextAlign.Center,
style = NearTheme.typography.B2_14_MEDIUM,
color = NearTheme.colors.GRAY01_888888,
)
- Spacer(modifier = Modifier.width(23.dp))
+ Spacer(modifier = Modifier.width(55.dp))
NearTextField(
modifier = Modifier.weight(1f),
- value = "가나다",
+ value = friendProfileEditorUIState.name.value,
onValueChange = {
+ onNameChanged(it)
},
)
}
- if (isErrorMessageVisible.value) {
+ if (friendProfileEditorUIState.name.error) {
Spacer(modifier = Modifier.height(8.dp))
Text(
- "이름을 입력해주세요.",
+ stringResource(R.string.friend_profile_editor_enter_name),
style = NearTheme.typography.FC_12_MEDIUM,
color = NearTheme.colors.NEGATIVE_F04E4E,
)
}
- Spacer(modifier = Modifier.height(16.dp))
+ Spacer(modifier = Modifier.height(32.dp))
Row(
modifier =
Modifier
@@ -378,17 +232,83 @@ fun FriendProfileEditorScreen(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
- "날짜",
+ stringResource(R.string.friend_profile_editor_relation),
style = NearTheme.typography.B2_14_MEDIUM,
color = NearTheme.colors.GRAY01_888888,
)
- Spacer(modifier = Modifier.width(62.dp))
+ Spacer(modifier = Modifier.width(72.dp))
+ Row(
+ modifier =
+ Modifier
+ .weight(1f)
+ .padding(end = 35.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ NearSmallRadioButton(
+ selected = friendProfileEditorUIState.relation == Relation.FRIEND,
+ onClick = {
+ onRelationChanged(Relation.FRIEND)
+ },
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = stringResource(R.string.friend_profile_editor_relation_friend),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.BLACK_1A1A1A,
+ )
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ NearSmallRadioButton(
+ selected = friendProfileEditorUIState.relation == Relation.FAMILY,
+ onClick = {
+ onRelationChanged(Relation.FAMILY)
+ },
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = stringResource(R.string.friend_profile_editor_relation_family),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.BLACK_1A1A1A,
+ )
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ NearSmallRadioButton(
+ selected = friendProfileEditorUIState.relation == Relation.ACQUAINTANCE,
+ onClick = { onRelationChanged(Relation.ACQUAINTANCE) },
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = stringResource(R.string.friend_profile_editor_relation_acquaintance),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.BLACK_1A1A1A,
+ )
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(33.dp))
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ stringResource(R.string.friend_profile_editor_contact_period),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.GRAY01_888888,
+ )
+ Spacer(modifier = Modifier.width(35.dp))
Surface(
modifier =
- modifier
+ Modifier
.weight(1f)
- .onNoRippleClick(onClick = {
- anniversaryDatePickerState.value = true
+ .onNoRippleClick({
+ showBottomSheet.value = true
}),
shape = RoundedCornerShape(12.dp),
border =
@@ -410,7 +330,78 @@ fun FriendProfileEditorScreen(
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
- "2020.06.24",
+ text =
+ stringResource(friendProfileEditorUIState.contactFrequency.reminderInterval.labelRes) +
+ stringResource(
+ R.string.friend_profile_editor_contact_period_format,
+ stringResource(friendProfileEditorUIState.contactFrequency.dayOfWeek.resId),
+ ),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.BLACK_1A1A1A,
+ )
+ Image(
+ painter = painterResource(id = R.drawable.ic_24_down),
+ contentDescription = null,
+ )
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ val birthdayDatePickerState = remember { mutableStateOf(false) }
+ val datePickerState =
+ rememberDatePickerState()
+ if (birthdayDatePickerState.value) {
+ NearDatePicker(
+ datePickerState = datePickerState,
+ onDismiss = { birthdayDatePickerState.value = false },
+ onDateSelected = {
+ it?.let {
+ onBirthdayChanged(it)
+ }
+ },
+ )
+ }
+ Text(
+ stringResource(R.string.friend_profile_editor_birthday),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.GRAY01_888888,
+ )
+ Spacer(modifier = Modifier.width(62.dp))
+ Surface(
+ modifier =
+ Modifier
+ .weight(1f),
+ shape = RoundedCornerShape(12.dp),
+ border =
+ BorderStroke(
+ width = 1.dp,
+ color = NearTheme.colors.GRAY03_EBEBEB,
+ ),
+ color = NearTheme.colors.WHITE_FFFFFF,
+ ) {
+ Row(
+ modifier =
+ Modifier
+ .padding(
+ start = 16.dp,
+ end = 12.dp,
+ top = 14.dp,
+ bottom = 14.dp,
+ ).onNoRippleClick({
+ birthdayDatePickerState.value = true
+ }),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ friendProfileEditorUIState.birthday.value
+ ?: stringResource(R.string.friend_profile_editor_select_date),
style = NearTheme.typography.B2_14_MEDIUM,
color = NearTheme.colors.BLACK_1A1A1A,
)
@@ -422,46 +413,184 @@ fun FriendProfileEditorScreen(
}
}
Spacer(modifier = Modifier.height(32.dp))
- Text(
+ Row(
modifier = Modifier.fillMaxWidth(),
- textAlign = TextAlign.End,
- text = "삭제하기",
- textDecoration = TextDecoration.Underline,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.friend_profile_editor_anniversary),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.GRAY01_888888,
+ )
+ Text(
+ modifier =
+ Modifier.onNoRippleClick(onClick = {
+ onAddAnniversary()
+ }),
+ text = stringResource(R.string.friend_profile_editor_anniversary_add),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.BLUE01_5AA2E9,
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+ if (friendProfileEditorUIState.anniversaries.isNotEmpty()) {
+ items(
+ count = friendProfileEditorUIState.anniversaries.size,
+ ) { index ->
+ Column(
+ modifier =
+ Modifier
+ .background(color = NearTheme.colors.BG02_F4F9FD)
+ .padding(
+ PaddingValues(
+ top = 20.dp,
+ bottom = 32.dp,
+ start = 24.dp,
+ end = 20.dp,
+ ),
+ ),
+ ) {
+ val anniversaryDatePickerState = remember { mutableStateOf(false) }
+ val datePickerState =
+ rememberDatePickerState()
+ if (anniversaryDatePickerState.value) {
+ NearDatePicker(
+ datePickerState = datePickerState,
+ onDismiss = { anniversaryDatePickerState.value = false },
+ onDateSelected = {
+ it?.let {
+ onAnniversaryDateSelected(index, it)
+ }
+ },
+ )
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = stringResource(R.string.friend_profile_editor_anniversary_name),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.GRAY01_888888,
+ )
+ Spacer(modifier = Modifier.width(23.dp))
+ NearTextField(
+ modifier = Modifier.weight(1f),
+ value = friendProfileEditorUIState.anniversaries[index].title.value,
+ onValueChange = {
+ onAnniversaryNameChange(index, it)
+ },
+ )
+ }
+ if (friendProfileEditorUIState.anniversaries[index].title.error) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ stringResource(R.string.friend_profile_editor_name_hint_text),
+ style = NearTheme.typography.FC_12_MEDIUM,
+ color = NearTheme.colors.NEGATIVE_F04E4E,
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ stringResource(R.string.friend_profile_editor_date),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.GRAY01_888888,
+ )
+ Spacer(modifier = Modifier.width(62.dp))
+ Surface(
+ modifier =
+ Modifier
+ .weight(1f)
+ .onNoRippleClick(onClick = {
+ anniversaryDatePickerState.value = true
+ }),
+ shape = RoundedCornerShape(12.dp),
+ border =
+ BorderStroke(
+ width = 1.dp,
+ color = NearTheme.colors.GRAY03_EBEBEB,
+ ),
+ color = NearTheme.colors.WHITE_FFFFFF,
+ ) {
+ Row(
+ modifier =
+ Modifier.padding(
+ start = 16.dp,
+ end = 12.dp,
+ top = 14.dp,
+ bottom = 14.dp,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ friendProfileEditorUIState.anniversaries[index].date.value
+ ?: stringResource(R.string.friend_profile_editor_select_date),
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.BLACK_1A1A1A,
+ )
+ Image(
+ painter = painterResource(id = R.drawable.ic_24_down),
+ contentDescription = null,
+ )
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(32.dp))
+ Text(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .onNoRippleClick(
+ onClick = {
+ onRemoveAnniversary(index)
+ },
+ ),
+ textAlign = TextAlign.End,
+ text = stringResource(R.string.friend_profile_editor_delete),
+ textDecoration = TextDecoration.Underline,
+ color = NearTheme.colors.GRAY01_888888,
+ )
+ }
+ }
+ }
+ item {
+ Spacer(modifier = Modifier.height(24.dp))
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp),
+ ) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp),
+ text = stringResource(R.string.friend_profile_editor_memo),
+ style = NearTheme.typography.B2_14_MEDIUM,
color = NearTheme.colors.GRAY01_888888,
)
+ Spacer(modifier = Modifier.width(23.dp))
+ NearLimitedTextField(
+ modifier =
+ Modifier
+ .weight(1f)
+ .height(180.dp),
+ value = friendProfileEditorUIState.memo.value ?: "",
+ onValueChange = {
+ onMemoChanged(it)
+ },
+ placeHolderText =
+ stringResource(R.string.friend_profile_editor_memo_default_text),
+ maxTextCount = 100,
+ )
+ Spacer(modifier = Modifier.height(80.dp))
}
}
}
- Spacer(modifier = Modifier.height(24.dp))
- Row(
- modifier =
- Modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp),
- ) {
- Text(
- modifier = Modifier.padding(top = 16.dp),
- text = "메모",
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.GRAY01_888888,
- )
- Spacer(modifier = Modifier.width(23.dp))
- NearLimitedTextField(
- modifier =
- Modifier
- .weight(1f)
- .height(180.dp),
- value = "",
- onValueChange = {
- },
- placeHolderText =
- "꼭 기억해야 할 내용을 기록해보세요.\n" +
- "예) 날생선 X, 작년 생일에\n" +
- "키링 선물함 등",
- maxTextCount = 100,
- )
- }
- Spacer(modifier = Modifier.height(80.dp))
}
}
@@ -469,6 +598,15 @@ fun FriendProfileEditorScreen(
@Composable
fun FriendProfileEditorScreenPreview() {
NearTheme {
- FriendProfileEditorScreen()
+ FriendProfileEditorScreen(
+ friendProfileEditorUIState =
+ FriendProfileEditorUIState(
+ contactFrequency =
+ ContactFrequency(
+ reminderInterval = ReminderInterval.EVERY_DAY,
+ dayOfWeek = DayOfWeek.SATURDAY,
+ ),
+ ),
+ )
}
}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt
index d3cbb6ce..8892075e 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt
@@ -1,21 +1,207 @@
package com.alarmy.near.presentation.feature.friendprofileedittor
+import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
+import com.alarmy.near.data.repository.FriendRepository
+import com.alarmy.near.model.Friend
import com.alarmy.near.model.Relation
+import com.alarmy.near.model.ReminderInterval
+import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.RouteFriendProfileEditor
+import com.alarmy.near.presentation.feature.friendprofileedittor.uistate.AnniversaryUIState
+import com.alarmy.near.presentation.feature.friendprofileedittor.uistate.FriendProfileEditorUIEvent
+import com.alarmy.near.presentation.feature.friendprofileedittor.uistate.FriendProfileEditorUIState
+import com.alarmy.near.presentation.feature.friendprofileedittor.uistate.toModel
+import com.alarmy.near.presentation.feature.friendprofileedittor.uistate.toUiModel
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
import javax.inject.Inject
@HiltViewModel
class FriendProfileEditorViewModel
@Inject
- constructor() : ViewModel() {
- private val _relation: MutableStateFlow = MutableStateFlow(null)
- val relation = _relation.asStateFlow()
+ constructor(
+ savedStateHandle: SavedStateHandle,
+ private val friendRepository: FriendRepository,
+ ) : ViewModel() {
+ private val friend: Friend =
+ savedStateHandle.toRoute(RouteFriendProfileEditor.routeTypeMap).friend
- fun setRelation(relation: Relation) {
- _relation.update { relation }
+ private val _uiState: MutableStateFlow =
+ MutableStateFlow(friend.toUiModel())
+ val uiState = _uiState.asStateFlow()
+
+ private val _uiEvent = Channel()
+ val uiEvent = _uiEvent.receiveAsFlow()
+
+ fun onNameChanged(value: String) {
+ if (value.length > MAX_NAME_LENGTH) {
+ return
+ }
+ _uiState.update {
+ it.copy(
+ name =
+ it.name.copy(
+ value = value,
+ isDirty = true,
+ error = value.isEmpty(),
+ ),
+ )
+ }
+ }
+
+ fun onRelationChanged(value: Relation) {
+ _uiState.update { it.copy(relation = value) }
+ }
+
+ fun onRemindIntervalChanged(value: ReminderInterval) {
+ _uiState.update {
+ it.copy(
+ contactFrequency =
+ it.contactFrequency.copy(
+ reminderInterval = value,
+ ),
+ )
+ }
+ }
+
+ fun onBirthdayChanged(value: Long) {
+ _uiState.update {
+ it.copy(
+ birthday =
+ it.birthday.copy(
+ value = convertMillisToDate(value),
+ isDirty = true,
+ ),
+ )
+ }
+ }
+
+ fun onAnniversaryTitleChanged(
+ index: Int,
+ value: String,
+ ) {
+ _uiState.update {
+ it.copy(
+ anniversaries =
+ it.anniversaries.toMutableList().apply {
+ this[index] =
+ this[index].copy(
+ title =
+ this[index].title.copy(
+ value = value,
+ isDirty = true,
+ error = value.isEmpty(),
+ ),
+ )
+ },
+ )
+ }
+ }
+
+ fun onAnniversaryDateChanged(
+ index: Int,
+ value: Long,
+ ) {
+ _uiState.update {
+ it.copy(
+ anniversaries =
+ it.anniversaries.toMutableList().apply {
+ this[index] =
+ this[index].copy(
+ date =
+ this[index].date.copy(
+ value = convertMillisToDate(value),
+ isDirty = true,
+ ),
+ )
+ },
+ )
+ }
+ }
+
+ fun onAddAnniversary() {
+ _uiState.update { it.copy(anniversaries = it.anniversaries + AnniversaryUIState()) }
+ }
+
+ fun onRemoveAnniversary(index: Int) {
+ _uiState.update {
+ it.copy(anniversaries = it.anniversaries.toMutableList().apply { removeAt(index) })
+ }
+ }
+
+ fun onMemoChanged(value: String?) {
+ value?.length?.let {
+ if (it > MAX_MEMO_LENGTH) {
+ return
+ }
+ }
+ _uiState.update {
+ it.copy(
+ memo =
+ it.memo.copy(
+ value = value,
+ isDirty = true,
+ ),
+ )
+ }
+ }
+
+ private fun convertMillisToDate(millis: Long): String {
+ val formatter = SimpleDateFormat("yyyy.MM.dd", Locale.getDefault())
+ return formatter.format(Date(millis))
+ }
+
+ fun onExit() {
+ viewModelScope.launch {
+ if (uiState.value.anniversaries.any { it.title.isDirty || it.date.isDirty } ||
+ uiState.value.name.isDirty || uiState.value.memo.isDirty ||
+ uiState.value.birthday.isDirty
+ ) {
+ _uiEvent.send(FriendProfileEditorUIEvent.WarningExit)
+ } else {
+ _uiEvent.send(FriendProfileEditorUIEvent.Exit)
+ }
+ }
+ }
+
+ fun onSubmit() {
+ val updatedFriend = _uiState.value
+ if ((updatedFriend.name.error || updatedFriend.anniversaries.any { it.title.error || it.title.value.isBlank() })) {
+ // error
+ return
+ }
+
+ viewModelScope.launch {
+ friendRepository
+ .updateFriend(
+ friendId = friend.friendId,
+ friend =
+ updatedFriend.toModel(
+ friendId = friend.friendId,
+ imageUrl = friend.imageUrl ?: "",
+ phone = friend.phone ?: "",
+ lastContactAt = friend.lastContactAt ?: "",
+ ),
+ ).catch {
+ }.collect {
+ _uiEvent.send(FriendProfileEditorUIEvent.FriendProfileEditSuccess(it))
+ }
+ }
+ }
+
+ companion object {
+ private const val MAX_NAME_LENGTH = 20
+ private const val MAX_MEMO_LENGTH = 200
}
}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/NearDatePicker.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/NearDatePicker.kt
index a3bfca5b..2f23a951 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/NearDatePicker.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/NearDatePicker.kt
@@ -27,9 +27,10 @@ fun NearDatePicker(
onDismiss: () -> Unit,
) {
DatePickerDialog(
- colors = DatePickerDefaults.colors().copy(
- containerColor = NearTheme.colors.WHITE_FFFFFF,
- ),
+ colors =
+ DatePickerDefaults.colors().copy(
+ containerColor = NearTheme.colors.WHITE_FFFFFF,
+ ),
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = {
@@ -45,21 +46,22 @@ fun NearDatePicker(
}
},
) {
- DatePicker(state = datePickerState, title = null, headline = null, showModeToggle = false,
+ DatePicker(
+ state = datePickerState,
+ title = null,
+ headline = null,
+ showModeToggle = false,
dateFormatter =
remember
{ DatePickerDefaults.dateFormatter() },
- colors = DatePickerDefaults.colors().copy(
- containerColor = NearTheme.colors.WHITE_FFFFFF,
- ))
+ colors =
+ DatePickerDefaults.colors().copy(
+ containerColor = NearTheme.colors.WHITE_FFFFFF,
+ ),
+ )
}
}
-private fun convertMillisToDate(millis: Long): String {
- val formatter = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault())
- return formatter.format(Date(millis))
-}
-
@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/ReminderIntervalBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/ReminderIntervalBottomSheet.kt
index b4f43b77..ce7902f1 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/ReminderIntervalBottomSheet.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/ReminderIntervalBottomSheet.kt
@@ -44,7 +44,7 @@ import com.alarmy.near.presentation.ui.theme.NearTheme
@Composable
fun ReminderIntervalBottomSheet(
modifier: Modifier = Modifier,
- selectedReminderInterval: ReminderInterval = ReminderInterval.WEEKLY,
+ selectedReminderInterval: ReminderInterval = ReminderInterval.EVERY_WEEK,
onSelectReminderInterval: (ReminderInterval) -> Unit = {},
sheetState: SheetState =
rememberModalBottomSheetState(
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/dialog/EditorExitDialog.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/dialog/EditorExitDialog.kt
index 5aa9c093..df55ac6d 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/dialog/EditorExitDialog.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/dialog/EditorExitDialog.kt
@@ -6,8 +6,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import com.alarmy.near.R
import com.alarmy.near.presentation.ui.theme.NearTheme
@Composable
@@ -20,27 +22,26 @@ internal fun EditorExitDialog(
modifier = modifier,
onDismissRequest = onDismissRequest,
title = {
- Text(text = "수정을 그만두시나요?")
+ Text(text = stringResource(R.string.editor_exit_title))
},
text = {
Text(
text =
- "화면을 나가면 \n" +
- "수정 내용은 저장되지 않아요.",
+ stringResource(R.string.editor_exit_content),
)
},
confirmButton = {
TextButton(
onClick = onConfirm,
) {
- Text("확인")
+ Text(stringResource(R.string.editor_exit_confirm))
}
},
dismissButton = {
TextButton(
onClick = onDismissRequest,
) {
- Text("취소")
+ Text(stringResource(R.string.editor_exit_dismiss))
}
},
shape = RoundedCornerShape(24.dp),
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/navigation/FriendProfileNavigation.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/navigation/FriendProfileNavigation.kt
index d7521c67..1fd39a14 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/navigation/FriendProfileNavigation.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/navigation/FriendProfileNavigation.kt
@@ -1,27 +1,90 @@
package com.alarmy.near.presentation.feature.friendprofileedittor.navigation
+import android.os.Build
+import android.os.Parcelable
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
+import androidx.navigation.NavType
import androidx.navigation.compose.composable
+import androidx.savedstate.SavedState
+import com.alarmy.near.model.Friend
import com.alarmy.near.presentation.feature.friendprofileedittor.FriendProfileEditorRoute
+import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.RouteFriendProfileEditor.Companion.routeTypeMap
+import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import kotlin.reflect.KType
+import kotlin.reflect.typeOf
+
+const val FRIEND_PROFILE_EDIT_COMPLETE_KEY = "FRIEND_PROFILE_EDIT_COMPLETE_KEY"
@Serializable
-object RouteFriendProfileEditor
+@Parcelize
+data class RouteFriendProfileEditor(
+ val friend: Friend,
+) : Parcelable {
+ companion object {
+ val routeTypeMap =
+ mapOf>(
+ typeOf() to FriendType,
+ )
+ }
+}
-fun NavController.navigateToFriendProfileEditor(navOptions: NavOptions) {
- navigate(RouteFriendProfileEditor, navOptions)
+fun NavController.navigateToFriendProfileEditor(
+ friend: Friend,
+ navOptions: NavOptions? = null,
+) {
+ navigate(
+ RouteFriendProfileEditor(
+ friend = friend,
+ ),
+ navOptions,
+ )
}
fun NavGraphBuilder.friendProfileEditorNavGraph(
onShowErrorSnackBar: (throwable: Throwable?) -> Unit,
onClickBackButton: () -> Unit = {},
+ onSuccessEdit: (Friend) -> Unit = {},
) {
- composable { backStackEntry ->
+ composable(
+ typeMap =
+ routeTypeMap,
+ ) { backStackEntry ->
FriendProfileEditorRoute(
onShowErrorSnackBar = onShowErrorSnackBar,
onClickBackButton = onClickBackButton,
+ onSuccessEdit = onSuccessEdit,
)
}
}
+
+internal val FriendType =
+ object : NavType(
+ isNullableAllowed = false,
+ ) {
+ override fun put(
+ bundle: SavedState,
+ key: String,
+ value: Friend,
+ ) {
+ bundle.putParcelable(key, value)
+ }
+
+ override fun get(
+ bundle: SavedState,
+ key: String,
+ ): Friend? =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ bundle.getParcelable(key, Friend::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ bundle.getParcelable(key)
+ }
+
+ override fun parseValue(value: String): Friend = Json.decodeFromString(value)
+
+ override fun serializeAsValue(value: Friend): String = Json.encodeToString(value)
+ }
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIEvent.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIEvent.kt
new file mode 100644
index 00000000..bf84abfa
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIEvent.kt
@@ -0,0 +1,19 @@
+package com.alarmy.near.presentation.feature.friendprofileedittor.uistate
+
+import com.alarmy.near.model.Friend
+
+sealed interface FriendProfileEditorUIEvent {
+ data class FriendProfileEditSuccess(
+ val friend: Friend,
+ ) : FriendProfileEditorUIEvent
+
+ data class FriendProfileEditFailure(
+ val throwable: Throwable,
+ ) : FriendProfileEditorUIEvent
+
+ data object FriendProfileEditNetworkError : FriendProfileEditorUIEvent
+
+ data object WarningExit : FriendProfileEditorUIEvent
+
+ data object Exit : FriendProfileEditorUIEvent
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt
new file mode 100644
index 00000000..4e92060a
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt
@@ -0,0 +1,68 @@
+package com.alarmy.near.presentation.feature.friendprofileedittor.uistate
+
+import com.alarmy.near.model.Anniversary
+import com.alarmy.near.model.ContactFrequency
+import com.alarmy.near.model.Friend
+import com.alarmy.near.model.Relation
+
+data class FriendProfileEditorUIState(
+ val name: InputField = InputField(""),
+ val relation: Relation = Relation.FRIEND,
+ val contactFrequency: ContactFrequency,
+ val birthday: InputField = InputField(null),
+ val anniversaries: List = emptyList(),
+ val memo: InputField = InputField(null),
+)
+
+data class AnniversaryUIState(
+ val title: InputField = InputField(""),
+ val date: InputField = InputField(null),
+)
+
+data class InputField(
+ val value: T,
+ val error: Boolean = false, // null이면 유효한 상태
+ val isDirty: Boolean = false, // 유저가 입력을 시도했는지
+)
+
+fun Friend.toUiModel(): FriendProfileEditorUIState =
+ FriendProfileEditorUIState(
+ name = InputField(name),
+ relation = relation,
+ contactFrequency = contactFrequency,
+ birthday = InputField(birthday),
+ anniversaries = anniversaryList.map { it.toUiModel() },
+ memo = InputField(memo),
+ )
+
+fun Anniversary.toUiModel(): AnniversaryUIState =
+ AnniversaryUIState(
+ title = InputField(title),
+ date = InputField(date),
+ )
+
+fun FriendProfileEditorUIState.toModel(
+ friendId: String,
+ imageUrl: String,
+ phone: String,
+ lastContactAt: String,
+): Friend =
+ Friend(
+ name = name.value,
+ relation = relation,
+ contactFrequency =
+ contactFrequency,
+ birthday = birthday.value?.replace(".", "-"),
+ anniversaryList =
+ anniversaries.map {
+ Anniversary(
+ title = it.title.value,
+ date = it.date.value?.replace(".", "-"),
+ )
+ },
+ friendId = friendId,
+ imageUrl = imageUrl,
+ memo = memo.value,
+ phone = phone,
+ lastContactAt = lastContactAt,
+ )
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt
index 61612787..0a146e16 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt
@@ -22,8 +22,6 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -52,11 +50,13 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.alarmy.near.R
-import com.alarmy.near.model.ContactFrequency
-import com.alarmy.near.model.FriendSummary
+import com.alarmy.near.model.friendsummary.ContactFrequencyLevel
+import com.alarmy.near.model.friendsummary.FriendSummary
import com.alarmy.near.model.monthly.MonthlyFriend
import com.alarmy.near.model.monthly.MonthlyFriendType
import com.alarmy.near.presentation.feature.home.component.MyContacts
+import com.alarmy.near.presentation.ui.component.dropdown.NearDropdownMenu
+import com.alarmy.near.presentation.ui.component.dropdown.NearDropdownMenuItem
import com.alarmy.near.presentation.ui.extension.dropShadow
import com.alarmy.near.presentation.ui.extension.onNoRippleClick
import com.alarmy.near.presentation.ui.theme.NearTheme
@@ -311,24 +311,16 @@ internal fun HomeScreen(
painter = painterResource(R.drawable.ic_32_menu),
contentDescription = stringResource(R.string.home_my_people_setting),
)
- DropdownMenu(
- modifier = Modifier.background(color = NearTheme.colors.WHITE_FFFFFF),
+ NearDropdownMenu(
expanded = dropdownState.value,
- shape = RoundedCornerShape(12.dp),
onDismissRequest = { dropdownState.value = false },
) {
- DropdownMenuItem(
+ NearDropdownMenuItem(
onClick = {
// TODO 연락처 화면 이동
dropdownState.value = false
},
- text = {
- Text(
- stringResource(R.string.home_menu_text_add_friend),
- style = NearTheme.typography.B2_14_MEDIUM,
- color = NearTheme.colors.BLACK_1A1A1A,
- )
- },
+ text = stringResource(R.string.home_menu_text_add_friend),
)
}
}
@@ -388,7 +380,7 @@ internal fun HomeScreenPreview() {
profileImageUrl = "https://search.yahoo.com/search?p=partiendo",
lastContactedAt = "2025-07-16",
isContacted = false,
- contactFrequency = ContactFrequency.HIGH,
+ contactFrequencyLevel = ContactFrequencyLevel.HIGH,
)
},
monthlyFriends =
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt
index 899c1503..c8a4ccfe 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt
@@ -3,7 +3,7 @@ package com.alarmy.near.presentation.feature.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alarmy.near.data.repository.FriendRepository
-import com.alarmy.near.model.FriendSummary
+import com.alarmy.near.model.friendsummary.FriendSummary
import com.alarmy.near.model.monthly.MonthlyFriend
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/component/ContactItem.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/component/ContactItem.kt
index 15fd3d28..892176ff 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/component/ContactItem.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/component/ContactItem.kt
@@ -19,8 +19,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.alarmy.near.R
-import com.alarmy.near.model.ContactFrequency
-import com.alarmy.near.model.FriendSummary
+import com.alarmy.near.model.friendsummary.ContactFrequencyLevel
+import com.alarmy.near.model.friendsummary.FriendSummary
import com.alarmy.near.presentation.ui.extension.onNoRippleClick
import com.alarmy.near.presentation.ui.theme.NearTheme
@@ -50,10 +50,10 @@ fun ContactItem(
.align(Alignment.TopEnd)
.offset(x = 4.dp, y = (-4).dp),
painter =
- when (friendSummary.contactFrequency) {
- ContactFrequency.LOW -> painterResource(R.drawable.ic_visual_24_emoji_0)
- ContactFrequency.MIDDLE -> painterResource(R.drawable.ic_visual_24_emoji_50)
- ContactFrequency.HIGH -> painterResource(R.drawable.ic_visual_24_emoji_100)
+ when (friendSummary.contactFrequencyLevel) {
+ ContactFrequencyLevel.LOW -> painterResource(R.drawable.ic_visual_24_emoji_0)
+ ContactFrequencyLevel.MIDDLE -> painterResource(R.drawable.ic_visual_24_emoji_50)
+ ContactFrequencyLevel.HIGH -> painterResource(R.drawable.ic_visual_24_emoji_100)
},
contentDescription = "",
)
@@ -99,7 +99,7 @@ fun ContactItemPreview() {
profileImageUrl = "",
lastContactedAt = "2025-04-21",
isContacted = true,
- contactFrequency = ContactFrequency.HIGH,
+ contactFrequencyLevel = ContactFrequencyLevel.HIGH,
),
)
}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/component/MyContacts.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/component/MyContacts.kt
index c09f3796..3e1ac181 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/component/MyContacts.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/component/MyContacts.kt
@@ -17,10 +17,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.alarmy.near.model.ContactFrequency
-import com.alarmy.near.model.FriendSummary
+import com.alarmy.near.model.friendsummary.ContactFrequencyLevel
+import com.alarmy.near.model.friendsummary.FriendSummary
import com.alarmy.near.presentation.ui.theme.NearTheme
-import java.time.LocalDate
private const val OVERFLOW_WIDTH_OF_CONTACT_ITEM_BY_NAME_TEXT = 34
@@ -240,7 +239,7 @@ fun MyContactsPreview() {
profileImageUrl = "https://search.yahoo.com/search?p=partiendo",
lastContactedAt = "2025-04-21",
isContacted = false,
- contactFrequency = ContactFrequency.LOW,
+ contactFrequencyLevel = ContactFrequencyLevel.LOW,
)
}.chunked(5),
)
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt
index 10990fba..c231a9c3 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt
@@ -89,7 +89,7 @@ private fun LoginIntroductionSection(modifier: Modifier = Modifier) {
Image(
modifier = modifier.wrapContentSize(Alignment.Center),
alignment = Alignment.Center,
- painter = painterResource(R.drawable.ic_near_logo_title),
+ painter = painterResource(R.drawable.ic_near_logo_title_primary),
contentDescription = stringResource(R.string.near_logo_title),
)
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/MainActivity.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/MainActivity.kt
index 35f00fdc..c79559a9 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/MainActivity.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/MainActivity.kt
@@ -4,17 +4,47 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.activity.viewModels
+import androidx.compose.runtime.getValue
+import androidx.core.splashscreen.SplashScreen
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.lifecycleScope
import com.alarmy.near.presentation.ui.theme.NearTheme
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
+ private val mainViewModel: MainViewModel by viewModels()
+
override fun onCreate(savedInstanceState: Bundle?) {
+ val splashScreen = installSplashScreen()
+
super.onCreate(savedInstanceState)
enableEdgeToEdge()
+ setupSplashScreen(splashScreen)
+
setContent {
NearTheme {
- NearApp()
+ val uiState by mainViewModel.uiState.collectAsStateWithLifecycle()
+ if (!uiState.isLoading) {
+ NearApp(
+ startDestination = uiState.startDestination,
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * 스플래시 스크린을 설정하고 MainViewModel의 상태를 관찰합니다.
+ * API 스플래시가 표시되는 동안 백그라운드에서 검증을 수행합니다.
+ */
+ private fun setupSplashScreen(splashScreen: SplashScreen) {
+ lifecycleScope.launch {
+ mainViewModel.uiState.collect { uiState ->
+ splashScreen.setKeepOnScreenCondition { uiState.isLoading }
}
}
}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/MainViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/MainViewModel.kt
new file mode 100644
index 00000000..37a23776
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/MainViewModel.kt
@@ -0,0 +1,78 @@
+package com.alarmy.near.presentation.feature.main
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.alarmy.near.data.repository.AuthRepository
+import com.alarmy.near.data.repository.OnBoardingRepository
+import com.alarmy.near.presentation.feature.home.navigation.RouteHome
+import com.alarmy.near.presentation.feature.login.navigation.RouteLogin
+import com.alarmy.near.presentation.feature.onboarding.navigation.RouteOnboarding
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class MainViewModel @Inject constructor(
+ private val authRepository: AuthRepository,
+ private val onBoardingRepository: OnBoardingRepository,
+) : ViewModel() {
+
+ // UI 상태 관리
+ private val _uiState = MutableStateFlow(MainUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ init {
+ checkAppStatus()
+ }
+
+ /**
+ * 앱 상태를 확인하고 스플래시 스크린을 제어합니다.
+ * 온보딩 완료 여부와 로그인 상태를 확인하여 적절한 화면으로 이동합니다.
+ */
+ private fun checkAppStatus() {
+ viewModelScope.launch {
+ runCatching {
+ // 온보딩 완료 여부 확인
+ val isOnboardingCompleted = onBoardingRepository.isOnboardingCompleted()
+ // 로그인 상태 검증
+ val isLoggedIn = authRepository.isLoggedIn()
+
+ // 시작 화면 결정
+ val startDestination = when {
+ !isOnboardingCompleted -> RouteOnboarding
+ isLoggedIn -> RouteHome
+ else -> RouteLogin
+ }
+
+ // UI 상태 업데이트
+ _uiState.value = _uiState.value.copy(
+ isLoading = false,
+ isOnboardingCompleted = isOnboardingCompleted,
+ isLoggedIn = isLoggedIn,
+ startDestination = startDestination
+ )
+ }.onFailure {
+ // 에러 발생 시 기본값으로 설정
+ _uiState.value = _uiState.value.copy(
+ isLoading = false,
+ isOnboardingCompleted = false,
+ isLoggedIn = false,
+ startDestination = RouteOnboarding
+ )
+ }
+ }
+ }
+}
+
+/**
+ * MainActivity의 UI 상태를 관리하는 데이터 클래스
+ */
+data class MainUiState(
+ val isLoading: Boolean = true,
+ val isOnboardingCompleted: Boolean = false,
+ val isLoggedIn: Boolean = false,
+ val startDestination: Any = RouteOnboarding,
+)
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearApp.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearApp.kt
index 106f00b5..1b7be78c 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearApp.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearApp.kt
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.Scaffold
@@ -26,6 +25,7 @@ import kotlinx.coroutines.launch
internal fun NearApp(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
+ startDestination: Any,
) {
val snackBarState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
@@ -48,6 +48,7 @@ internal fun NearApp(
NearNavHost(
modifier = Modifier.consumeWindowInsets(innerPadding), // 하위 뷰에 Padding을 소비한 것으로 알립니다.
navController = navController,
+ startDestination = startDestination,
onShowSnackbar = {
scope.launch {
snackBarState.showSnackbar(
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt
index 3b7ad4b9..d7ca83b2 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt
@@ -1,44 +1,110 @@
package com.alarmy.near.presentation.feature.main
+import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.core.net.toUri
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
+import androidx.navigation.navOptions
import com.alarmy.near.presentation.feature.friendprofile.navigation.friendProfileNavGraph
import com.alarmy.near.presentation.feature.friendprofile.navigation.navigateToFriendProfile
+import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.FRIEND_PROFILE_EDIT_COMPLETE_KEY
import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.friendProfileEditorNavGraph
-import com.alarmy.near.presentation.feature.home.navigation.RouteHome
+import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.navigateToFriendProfileEditor
import com.alarmy.near.presentation.feature.home.navigation.homeNavGraph
import com.alarmy.near.presentation.feature.home.navigation.navigateToHome
import com.alarmy.near.presentation.feature.login.navigation.RouteLogin
import com.alarmy.near.presentation.feature.login.navigation.loginNavGraph
+import com.alarmy.near.presentation.feature.login.navigation.navigateToLogin
+import com.alarmy.near.presentation.feature.onboarding.navigation.RouteOnboarding
+import com.alarmy.near.presentation.feature.onboarding.navigation.onboardingNavGraph
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
@Composable
internal fun NearNavHost(
modifier: Modifier = Modifier,
navController: NavHostController,
+ startDestination: Any, // 나중에 모든 루트를 sealed로 구성하면 sealed 타입으로 변경
onShowSnackbar: (Throwable?) -> Unit = { _ -> },
) {
+ val context = LocalContext.current
+
/*
* 화면 이동 및 구성을 위한 컴포저블 함수입니다.
+ * startDestination 파라미터로 받은 시작 화면으로 이동합니다.
* */
NavHost(
modifier = modifier,
navController = navController,
- startDestination = RouteLogin,
+ startDestination = startDestination,
) {
+ // 온보딩 화면 NavGraph
+ onboardingNavGraph(
+ onNavigateToLogin = {
+ navController.navigateToLogin(
+ navOptions =
+ navOptions {
+ popUpTo(RouteOnboarding) { inclusive = true }
+ },
+ )
+ },
+ )
+
+ friendProfileNavGraph(onShowErrorSnackBar = onShowSnackbar, onClickBackButton = {
+ navController.popBackStack()
+ }, onClickCallButton = { phoneNumber ->
+ val intent =
+ Intent(Intent.ACTION_DIAL).apply {
+ data = "tel:$phoneNumber".toUri()
+ }
+ context.startActivity(intent)
+ }, onClickMessageButton = { phoneNumber ->
+ val intent =
+ Intent(Intent.ACTION_VIEW).apply {
+ data = "sms:$phoneNumber".toUri()
+ }
+ context.startActivity(intent)
+ }, onEditFriendInfo = {
+ navController.navigateToFriendProfileEditor(
+ friend =
+ it.copy(
+ imageUrl =
+ it.imageUrl?.let { imageUrl ->
+ URLEncoder.encode(
+ imageUrl,
+ StandardCharsets.UTF_8.toString(),
+ )
+ },
+ ),
+ )
+ })
+ friendProfileEditorNavGraph(onShowErrorSnackBar = onShowSnackbar, onClickBackButton = {
+ navController.popBackStack()
+ }, onSuccessEdit = {
+ navController.previousBackStackEntry?.savedStateHandle?.set(
+ FRIEND_PROFILE_EDIT_COMPLETE_KEY,
+ it,
+ )
+ navController.popBackStack()
+ })
+
+
// 로그인 화면 NavGraph
loginNavGraph(
onShowErrorSnackBar = onShowSnackbar,
onNavigateToHome = {
navController.navigateToHome(
- navOptions = androidx.navigation.navOptions {
- popUpTo(RouteLogin) { inclusive = true }
- }
+ navOptions =
+ navOptions {
+ popUpTo(RouteLogin) { inclusive = true }
+ },
)
- }
+ },
)
-
+
// 홈 화면 NavGraph
homeNavGraph(
onShowErrorSnackBar = onShowSnackbar,
@@ -49,21 +115,21 @@ internal fun NearNavHost(
onAlarmClick = {},
onAddContactClick = {},
)
-
+
// 친구 프로필 화면 NavGraph
friendProfileNavGraph(
onShowErrorSnackBar = onShowSnackbar,
onClickBackButton = {
navController.popBackStack()
- }
+ },
)
-
+
// 친구 프로필 편집 화면 NavGraph
friendProfileEditorNavGraph(
onShowErrorSnackBar = onShowSnackbar,
onClickBackButton = {
navController.popBackStack()
- }
+ },
)
}
}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingScreen.kt
new file mode 100644
index 00000000..48928afc
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingScreen.kt
@@ -0,0 +1,234 @@
+package com.alarmy.near.presentation.feature.onboarding
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.alarmy.near.R
+import com.alarmy.near.presentation.feature.onboarding.components.BackgroundArea
+import com.alarmy.near.presentation.feature.onboarding.components.OnboardingButton
+import com.alarmy.near.presentation.feature.onboarding.components.PageIndicator
+import com.alarmy.near.presentation.feature.onboarding.model.OnboardingPage
+import com.alarmy.near.presentation.ui.theme.NearTheme
+import kotlinx.coroutines.launch
+
+/**
+ * 온보딩 화면 메인 컴포넌트
+ * 5페이지로 구성된 뷰페이저 형태의 온보딩 화면
+ */
+@Composable
+fun OnboardingScreen(
+ onNavigateToLogin: () -> Unit,
+ viewModel: OnboardingViewModel = hiltViewModel(),
+) {
+ // UI 상태 관찰
+ val uiState by viewModel.uiState.collectAsState()
+
+ // 사이드 이펙트 처리
+ LaunchedEffect(Unit) {
+ viewModel.effect.collect { effect ->
+ when (effect) {
+ is OnboardingEffect.NavigateToLogin -> {
+ onNavigateToLogin()
+ }
+ }
+ }
+ }
+
+ // 온보딩 페이지 데이터 - remember로 성능 최적화
+ val pages = remember {
+ listOf(
+ OnboardingPage(
+ titleResId = R.string.first_onboarding_title,
+ image = R.drawable.img_onboarding_page_first,
+ ),
+ OnboardingPage(
+ titleResId = R.string.second_onboarding_title,
+ image = R.drawable.img_onboarding_page_second,
+ ),
+ OnboardingPage(
+ titleResId = R.string.third_onboarding_title,
+ image = R.drawable.img_onboarding_page_third,
+ ),
+ OnboardingPage(
+ titleResId = R.string.fourth_onboarding_title,
+ image = R.drawable.img_onboarding_page_forth,
+ ),
+ OnboardingPage(
+ titleResId = R.string.fifth_onboarding_title,
+ image = R.drawable.img_onboarding_page_fifth,
+ ),
+ )
+ }
+
+ // 상태바와 네비게이션 바 높이 계산
+ val density = LocalDensity.current
+ val statusBarHeightDp = with(density) { WindowInsets.statusBars.getTop(density).toDp() }
+ val navigationBarHeightDp = with(density) { WindowInsets.navigationBars.getBottom(density).toDp() }
+
+ // 페이저 상태 관리
+ val pagerState = rememberPagerState(pageCount = { pages.size })
+ val scope = rememberCoroutineScope()
+
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ BackgroundArea()
+ Column(
+ modifier = Modifier
+ .padding(top = statusBarHeightDp, bottom = navigationBarHeightDp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ // 뷰페이저
+ HorizontalPager(
+ state = pagerState,
+ modifier = Modifier.weight(1f),
+ ) { page ->
+ OnboardingPageContent(
+ page = pages[page],
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
+
+ Spacer(modifier = Modifier.size(25.dp))
+
+ // 페이지 인디케이터
+ PageIndicator(
+ pageCount = pages.size,
+ currentPage = pagerState.currentPage,
+ )
+
+ Spacer(modifier = Modifier.size(32.dp))
+
+ // 다음/완료 버튼
+ OnboardingButton(
+ currentPage = pagerState.currentPage,
+ totalPages = pages.size,
+ isLoading = uiState.isLoading,
+ onNextClick = {
+ if (pagerState.currentPage < pages.size - 1) {
+ scope.launch {
+ pagerState.animateScrollToPage(pagerState.currentPage + 1)
+ }
+ } else {
+ // 온보딩 완료 시 DataStore에 저장
+ viewModel.completeOnboarding()
+ }
+ },
+ )
+ Spacer(modifier = Modifier.size(24.dp))
+ }
+ }
+}
+
+/**
+ * 온보딩 페이지 콘텐츠 컴포넌트
+ * 각 페이지의 제목과 설명을 표시
+ */
+@Composable
+private fun OnboardingPageContent(
+ page: OnboardingPage,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(modifier = Modifier.size(45.dp))
+
+ // 각 온보딩 페이지 타이틀
+ Text(
+ text = createAnnotatedText(stringResource(page.titleResId)),
+ textAlign = TextAlign.Center,
+ style =
+ NearTheme.typography.H1_24_BOLD.copy(
+ fontSize = 20.sp,
+ lineHeight = 30.sp,
+ ),
+ )
+
+ Spacer(modifier = Modifier.size(24.dp))
+
+ Image(
+ modifier = Modifier.weight(1f),
+ painter = painterResource(page.image),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ )
+
+ }
+}
+
+/**
+ * \n 이후 텍스트에 다른 색상을 적용하는 AnnotatedString 생성
+ * 현 페이지에서 \n 이후 텍스트 색상이 다른 규칙이 있습니다.
+ */
+@Composable
+private fun createAnnotatedText(
+ text: String,
+ defaultColor: Color = NearTheme.colors.BLACK_1A1A1A,
+ highlightColor: Color = NearTheme.colors.BLUE01_5AA2E9,
+): AnnotatedString =
+ buildAnnotatedString {
+ val newLineIndex = text.indexOf("\n")
+
+ if (newLineIndex != -1) {
+ // \n 이전 텍스트 (기본 색상)
+ appendStyledText(text.substring(0, newLineIndex), defaultColor)
+ // \n 이후 텍스트 (강조 색상)
+ appendStyledText(text.substring(newLineIndex), highlightColor)
+ } else {
+ // 텍스트에 \n가 없는 경우
+ appendStyledText(text, defaultColor)
+ }
+ }
+
+/**
+ * 지정된 색상으로 텍스트를 추가하는 함수
+ */
+private fun AnnotatedString.Builder.appendStyledText(text: String, color: Color) {
+ withStyle(style = SpanStyle(color = color)) {
+ append(text)
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun OnboardingScreenPreview() {
+ NearTheme {
+ OnboardingScreen(
+ onNavigateToLogin = {},
+ )
+ }
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingViewModel.kt
new file mode 100644
index 00000000..d5c65e72
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingViewModel.kt
@@ -0,0 +1,63 @@
+package com.alarmy.near.presentation.feature.onboarding
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.alarmy.near.data.repository.OnBoardingRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+/**
+ * 온보딩 화면의 상태와 로직을 관리하는 ViewModel
+ */
+@HiltViewModel
+class OnboardingViewModel @Inject constructor(
+ private val onBoardingRepository: OnBoardingRepository,
+) : ViewModel() {
+ private val _uiState = MutableStateFlow(OnboardingUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ private val _effect = MutableSharedFlow()
+ val effect = _effect.asSharedFlow()
+
+ /**
+ * 온보딩 완료 처리
+ */
+ fun completeOnboarding() {
+ viewModelScope.launch {
+ _uiState.value = _uiState.value.copy(isLoading = true)
+
+ runCatching {
+ onBoardingRepository.markOnboardingAsCompleted()
+ }.onSuccess {
+ _effect.emit(OnboardingEffect.NavigateToLogin)
+ }.onFailure { exception ->
+ _uiState.value = _uiState.value.copy(
+ isLoading = false,
+ error = exception.message
+ )
+ }
+ }
+ }
+
+}
+
+/**
+ * 온보딩 UI 상태
+ */
+data class OnboardingUiState(
+ val isLoading: Boolean = false,
+ val error: String? = null,
+)
+
+/**
+ * 온보딩 사이드 이펙트
+ */
+sealed class OnboardingEffect {
+ object NavigateToLogin : OnboardingEffect()
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/components/BackgroundArea.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/components/BackgroundArea.kt
new file mode 100644
index 00000000..0f6f6062
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/components/BackgroundArea.kt
@@ -0,0 +1,85 @@
+package com.alarmy.near.presentation.feature.onboarding.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.BlurEffect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.TileMode
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.alarmy.near.presentation.ui.theme.NearTheme
+
+/**
+ * 배경 장식 컴포넌트
+ * 원형 블러 배경 장식을 제공하는 컴포넌트
+ */
+@Composable
+fun BackgroundArea() {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(NearTheme.colors.WHITE_FFFFFF),
+ ) {
+ BackgroundEllipse(
+ modifier = Modifier.offset(x = (-78).dp),
+ )
+
+ BackgroundEllipse(
+ modifier = Modifier.offset(x = 201.dp, y = 155.dp),
+ opacity = 0.2f,
+ color = NearTheme.colors.PURPLE01_4E3EC7,
+ )
+ }
+}
+
+@Composable
+fun BackgroundEllipse(
+ modifier: Modifier = Modifier,
+ color: Color = NearTheme.colors.BLUE03_58ABEC,
+ blurRadius: Float = 200f,
+ size: Dp = 251.dp,
+ opacity: Float = 0.3f,
+) {
+ Box(
+ modifier = modifier
+ .size(size)
+ .graphicsLayer {
+ renderEffect = BlurEffect(
+ radiusX = blurRadius,
+ radiusY = blurRadius,
+ edgeTreatment = TileMode.Clamp,
+ )
+ }
+ .alpha(opacity)
+ .background(
+ color = color,
+ shape = CircleShape
+ )
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+fun BackgroundAreaPreview() {
+ NearTheme {
+ BackgroundArea()
+ }
+}
+
+@Preview()
+@Composable
+fun BackgroundDecorationPreview() {
+ NearTheme {
+ BackgroundEllipse()
+ }
+}
+
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/components/OnboardingButton.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/components/OnboardingButton.kt
new file mode 100644
index 00000000..9e33148b
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/components/OnboardingButton.kt
@@ -0,0 +1,62 @@
+package com.alarmy.near.presentation.feature.onboarding.components
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.alarmy.near.R
+import com.alarmy.near.presentation.ui.component.button.NearBasicButton
+import com.alarmy.near.presentation.ui.theme.NearTheme
+
+/**
+ * 온보딩 화면용 버튼 컴포넌트
+ */
+@Composable
+fun OnboardingButton(
+ currentPage: Int,
+ totalPages: Int,
+ isLoading: Boolean = false,
+ onNextClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val isLastPage = currentPage == totalPages - 1
+ val buttonText = if (isLastPage) stringResource(R.string.onboarding_auth_button_text) else stringResource(R.string.onboarding_next_button_text)
+
+ NearBasicButton(
+ modifier =
+ modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ onClick = onNextClick,
+ enabled = !isLoading,
+ contentPadding = PaddingValues(vertical = 17.dp),
+ ) {
+ Text(
+ text = buttonText,
+ style =
+ NearTheme.typography.B1_16_BOLD,
+ )
+ }
+}
+
+@Preview
+@Composable
+fun OnboardingButtonPreview() {
+ NearTheme {
+ OnboardingButton(currentPage = 0, totalPages = 5, onNextClick = {})
+ }
+}
+
+@Preview
+@Composable
+fun OnboardingButtonLastPreview() {
+ NearTheme {
+ OnboardingButton(currentPage = 4, totalPages = 5, onNextClick = {})
+ }
+}
+
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/components/PageIndicator.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/components/PageIndicator.kt
new file mode 100644
index 00000000..63b6a6d4
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/components/PageIndicator.kt
@@ -0,0 +1,51 @@
+package com.alarmy.near.presentation.feature.onboarding.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.alarmy.near.presentation.ui.theme.NearTheme
+
+/**
+ * 페이지 인디케이터 컴포넌트
+ * 현재 페이지를 시각적으로 표시하는 점들
+ */
+@Composable
+fun PageIndicator(
+ pageCount: Int,
+ currentPage: Int,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ repeat(pageCount) { index ->
+ val isSelected = index == currentPage
+ Box(
+ modifier =
+ Modifier
+ .size(8.dp)
+ .clip(CircleShape)
+ .background(
+ if (isSelected) NearTheme.colors.BLUE01_5AA2E9 else NearTheme.colors.GRAY03_EBEBEB,
+ ),
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PageIndicatorPreview() {
+ NearTheme {
+ PageIndicator(pageCount = 3, currentPage = 1)
+ }
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/model/OnboardingPage.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/model/OnboardingPage.kt
new file mode 100644
index 00000000..ab0290eb
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/model/OnboardingPage.kt
@@ -0,0 +1,13 @@
+package com.alarmy.near.presentation.feature.onboarding.model
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+
+/**
+ * 온보딩 페이지 데이터 모델
+ * 각 페이지의 정보를 담는 데이터 클래스
+ */
+data class OnboardingPage(
+ @StringRes val titleResId: Int,
+ @DrawableRes val image: Int,
+)
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/navigation/NavigationOnboarding.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/navigation/NavigationOnboarding.kt
new file mode 100644
index 00000000..b8d39ef9
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/navigation/NavigationOnboarding.kt
@@ -0,0 +1,26 @@
+package com.alarmy.near.presentation.feature.onboarding.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.alarmy.near.presentation.feature.onboarding.OnboardingScreen
+import kotlinx.serialization.Serializable
+
+@Serializable
+object RouteOnboarding
+
+// 온보딩 화면으로 네비게이션
+fun NavController.navigateToOnboarding() {
+ navigate(RouteOnboarding) {
+ popUpTo(0) { inclusive = true }
+ }
+}
+
+// 온보딩 네비게이션 그래프
+fun NavGraphBuilder.onboardingNavGraph(onNavigateToLogin: () -> Unit) {
+ composable {
+ OnboardingScreen(
+ onNavigateToLogin = onNavigateToLogin,
+ )
+ }
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/NearFrame.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/NearFrame.kt
new file mode 100644
index 00000000..08821874
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/NearFrame.kt
@@ -0,0 +1,35 @@
+package com.alarmy.near.presentation.ui.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import com.alarmy.near.presentation.ui.theme.NearTheme
+
+@Composable
+fun NearFrame(
+ modifier: Modifier = Modifier,
+ backgroundColor: Color = NearTheme.colors.WHITE_FFFFFF,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ val density = LocalDensity.current
+ val statusBarHeightDp = with(density) { WindowInsets.statusBars.getTop(density).toDp() }
+ val navigationBarHeightDp = with(density) { WindowInsets.navigationBars.getBottom(density).toDp() }
+ Column(
+ modifier =
+ modifier
+ .fillMaxSize()
+ .background(
+ color = backgroundColor,
+ ).padding(top = statusBarHeightDp, bottom = navigationBarHeightDp),
+ content = content,
+ )
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/dropdown/NearDropDown.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/dropdown/NearDropDown.kt
new file mode 100644
index 00000000..f92309b8
--- /dev/null
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/dropdown/NearDropDown.kt
@@ -0,0 +1,84 @@
+package com.alarmy.near.presentation.ui.component.dropdown
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.alarmy.near.presentation.ui.theme.NearTheme
+
+@Composable
+fun NearDropdownMenu(
+ modifier: Modifier = Modifier,
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ DropdownMenu(
+ modifier = modifier.background(NearTheme.colors.WHITE_FFFFFF),
+ expanded = expanded,
+ shape = RoundedCornerShape(12.dp),
+ onDismissRequest = onDismissRequest,
+ content = content,
+ )
+}
+
+@Composable
+fun NearDropdownMenuItem(
+ text: String,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+) {
+ DropdownMenuItem(
+ modifier = modifier,
+ onClick = onClick,
+ text = {
+ Text(
+ text = text,
+ style = NearTheme.typography.B2_14_MEDIUM,
+ color = NearTheme.colors.BLACK_1A1A1A,
+ )
+ },
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+fun NearDropdownMenuPreview() {
+ var expanded by remember { mutableStateOf(true) }
+
+ NearTheme {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentSize(Alignment.Center)
+ ) {
+ NearDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+ NearDropdownMenuItem(
+ text = "친구 정보 수정",
+ onClick = { expanded = false }
+ )
+ NearDropdownMenuItem(
+ text = "친구 삭제",
+ onClick = { expanded = false }
+ )
+ }
+ }
+ }
+}
diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/theme/Color.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/theme/Color.kt
index 61de2917..45d620b4 100644
--- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/theme/Color.kt
+++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/theme/Color.kt
@@ -13,10 +13,13 @@ object NearColorPallete {
val GRAY04_F7F7F7 = Color(0xFFF7F7F7)
val BLUE01_5AA2E9 = Color(0xFF5AA2E9)
val BLUE02_8ACCFF = Color(0xFF8ACCFF)
+ val BLUE03_58ABEC = Color(0xFF58ABEC)
val BG01_E3F0F9 = Color(0xFFE3F0F9)
val BG02_F4F9FD = Color(0xFFF4F9FD)
val NEGATIVE_F04E4E = Color(0xFFF04E4E)
val DIM_000000 = Color(0x99000000)
+
+ val PURPLE01_4E3EC7 = Color(0xFF4E3EC7)
}
@Suppress("PropertyName")
@@ -33,6 +36,9 @@ data class NearColor(
val BG02_F4F9FD: Color = NearColorPallete.BG02_F4F9FD,
val NEGATIVE_F04E4E: Color = NearColorPallete.NEGATIVE_F04E4E,
val DIM_000000: Color = NearColorPallete.DIM_000000,
+ // 온보딩 배경 장식 색상
+ val BLUE03_58ABEC: Color = NearColorPallete.BLUE03_58ABEC,
+ val PURPLE01_4E3EC7: Color = NearColorPallete.PURPLE01_4E3EC7,
)
val darkColor = NearColor()
diff --git a/Near/app/src/main/res/drawable-hdpi/img_bg.png b/Near/app/src/main/res/drawable-hdpi/img_bg.png
new file mode 100644
index 00000000..7ad84255
Binary files /dev/null and b/Near/app/src/main/res/drawable-hdpi/img_bg.png differ
diff --git a/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_fifth.png b/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_fifth.png
new file mode 100644
index 00000000..c5429c1b
Binary files /dev/null and b/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_fifth.png differ
diff --git a/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_first.png b/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_first.png
new file mode 100644
index 00000000..e2c31e78
Binary files /dev/null and b/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_first.png differ
diff --git a/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_forth.png b/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_forth.png
new file mode 100644
index 00000000..84cbf30c
Binary files /dev/null and b/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_forth.png differ
diff --git a/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_second.png b/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_second.png
new file mode 100644
index 00000000..d5a81024
Binary files /dev/null and b/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_second.png differ
diff --git a/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_third.png b/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_third.png
new file mode 100644
index 00000000..b0895a42
Binary files /dev/null and b/Near/app/src/main/res/drawable-hdpi/img_onboarding_page_third.png differ
diff --git a/Near/app/src/main/res/drawable/img_bg.png b/Near/app/src/main/res/drawable-mdpi/img_bg.png
similarity index 100%
rename from Near/app/src/main/res/drawable/img_bg.png
rename to Near/app/src/main/res/drawable-mdpi/img_bg.png
diff --git a/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_fifth.png b/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_fifth.png
new file mode 100644
index 00000000..dc3e4d48
Binary files /dev/null and b/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_fifth.png differ
diff --git a/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_first.png b/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_first.png
new file mode 100644
index 00000000..89327daf
Binary files /dev/null and b/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_first.png differ
diff --git a/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_forth.png b/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_forth.png
new file mode 100644
index 00000000..35375156
Binary files /dev/null and b/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_forth.png differ
diff --git a/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_second.png b/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_second.png
new file mode 100644
index 00000000..5f7b59d9
Binary files /dev/null and b/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_second.png differ
diff --git a/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_third.png b/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_third.png
new file mode 100644
index 00000000..20bd4402
Binary files /dev/null and b/Near/app/src/main/res/drawable-mdpi/img_onboarding_page_third.png differ
diff --git a/Near/app/src/main/res/drawable-xhdpi/img_bg.png b/Near/app/src/main/res/drawable-xhdpi/img_bg.png
new file mode 100644
index 00000000..eca43ad9
Binary files /dev/null and b/Near/app/src/main/res/drawable-xhdpi/img_bg.png differ
diff --git a/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_fifth.png b/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_fifth.png
new file mode 100644
index 00000000..4586afd6
Binary files /dev/null and b/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_fifth.png differ
diff --git a/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_first.png b/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_first.png
new file mode 100644
index 00000000..2a910597
Binary files /dev/null and b/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_first.png differ
diff --git a/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_forth.png b/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_forth.png
new file mode 100644
index 00000000..aeccba99
Binary files /dev/null and b/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_forth.png differ
diff --git a/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_second.png b/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_second.png
new file mode 100644
index 00000000..48b7a611
Binary files /dev/null and b/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_second.png differ
diff --git a/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_third.png b/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_third.png
new file mode 100644
index 00000000..8892663c
Binary files /dev/null and b/Near/app/src/main/res/drawable-xhdpi/img_onboarding_page_third.png differ
diff --git a/Near/app/src/main/res/drawable-xxhdpi/img_bg.png b/Near/app/src/main/res/drawable-xxhdpi/img_bg.png
new file mode 100644
index 00000000..bb6e965a
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxhdpi/img_bg.png differ
diff --git a/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_fifth.png b/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_fifth.png
new file mode 100644
index 00000000..083c53fe
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_fifth.png differ
diff --git a/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_first.png b/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_first.png
new file mode 100644
index 00000000..ad6490c0
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_first.png differ
diff --git a/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_forth.png b/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_forth.png
new file mode 100644
index 00000000..c1f11f3c
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_forth.png differ
diff --git a/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_second.png b/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_second.png
new file mode 100644
index 00000000..e5e07077
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_second.png differ
diff --git a/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_third.png b/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_third.png
new file mode 100644
index 00000000..2a907a2a
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxhdpi/img_onboarding_page_third.png differ
diff --git a/Near/app/src/main/res/drawable-xxxhdpi/img_bg.png b/Near/app/src/main/res/drawable-xxxhdpi/img_bg.png
new file mode 100644
index 00000000..bc5e2345
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxxhdpi/img_bg.png differ
diff --git a/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_fifth.png b/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_fifth.png
new file mode 100644
index 00000000..df5f9c63
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_fifth.png differ
diff --git a/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_first.png b/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_first.png
new file mode 100644
index 00000000..df693e92
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_first.png differ
diff --git a/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_forth.png b/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_forth.png
new file mode 100644
index 00000000..076d62e9
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_forth.png differ
diff --git a/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_second.png b/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_second.png
new file mode 100644
index 00000000..e5b4d61e
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_second.png differ
diff --git a/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_third.png b/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_third.png
new file mode 100644
index 00000000..e6c9cdf3
Binary files /dev/null and b/Near/app/src/main/res/drawable-xxxhdpi/img_onboarding_page_third.png differ
diff --git a/Near/app/src/main/res/drawable/ic_launcher_foreground.xml b/Near/app/src/main/res/drawable/ic_launcher_foreground.xml
index 2b068d11..db210bef 100644
--- a/Near/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/Near/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -1,30 +1,33 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+ android:width="100dp"
+ android:height="101dp"
+ android:viewportWidth="100"
+ android:viewportHeight="101">
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Near/app/src/main/res/drawable/ic_near_logo_title.xml b/Near/app/src/main/res/drawable/ic_near_logo_title_primary.xml
similarity index 100%
rename from Near/app/src/main/res/drawable/ic_near_logo_title.xml
rename to Near/app/src/main/res/drawable/ic_near_logo_title_primary.xml
diff --git a/Near/app/src/main/res/drawable/ic_near_logo_title_white.xml b/Near/app/src/main/res/drawable/ic_near_logo_title_white.xml
new file mode 100644
index 00000000..68cda581
--- /dev/null
+++ b/Near/app/src/main/res/drawable/ic_near_logo_title_white.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/Near/app/src/main/res/drawable/img_100_character_success.xml b/Near/app/src/main/res/drawable/img_100_character_success.xml
new file mode 100644
index 00000000..db210bef
--- /dev/null
+++ b/Near/app/src/main/res/drawable/img_100_character_success.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Near/app/src/main/res/drawable/img_splash_logo.xml b/Near/app/src/main/res/drawable/img_splash_logo.xml
new file mode 100644
index 00000000..3ea2c402
--- /dev/null
+++ b/Near/app/src/main/res/drawable/img_splash_logo.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/Near/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Near/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index 6f3b755b..00000000
--- a/Near/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Near/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Near/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 6f3b755b..e61cccae 100644
--- a/Near/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/Near/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,6 +1,4 @@
-
-
-
-
\ No newline at end of file
+
+
diff --git a/Near/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Near/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..f1fae059
Binary files /dev/null and b/Near/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/Near/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Near/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78e..00000000
Binary files a/Near/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/Near/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/Near/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..cff35eaa
Binary files /dev/null and b/Near/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/Near/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/Near/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d1..00000000
Binary files a/Near/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/Near/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Near/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..1d716579
Binary files /dev/null and b/Near/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/Near/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/Near/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d64..00000000
Binary files a/Near/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/Near/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/Near/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..55dad60a
Binary files /dev/null and b/Near/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/Near/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/Near/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611da..00000000
Binary files a/Near/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/Near/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Near/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..4bdeb0d0
Binary files /dev/null and b/Near/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/Near/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/Near/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a3070..00000000
Binary files a/Near/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/Near/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/Near/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..c57d22ed
Binary files /dev/null and b/Near/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/Near/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/Near/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a6956..00000000
Binary files a/Near/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..10430089
Binary files /dev/null and b/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77f..00000000
Binary files a/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..afc978cf
Binary files /dev/null and b/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f508..00000000
Binary files a/Near/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..b24a89ed
Binary files /dev/null and b/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d6427..00000000
Binary files a/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..2010a26f
Binary files /dev/null and b/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae37..00000000
Binary files a/Near/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/Near/app/src/main/res/values/strings.xml b/Near/app/src/main/res/values/strings.xml
index f6daf58a..a67f8928 100644
--- a/Near/app/src/main/res/values/strings.xml
+++ b/Near/app/src/main/res/values/strings.xml
@@ -6,6 +6,19 @@
뒤로 가기
메뉴
+ 네트워크 에러가 발생했습니다.
+
+
+ 스플래쉬 배경
+
+
+ 소중한 사람들과\n더 가까워질 수 있도록
+ 연락처와 카카오톡으로\n챙길 사람 등록하기
+ 챙기고 싶은 날에\n알림 받기
+ 오늘도 잘 챙겼는지\n기록 남기기
+ 더 잘 챙길 수 있게\n메시지 추천 받기
+ 로그인/회원가입
+ 다음
Near 로고
@@ -22,6 +35,7 @@
연락처 추가
가까워 지고 싶은 사람을\n추가해보세요.
사람 추가
+ 사람 추가
전화걸기
@@ -35,13 +49,55 @@
기념일
메모
꼭 기억해야 할 내용을 기록해보세요.\n예) 날생선 X, 작년 생일에 키링 선물함 등
- 친구
+ %1$s 더 가까워졌어요
+ 친구
가족
지인
+ 프로필 상세
+ 친구
+ 가족
+ 지인
+ 연락 주기
+ 생일
+ 날짜 선택
+ 삭제하기
+ 꼭 기억해야 할 내용을 기록해보세요.\n예) 날생선 X, 작년 생일에 키링 선물함 등
+ 이름을 입력해주세요.
+ 기념일 이름
+ 추가하기
+ 기념일
+ (%1$s 마다)
+ 더 가까워졌어요!
+ 수정
+ 삭제
+ 이번달은 챙길 사람이 없네요.
+ %1$d번째 챙김
+ 챙김 기록
+ 완료
+ 이름
+ 이름을 입력해주세요.
+ 관계
+ 날짜
+ 메모
+
+
매일
매주
2주
매달
6개월
- 사람 추가
+
+
+ 수정을 그만두시나요?
+ 화면을 나가면 \n수정 내용은 저장되지 않아요.
+ 확인
+ 취소
+ 월요일
+ 화요일
+ 수요일
+ 목요일
+ 금요일
+ 토요일
+ 일요일
+
diff --git a/Near/app/src/main/res/values/themes.xml b/Near/app/src/main/res/values/themes.xml
index dbbeb9df..abc32d05 100644
--- a/Near/app/src/main/res/values/themes.xml
+++ b/Near/app/src/main/res/values/themes.xml
@@ -1,5 +1,14 @@
+
-
\ No newline at end of file
+
+
+
+
diff --git a/Near/gradle/libs.versions.toml b/Near/gradle/libs.versions.toml
index 21b73514..0c32d4ea 100644
--- a/Near/gradle/libs.versions.toml
+++ b/Near/gradle/libs.versions.toml
@@ -30,6 +30,8 @@ datastorePreferences = "1.1.7"
datastoreCore = "1.1.7"
#Kakao
v2All = "2.21.7"
+# Splash Screen
+splashScreen = "1.0.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -63,6 +65,7 @@ logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-intercep
retrofit-kotlin-serialization-converter = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofitVersion" }
androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" }
v2-all = { module = "com.kakao.sdk:v2-all", version.ref = "v2All" }
+androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashScreen" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -72,4 +75,5 @@ hilt-application = { id = "com.google.dagger.hilt.android", version.ref = "hiltV
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlintVersion" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+kotlin-parcelize = {id = "kotlin-parcelize"}