diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/Qualifier.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/Qualifier.kt deleted file mode 100644 index d397be3..0000000 --- a/app/src/main/java/com/hackathon/alddeul_babsang/app/Qualifier.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.hackathon.alddeul_babsang.app - -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class AlddeulRetrofit diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/DataSourceModule.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/DataSourceModule.kt similarity index 57% rename from app/src/main/java/com/hackathon/alddeul_babsang/app/DataSourceModule.kt rename to app/src/main/java/com/hackathon/alddeul_babsang/app/di/DataSourceModule.kt index fd9510c..375bc93 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/app/DataSourceModule.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/DataSourceModule.kt @@ -1,7 +1,9 @@ -package com.hackathon.alddeul_babsang.app +package com.hackathon.alddeul_babsang.app.di import com.hackathon.alddeul_babsang.data.datasource.ExampleDataSource +import com.hackathon.alddeul_babsang.data.datasource.UserPreferencesDataSource import com.hackathon.alddeul_babsang.data.datasourceimpl.ExampleDataSourceImpl +import com.hackathon.alddeul_babsang.data.datasourceimpl.UserPreferencesDataSourceImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -15,4 +17,8 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindExampleDataSource(exampleDataSourceImpl: ExampleDataSourceImpl): ExampleDataSource + + @Binds + @Singleton + abstract fun bindUserPreferencesDataSource(userPreferencesDataSourceImpl: UserPreferencesDataSourceImpl): UserPreferencesDataSource } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/Extension.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/Extension.kt similarity index 79% rename from app/src/main/java/com/hackathon/alddeul_babsang/app/Extension.kt rename to app/src/main/java/com/hackathon/alddeul_babsang/app/di/Extension.kt index 670021d..2a5997d 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/app/Extension.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/Extension.kt @@ -1,4 +1,4 @@ -package com.hackathon.alddeul_babsang.app +package com.hackathon.alddeul_babsang.app.di fun String?.isJsonObject(): Boolean = this?.startsWith("{") == true && this.endsWith("}") fun String?.isJsonArray(): Boolean = this?.startsWith("[") == true && this.endsWith("]") \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/di/Qualifier.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/Qualifier.kt new file mode 100644 index 0000000..f1870b1 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/Qualifier.kt @@ -0,0 +1,15 @@ +package com.hackathon.alddeul_babsang.app.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AlddeulRetrofit + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AccessToken + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class UserPreferences diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/RepositoryModule.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/RepositoryModule.kt similarity index 57% rename from app/src/main/java/com/hackathon/alddeul_babsang/app/RepositoryModule.kt rename to app/src/main/java/com/hackathon/alddeul_babsang/app/di/RepositoryModule.kt index 947ffd5..9e11386 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/app/RepositoryModule.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/RepositoryModule.kt @@ -1,7 +1,9 @@ -package com.hackathon.alddeul_babsang.app +package com.hackathon.alddeul_babsang.app.di import com.hackathon.alddeul_babsang.data.repositoryimpl.ExampleRepositoryImpl +import com.hackathon.alddeul_babsang.data.repositoryimpl.UserPreferencesRepositoryImpl import com.hackathon.alddeul_babsang.domain.repository.ExampleRepository +import com.hackathon.alddeul_babsang.domain.repository.UserPreferencesRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -15,4 +17,8 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindExampleRepository(exampleRepositoryImpl: ExampleRepositoryImpl): ExampleRepository + + @Binds + @Singleton + abstract fun bindUserPreferencesRepository(userPreferencesRepositoryImpl: UserPreferencesRepositoryImpl): UserPreferencesRepository } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/RetrofitModule.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/RetrofitModule.kt similarity index 80% rename from app/src/main/java/com/hackathon/alddeul_babsang/app/RetrofitModule.kt rename to app/src/main/java/com/hackathon/alddeul_babsang/app/di/RetrofitModule.kt index 455714f..3811a20 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/app/RetrofitModule.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/RetrofitModule.kt @@ -1,12 +1,14 @@ -package com.hackathon.alddeul_babsang.app +package com.hackathon.alddeul_babsang.app.di import com.hackathon.alddeul_babsang.BuildConfig.BASE_URL +import com.hackathon.alddeul_babsang.app.interceptor.TokenInterceptor import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.serialization.json.Json +import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -23,13 +25,20 @@ object RetrofitModule { @Provides @Singleton fun provideOkHttpClient( - loggingInterceptor: HttpLoggingInterceptor + loggingInterceptor: HttpLoggingInterceptor, + @AccessToken tokenInterceptor: Interceptor, ): OkHttpClient = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) + .addInterceptor(tokenInterceptor) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() + @Provides + @Singleton + @AccessToken + fun provideAuthInterceptor(interceptor: TokenInterceptor): Interceptor = interceptor + @Provides @Singleton fun provideLoggingInterceptor(): HttpLoggingInterceptor { diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/ServiceModule.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/ServiceModule.kt similarity index 91% rename from app/src/main/java/com/hackathon/alddeul_babsang/app/ServiceModule.kt rename to app/src/main/java/com/hackathon/alddeul_babsang/app/di/ServiceModule.kt index 308511a..952ed33 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/app/ServiceModule.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/ServiceModule.kt @@ -1,4 +1,4 @@ -package com.hackathon.alddeul_babsang.app +package com.hackathon.alddeul_babsang.app.di import com.sopt.data.service.ExampleApiService import dagger.Module diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/di/UserPreferencesModule.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/UserPreferencesModule.kt new file mode 100644 index 0000000..18ca339 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/UserPreferencesModule.kt @@ -0,0 +1,26 @@ +package com.hackathon.alddeul_babsang.app.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object UserPreferencesModule { + private const val USER_PREFERENCES = "user_preferences" + private val Context.userDataStore by preferencesDataStore(name = USER_PREFERENCES) + + @Provides + @Singleton + @UserPreferences + fun provideDataStore( + @ApplicationContext context: Context + ): DataStore = context.userDataStore +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/interceptor/TokenInterceptor.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/interceptor/TokenInterceptor.kt new file mode 100644 index 0000000..3dc4f5d --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/app/interceptor/TokenInterceptor.kt @@ -0,0 +1,35 @@ +package com.hackathon.alddeul_babsang.app.interceptor + +import com.hackathon.alddeul_babsang.data.datasource.UserPreferencesDataSource +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.Response +import timber.log.Timber +import javax.inject.Inject + +class TokenInterceptor @Inject constructor( + private val userPreferencesDataSource: UserPreferencesDataSource +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response = runBlocking { + val request = chain.request() + val url = request.url.toString() + + // 특정 URL 패턴에 대해서는 토큰을 추가하지 않음 + if (url.contains("/api/v1/auth/login") || + url.contains("api/v1/signup") || + url.contains("api/v1/member")) { + // 로그인 요청 등 토큰이 필요 없는 요청의 경우 + return@runBlocking chain.proceed(request) + } + + // 토큰이 필요한 요청의 경우 + val accessToken = userPreferencesDataSource.getUserAccessToken().first() + val newRequest = request.newBuilder() + .addHeader("Authorization", "Bearer $accessToken") + .build() + + Timber.tag("interceptor").d("accessToken $accessToken") + chain.proceed(newRequest) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/datasource/UserPreferencesDataSource.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasource/UserPreferencesDataSource.kt new file mode 100644 index 0000000..dd70183 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasource/UserPreferencesDataSource.kt @@ -0,0 +1,16 @@ +package com.hackathon.alddeul_babsang.data.datasource + +import kotlinx.coroutines.flow.Flow + +interface UserPreferencesDataSource { + suspend fun saveUserAccessToken(accessToken: String) + fun getUserAccessToken(): Flow + + suspend fun saveCheckLogin(checkLogin: Boolean) + fun getCheckLogin(): Flow + + suspend fun saveUserRefreshToken(refreshToken: String) + fun getUserRefreshToken(): Flow + + suspend fun clear() +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/datasourceimpl/UserPreferencesDataSourceImpl.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasourceimpl/UserPreferencesDataSourceImpl.kt new file mode 100644 index 0000000..65bcce9 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasourceimpl/UserPreferencesDataSourceImpl.kt @@ -0,0 +1,58 @@ +package com.hackathon.alddeul_babsang.data.datasourceimpl + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.hackathon.alddeul_babsang.app.di.UserPreferences +import com.hackathon.alddeul_babsang.data.datasource.UserPreferencesDataSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class UserPreferencesDataSourceImpl @Inject constructor( + @UserPreferences private val dataStore: DataStore +) : UserPreferencesDataSource { + private val USER_ACCESS_TOKEN = stringPreferencesKey("user_access_token") + private val USER_REFRESH_TOKEN = stringPreferencesKey("user_refresh_token") + private val CHECK_LOGIN = booleanPreferencesKey("check_login") + + override suspend fun saveUserAccessToken(accessToken: String) { + dataStore.edit { preferences -> + preferences[USER_ACCESS_TOKEN] = accessToken + } + } + + override fun getUserAccessToken(): Flow = dataStore.data.map { preferences -> + preferences[USER_ACCESS_TOKEN] + } + + override suspend fun saveCheckLogin(checkLogin: Boolean) { + dataStore.edit { preferences -> + preferences[CHECK_LOGIN] = checkLogin + } + } + + override fun getCheckLogin(): Flow = dataStore.data.map { preferences -> + preferences[CHECK_LOGIN] ?: false + } + + override suspend fun saveUserRefreshToken(refreshToken: String) { + dataStore.edit { preferences -> + preferences[USER_REFRESH_TOKEN] = refreshToken + } + } + + override fun getUserRefreshToken(): Flow = dataStore.data.map { preferences -> + preferences[USER_REFRESH_TOKEN] + } + + override suspend fun clear() { + dataStore.edit { preferences -> + preferences.remove(USER_ACCESS_TOKEN) + preferences.remove(USER_REFRESH_TOKEN) + preferences.remove(CHECK_LOGIN) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/repositoryimpl/UserPreferencesRepositoryImpl.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/repositoryimpl/UserPreferencesRepositoryImpl.kt new file mode 100644 index 0000000..23fefcb --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/repositoryimpl/UserPreferencesRepositoryImpl.kt @@ -0,0 +1,32 @@ +package com.hackathon.alddeul_babsang.data.repositoryimpl + +import com.hackathon.alddeul_babsang.data.datasource.UserPreferencesDataSource +import com.hackathon.alddeul_babsang.domain.repository.UserPreferencesRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class UserPreferencesRepositoryImpl @Inject constructor( + private val dataSource: UserPreferencesDataSource +) : UserPreferencesRepository { + + override suspend fun saveUserAccessToken(accessToken: String) { + dataSource.saveUserAccessToken(accessToken) + } + + override fun getUserAccessToken(): Flow = dataSource.getUserAccessToken() + + override suspend fun saveCheckLogin(checkLogin: Boolean) { + dataSource.saveCheckLogin(checkLogin) + } + + override fun getCheckLogin(): Flow = dataSource.getCheckLogin() + + override suspend fun saveUserRefreshToken(refreshToken: String) { + dataSource.saveUserRefreshToken(refreshToken) + } + + override fun getUserRefreshToken(): Flow = dataSource.getUserRefreshToken() + + override suspend fun clear() = dataSource.clear() + +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/domain/entity/BabsangDetailEntity.kt b/app/src/main/java/com/hackathon/alddeul_babsang/domain/entity/BabsangDetailEntity.kt index 5a3f808..26d31f3 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/domain/entity/BabsangDetailEntity.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/domain/entity/BabsangDetailEntity.kt @@ -9,5 +9,6 @@ data class BabsangDetailEntity( val phone: String, val rating: Double, val menu: String, - val review: List + val review: List, + var isFavorite: Boolean = false ) \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/domain/entity/BabsangListEntity.kt b/app/src/main/java/com/hackathon/alddeul_babsang/domain/entity/LikesEntity.kt similarity index 88% rename from app/src/main/java/com/hackathon/alddeul_babsang/domain/entity/BabsangListEntity.kt rename to app/src/main/java/com/hackathon/alddeul_babsang/domain/entity/LikesEntity.kt index 8a09967..031823f 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/domain/entity/BabsangListEntity.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/domain/entity/LikesEntity.kt @@ -1,6 +1,6 @@ package com.hackathon.alddeul_babsang.domain.entity -data class BabsangListEntity( +data class LikesEntity( val id: Long, val avatar: String? = null, val name: String, diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/domain/repository/UserPreferencesRepository.kt b/app/src/main/java/com/hackathon/alddeul_babsang/domain/repository/UserPreferencesRepository.kt new file mode 100644 index 0000000..bcaf429 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/domain/repository/UserPreferencesRepository.kt @@ -0,0 +1,16 @@ +package com.hackathon.alddeul_babsang.domain.repository + +import kotlinx.coroutines.flow.Flow + +interface UserPreferencesRepository { + suspend fun saveUserAccessToken(accessToken: String) + fun getUserAccessToken(): Flow + + suspend fun saveCheckLogin(checkLogin: Boolean) + fun getCheckLogin(): Flow + + suspend fun saveUserRefreshToken(refreshToken: String) + fun getUserRefreshToken(): Flow + + suspend fun clear() +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/auth/screen/LoginScreen.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/auth/screen/LoginScreen.kt index 1bded55..c17e497 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/auth/screen/LoginScreen.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/auth/screen/LoginScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.hackathon.alddeul_babsang.R import com.hackathon.alddeul_babsang.core_ui.component.AlddeulButton @@ -46,6 +47,7 @@ fun LoginRoute( ) { val systemUiController = rememberSystemUiController() val keyboardController = LocalSoftwareKeyboardController.current + val loginViewModel: LoginViewModel = hiltViewModel() SideEffect { systemUiController.setStatusBarColor( @@ -56,6 +58,10 @@ fun LoginRoute( LoginScreen( onLoginClick = { keyboardController?.hide() + loginViewModel.apply { + saveCheckLogin(true) + saveUserAccessToken("111") + } navigator.navigateMain() }, onSignUpClick = { navigator.navigateSignUp1() } diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/auth/screen/LoginViewModel.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/auth/screen/LoginViewModel.kt new file mode 100644 index 0000000..2635f97 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/auth/screen/LoginViewModel.kt @@ -0,0 +1,51 @@ +package com.hackathon.alddeul_babsang.presentation.auth.screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hackathon.alddeul_babsang.domain.repository.UserPreferencesRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val userPreferencesRepository: UserPreferencesRepository +) : ViewModel() { + private val _accessToken = MutableStateFlow(null) + val accessToken: StateFlow = _accessToken + + private val _refreshToken = MutableStateFlow(null) + val refreshToken: StateFlow = _refreshToken + + fun getUserAccessToken() = userPreferencesRepository.getUserAccessToken() + + fun saveUserAccessToken(accessToken: String) { + viewModelScope.launch { + userPreferencesRepository.saveUserAccessToken(accessToken) + } + } + + fun getCheckLogin() = userPreferencesRepository.getCheckLogin() + + fun saveCheckLogin(checkLogin: Boolean) { + viewModelScope.launch { + userPreferencesRepository.saveCheckLogin(checkLogin) + } + } + + fun saveUserRefreshToken(refreshToken: String) { + viewModelScope.launch { + userPreferencesRepository.saveUserRefreshToken(refreshToken) + } + } + + fun getUserRefreshToken() = userPreferencesRepository.getUserRefreshToken() + + fun clear() { + viewModelScope.launch { + userPreferencesRepository.clear() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/auth/screen/SplashScreen.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/auth/screen/SplashScreen.kt index 9dba23c..3dfcfee 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/auth/screen/SplashScreen.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/auth/screen/SplashScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.zIndex +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController @@ -29,6 +30,7 @@ import com.hackathon.alddeul_babsang.core_ui.theme.Peach200 import com.hackathon.alddeul_babsang.core_ui.theme.White import com.hackathon.alddeul_babsang.core_ui.theme.bmDohyeonRegular import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first @Composable fun SplashScreen(navController: NavController, modifier: Modifier = Modifier) { @@ -38,6 +40,7 @@ fun SplashScreen(navController: NavController, modifier: Modifier = Modifier) { startY = 0.0f, endY = 1500f, ) + val loginViewModel: LoginViewModel = hiltViewModel() SideEffect { systemUiController.setStatusBarColor( @@ -47,9 +50,24 @@ fun SplashScreen(navController: NavController, modifier: Modifier = Modifier) { LaunchedEffect(Unit) { delay(2500) - navController.navigate("login") { - popUpTo(navController.graph.startDestinationId) { - inclusive = true + when { + (loginViewModel.getUserAccessToken().toString().isNotBlank() && + loginViewModel.getCheckLogin().first()) -> { + navController.navigate("main") { + popUpTo("splash") { + inclusive = true + } + launchSingleTop = true + } + } + + else -> { + navController.navigate("login") { + popUpTo("splash") { + inclusive = true + } + launchSingleTop = true + } } } } diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/DetailScreen.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/DetailScreen.kt index 2300c93..cd69a6c 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/DetailScreen.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/DetailScreen.kt @@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -77,16 +79,7 @@ fun DetailRoute( } DetailScreen( - data = BabsangDetailEntity( - id = 1, - name = "송이네 밥상", - codeName = "경양식/일식", - address = "서울특별시 용산구 청파동 11", - phone = "02-210-0220", - rating = 4.3, - menu = "김치찌개: 8000, 된장찌개 9000", - review = detailViewModel.mockReviews - ), + data = detailViewModel.mockDetail, onBackClick = { navigator.navigateBack() }, onReviewClick = { id -> navigator.navigateReview(id) }, detailViewModel = detailViewModel @@ -101,8 +94,7 @@ fun DetailScreen( onReviewClick: (Long) -> Unit = {}, detailViewModel: DetailViewModel ) { - var isFavorite by remember { mutableStateOf(false) } - val scrollState = rememberScrollState() + var isFavorite by remember { mutableStateOf(data.isFavorite) } Scaffold( modifier = Modifier.fillMaxSize(), @@ -141,140 +133,140 @@ fun DetailScreen( ) }, ) { innerPadding -> - - Column( + LazyColumn( modifier = Modifier .padding(innerPadding) .fillMaxSize() - .verticalScroll(scrollState) ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(200.dp) - .background(Orange700) - .padding(horizontal = 20.dp) - ) { - AsyncImage( - model = data.avatar ?: when (data.codeName) { - "한식" -> R.drawable.ic_korean_food - "중식" -> R.drawable.ic_chinese_food - "경양식/일식" -> R.drawable.ic_japanese_food - else -> R.drawable.ic_etc_food - }, - contentDescription = null, + item { + Box( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp)) .height(200.dp) - .background(Orange400) - .then( - if (data.avatar == null) Modifier.size(100.dp) else Modifier - ), - contentScale = if (data.avatar == null) ContentScale.None else ContentScale.FillBounds, - alignment = Alignment.Center - ) - } - Box( - modifier = Modifier - .fillMaxWidth() - .background(White) - .padding(horizontal = 20.dp) - ) { - Column( + .background(Orange700) + .padding(horizontal = 20.dp) + ) { + AsyncImage( + model = data.avatar ?: when (data.codeName) { + "한식" -> R.drawable.ic_korean_food + "중식" -> R.drawable.ic_chinese_food + "경양식/일식" -> R.drawable.ic_japanese_food + else -> R.drawable.ic_etc_food + }, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp)) + .height(200.dp) + .background(Orange400) + .then( + if (data.avatar == null) Modifier.size(100.dp) else Modifier + ), + contentScale = if (data.avatar == null) ContentScale.None else ContentScale.FillBounds, + alignment = Alignment.Center + ) + } + Box( modifier = Modifier .fillMaxWidth() - .background( - color = White, - shape = RoundedCornerShape( - bottomStart = 10.dp, - bottomEnd = 10.dp - ), - ) - .border( - width = 1.dp, - color = Orange700, - shape = RoundedCornerShape( - bottomStart = 10.dp, - bottomEnd = 10.dp + .background(White) + .padding(horizontal = 20.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = White, + shape = RoundedCornerShape( + bottomStart = 10.dp, + bottomEnd = 10.dp + ), ) + .border( + width = 1.dp, + color = Orange700, + shape = RoundedCornerShape( + bottomStart = 10.dp, + bottomEnd = 10.dp + ) + ) + .padding(vertical = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = data.name, + style = head4Bold, + color = Gray900 ) - .padding(vertical = 12.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = data.name, - style = head4Bold, - color = Gray900 - ) - Text( - modifier = Modifier.padding(top = 15.dp), - text = data.address, - style = head7Regular, - color = Gray300 - ) - Text( - text = data.phone, - style = head7Regular, - color = Gray300 - ) - Spacer(modifier = Modifier.height(15.dp)) - Image( - modifier = Modifier.size(width = 185.dp, height = 35.dp), - painter = when (round(data.rating)) { - in 0.0..1.4 -> painterResource(id = R.drawable.ic_star_one) - in 1.5..2.4 -> painterResource(id = R.drawable.ic_star_two) - in 2.5..3.4 -> painterResource(id = R.drawable.ic_star_three) - in 3.5..4.4 -> painterResource(id = R.drawable.ic_star_four) - in 4.5..5.0 -> painterResource(id = R.drawable.ic_star_five) - else -> throw IllegalArgumentException("Invalid star value") - }, - contentDescription = null - ) - Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { onReviewClick(data.id) }, - colors = ButtonDefaults.buttonColors( - containerColor = Blue + Text( + modifier = Modifier.padding(top = 15.dp), + text = data.address, + style = head7Regular, + color = Gray300 ) - ) { Text( - text = stringResource(R.string.btn_detail_review), - style = body4Semi, - color = White + text = data.phone, + style = head7Regular, + color = Gray300 + ) + Spacer(modifier = Modifier.height(15.dp)) + Image( + modifier = Modifier.size(width = 185.dp, height = 35.dp), + painter = when (round(data.rating)) { + in 0.0..1.4 -> painterResource(id = R.drawable.ic_star_one) + in 1.5..2.4 -> painterResource(id = R.drawable.ic_star_two) + in 2.5..3.4 -> painterResource(id = R.drawable.ic_star_three) + in 3.5..4.4 -> painterResource(id = R.drawable.ic_star_four) + in 4.5..5.0 -> painterResource(id = R.drawable.ic_star_five) + else -> throw IllegalArgumentException("Invalid star value") + }, + contentDescription = null ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { onReviewClick(data.id) }, + colors = ButtonDefaults.buttonColors( + containerColor = Blue + ) + ) { + Text( + text = stringResource(R.string.btn_detail_review), + style = body4Semi, + color = White + ) + } } } - } - Spacer(modifier = Modifier.height(30.dp)) - Column( - modifier = Modifier.padding(horizontal = 20.dp) - ) { - Text( - modifier = Modifier.padding(bottom = 12.dp), - text = stringResource(R.string.tv_detail_menu), - style = head7Semi, - color = Gray900 - ) + Spacer(modifier = Modifier.height(30.dp)) Column( - modifier = Modifier - .fillMaxWidth() - .border( - width = 1.dp, - color = Gray200, - shape = RoundedCornerShape(10.dp) - ) - .padding(horizontal = 16.dp, vertical = 19.dp), - verticalArrangement = Arrangement.spacedBy(6.dp) + modifier = Modifier.padding(horizontal = 20.dp) ) { - for (item in detailViewModel.mockMenuList) { - MenuItem( - data = item - ) + Text( + modifier = Modifier.padding(bottom = 12.dp), + text = stringResource(R.string.tv_detail_menu), + style = head7Semi, + color = Gray900 + ) + Column( + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = Gray200, + shape = RoundedCornerShape(10.dp) + ) + .padding(horizontal = 16.dp, vertical = 19.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + for (item in detailViewModel.mockMenuList) { + MenuItem( + data = item + ) + } } } Text( - modifier = Modifier.padding(vertical = 12.dp), + modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp), text = stringResource( R.string.tv_detail_review, detailViewModel.mockReviews.size @@ -282,15 +274,12 @@ fun DetailScreen( style = head7Semi, color = Gray900 ) - Column( - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - for (item in detailViewModel.mockReviews) { - ReviewItem( - data = item - ) - } - } + } + items(detailViewModel.mockReviews) { item -> + ReviewItem( + data = item + ) + Spacer(modifier = Modifier.height(16.dp)) } } } @@ -301,25 +290,7 @@ fun DetailScreen( fun DetailScreenPreview() { AlddeulBabsangTheme { DetailScreen( - data = BabsangDetailEntity( - id = 1, - name = "송이네 밥상", - codeName = "한식", - address = "서울특별시 용산구 청파동 11", - phone = "02-210-0220", - rating = 4.5, - menu = "김치찌개: 8000, 된장찌개 9000", - review = listOf( - ReviewEntity( - id = 1, - avatar = "", - nickname = "김철수", - star = 4.5, - content = "맛있어요", - createdAt = "2021-10-10" - ) - ) - ), + data = DetailViewModel().mockDetail, detailViewModel = hiltViewModel() ) } diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/DetailViewModel.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/DetailViewModel.kt index 30dae7f..86bc8b9 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/DetailViewModel.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/DetailViewModel.kt @@ -1,6 +1,7 @@ package com.hackathon.alddeul_babsang.presentation.detail.screen import androidx.lifecycle.ViewModel +import com.hackathon.alddeul_babsang.domain.entity.BabsangDetailEntity import com.hackathon.alddeul_babsang.domain.entity.MenuEntity import com.hackathon.alddeul_babsang.domain.entity.ReviewEntity import dagger.hilt.android.lifecycle.HiltViewModel @@ -39,4 +40,25 @@ class DetailViewModel @Inject constructor() : ViewModel() { content = "위생이 별로에요!" ), ) + + val mockDetail = BabsangDetailEntity( + id = 1, + name = "송이네 밥상", + codeName = "한식", + address = "서울특별시 용산구 청파동 11", + phone = "02-210-0220", + rating = 4.5, + menu = "김치찌개: 8000, 된장찌개 9000", + review = listOf( + ReviewEntity( + id = 1, + avatar = "", + nickname = "김철수", + star = 4.5, + content = "맛있어요", + createdAt = "2021-10-10" + ) + ), + isFavorite = true + ) } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/ReviewItem.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/ReviewItem.kt index 0a5eb36..371309c 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/ReviewItem.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/ReviewItem.kt @@ -41,6 +41,7 @@ fun ReviewItem( ) { Row( modifier = Modifier + .padding(horizontal = 20.dp) .fillMaxWidth() .border( width = 1.dp, diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/navigation/ProfileNavGraph.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/navigation/ProfileNavGraph.kt index f6b6ee9..b8362da 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/navigation/ProfileNavGraph.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/navigation/ProfileNavGraph.kt @@ -2,7 +2,7 @@ package com.hackathon.alddeul_babsang.presentation.profile.navigation import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable -import com.hackathon.alddeul_babsang.presentation.profile.screen.ProfileLikeListRoute +import com.hackathon.alddeul_babsang.presentation.profile.screen.LikeRoute import com.hackathon.alddeul_babsang.presentation.profile.screen.ProfileRoute @@ -12,7 +12,7 @@ fun NavGraphBuilder.profileNavGraph( composable(route = "profile") { ProfileRoute(navigator = navigator) } - composable(route = "profileLikeList") { - ProfileLikeListRoute(navigator = navigator) + composable(route = "like") { + LikeRoute(navigator = navigator) } } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/navigation/ProfileNavigator.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/navigation/ProfileNavigator.kt index 04ea985..07d06b9 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/navigation/ProfileNavigator.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/navigation/ProfileNavigator.kt @@ -16,10 +16,14 @@ class ProfileNavigator( fun navigateLogin() { navController.navigate("login") { - popUpTo("profile") { inclusive = true } + popUpTo(0) { inclusive = true } launchSingleTop = true } } + fun navigateLike() { + navController.navigate("like") + } + } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/BabsangListItem.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeItem.kt similarity index 88% rename from app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/BabsangListItem.kt rename to app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeItem.kt index b63e9f2..012fcb1 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/BabsangListItem.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeItem.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.compose.rememberNavController import coil.compose.AsyncImage import com.hackathon.alddeul_babsang.R import com.hackathon.alddeul_babsang.core_ui.theme.AlddeulBabsangTheme @@ -38,16 +37,14 @@ import com.hackathon.alddeul_babsang.core_ui.theme.Orange900 import com.hackathon.alddeul_babsang.core_ui.theme.body2Regular import com.hackathon.alddeul_babsang.core_ui.theme.body4Regular import com.hackathon.alddeul_babsang.core_ui.theme.head4Bold -import com.hackathon.alddeul_babsang.domain.entity.BabsangListEntity -import com.hackathon.alddeul_babsang.presentation.profile.navigation.ProfileNavigator +import com.hackathon.alddeul_babsang.domain.entity.LikesEntity @Composable fun LikeItem( - navigator: ProfileNavigator, - data: BabsangListEntity + onClick: () -> Unit = {}, + data: LikesEntity ) { - - var isFavorite by remember { mutableStateOf(data.favorite ?: false) } + var isFavorite by remember { mutableStateOf(data.favorite) } // 클릭할 때마다 favorite 값 토글 val heartIconId = if (isFavorite) { @@ -56,7 +53,6 @@ fun LikeItem( R.drawable.ic_heart_white } - Column( modifier = Modifier .fillMaxWidth() @@ -65,10 +61,7 @@ fun LikeItem( color = Orange700, shape = RoundedCornerShape(14.dp) ) - .height(240.dp) - .clickable(onClick = { - navigator.navigateDetail(data.id) - }) + .clickable(onClick = { onClick() }) ) { Box( modifier = Modifier @@ -78,22 +71,19 @@ fun LikeItem( ) { // AsyncImage 로드 LoadImageWithPlaceholder(data.codeName, data.avatar) - Image( painter = painterResource(heartIconId), contentDescription = null, modifier = Modifier .padding(end = 10.dp, top = 10.dp) - .align(Alignment.TopEnd) // Box 내에서 오른쪽 끝으로 배치 + .align(Alignment.TopEnd) .clickable { // 클릭 시 좋아요 상태를 토글 isFavorite = !isFavorite } ) } - Spacer(modifier = Modifier.height(15.dp)) - Row { Text( text = data.name, @@ -101,9 +91,7 @@ fun LikeItem( color = Orange900, modifier = Modifier.padding(start = 20.dp) ) - Spacer(modifier = Modifier.width(15.dp)) - Text( text = data.codeName, style = body2Regular, @@ -113,23 +101,19 @@ fun LikeItem( .padding(bottom = 3.dp) ) } - Spacer(modifier = Modifier.height(12.dp)) - Text( text = data.address, style = body4Regular, color = Gray300, modifier = Modifier.padding(start = 20.dp) ) - Spacer(modifier = Modifier.height(7.dp)) - Text( text = data.phone, style = body4Regular, color = Gray300, - modifier = Modifier.padding(start = 20.dp) + modifier = Modifier.padding(start = 20.dp, bottom = 20.dp) ) } } @@ -143,7 +127,6 @@ fun LoadImageWithPlaceholder(codeName: String, imageUrl: String?) { else -> R.drawable.ic_etc_food } - Box( modifier = Modifier .fillMaxWidth() @@ -171,9 +154,7 @@ fun LoadImageWithPlaceholder(codeName: String, imageUrl: String?) { .fillMaxWidth() // 가로는 꽉 차게 .fillMaxHeight() // 세로도 꽉 차게 .clip(RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)) - ) - } } } @@ -181,13 +162,9 @@ fun LoadImageWithPlaceholder(codeName: String, imageUrl: String?) { @Preview(showBackground = true) @Composable fun LikeItemPreview() { - val navController = rememberNavController() - val navigator = ProfileNavigator(navController) - AlddeulBabsangTheme { LikeItem( - navigator = navigator, - data = BabsangListEntity( + data = LikesEntity( id = 1, avatar = null, name = "송이네 밥상", diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileLikeListScreen.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeScreen.kt similarity index 60% rename from app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileLikeListScreen.kt rename to app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeScreen.kt index a3929af..b1f9836 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileLikeListScreen.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeScreen.kt @@ -2,13 +2,11 @@ package com.hackathon.alddeul_babsang.presentation.profile.screen import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -18,7 +16,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource @@ -29,7 +26,6 @@ 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.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.hackathon.alddeul_babsang.R import com.hackathon.alddeul_babsang.core_ui.theme.AlddeulBabsangTheme @@ -38,15 +34,15 @@ import com.hackathon.alddeul_babsang.core_ui.theme.Orange800 import com.hackathon.alddeul_babsang.core_ui.theme.White import com.hackathon.alddeul_babsang.core_ui.theme.head4Bold import com.hackathon.alddeul_babsang.core_ui.theme.head6Semi -import com.hackathon.alddeul_babsang.domain.entity.BabsangListEntity import com.hackathon.alddeul_babsang.presentation.profile.navigation.ProfileNavigator @Composable -fun ProfileLikeListRoute( +fun LikeRoute( navigator: ProfileNavigator ) { val systemUiController = rememberSystemUiController() + val likeViewModel: LikeViewModel = hiltViewModel() SideEffect { systemUiController.setStatusBarColor( @@ -54,22 +50,20 @@ fun ProfileLikeListRoute( ) } - val babsangListViewModel: BabsangListViewModel = hiltViewModel() - ProfileLikeListScreen( - navigator = navigator, // navController 대신 navigator 전달 + LikeScreen( + onItemClick = { id -> navigator.navigateDetail(id) }, onBackClick = { navigator.navigateBack() }, - babsangListViewModel = babsangListViewModel + likeViewModel = likeViewModel ) } @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ProfileLikeListScreen( - navigator: ProfileNavigator, +fun LikeScreen( + onItemClick: (Long) -> Unit = {}, onBackClick: () -> Unit = {}, - babsangListViewModel: BabsangListViewModel + likeViewModel: LikeViewModel ) { - val scrollState = rememberScrollState() Scaffold( @@ -97,65 +91,45 @@ fun ProfileLikeListScreen( ) } ) { innerPadding -> - Column( + LazyColumn( modifier = Modifier .padding(innerPadding) .fillMaxSize() - .padding(horizontal = 20.dp, vertical = 16.dp) - .verticalScroll(scrollState), + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = buildAnnotatedString { - withStyle(style = SpanStyle(color = Orange800)) { - append("좋아요 히스토리") - } - append("를 바탕으로\n") - withStyle(style = SpanStyle(color = Orange800)) { - append("밥상을 추천") - } - append("해드려요") - }, - style = head6Semi - - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - for (item in babsangListViewModel.mockLikeList) { - LikeItem( - navigator = navigator, - data = item - ) - } + item { + Text( + modifier = Modifier.padding(top = 8.dp, start = 8.dp), + text = buildAnnotatedString { + withStyle(style = SpanStyle(color = Orange800)) { + append(stringResource(R.string.tv_like_history)) + } + append(stringResource(R.string.tv_like_connection)) + withStyle(style = SpanStyle(color = Orange800)) { + append(stringResource(R.string.tv_like_recommend)) + } + append(stringResource(R.string.tv_like_end)) + }, + style = head6Semi + ) + } + items(likeViewModel.mockLikes) { item -> + LikeItem( + onClick = { onItemClick(item.id) }, + data = item + ) } - } } - - } @Preview @Composable fun ProfileLikeListScreenPreview() { - val babsangListViewModel: BabsangListViewModel = hiltViewModel() - val navController = rememberNavController() - val navigator = ProfileNavigator(navController) - AlddeulBabsangTheme { - ProfileLikeListScreen( - navigator = navigator, - onBackClick = { }, - babsangListViewModel = babsangListViewModel + LikeScreen( + likeViewModel = hiltViewModel() ) - - } } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/BabsangListViewModel.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeViewModel.kt similarity index 84% rename from app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/BabsangListViewModel.kt rename to app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeViewModel.kt index 30e668c..00be6eb 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/BabsangListViewModel.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeViewModel.kt @@ -2,16 +2,14 @@ package com.hackathon.alddeul_babsang.presentation.profile.screen import androidx.lifecycle.ViewModel -import com.hackathon.alddeul_babsang.domain.entity.BabsangListEntity +import com.hackathon.alddeul_babsang.domain.entity.LikesEntity import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class BabsangListViewModel @Inject constructor() : ViewModel() { - - - val mockLikeList = listOf( - BabsangListEntity( +class LikeViewModel @Inject constructor() : ViewModel() { + val mockLikes = listOf( + LikesEntity( id = 1, avatar = null, name = "송이네 밥상", @@ -20,7 +18,7 @@ class BabsangListViewModel @Inject constructor() : ViewModel() { phone = "02-210-0120", favorite = true ), - BabsangListEntity( + LikesEntity( id = 2, avatar = null, name = "송이네 일식", @@ -29,7 +27,7 @@ class BabsangListViewModel @Inject constructor() : ViewModel() { phone = "02-210-0220", favorite = true ), - BabsangListEntity( + LikesEntity( id = 3, avatar = null, name = "송이네 한식", @@ -38,7 +36,7 @@ class BabsangListViewModel @Inject constructor() : ViewModel() { phone = "02-223-0220", favorite = true ), - BabsangListEntity( + LikesEntity( id = 4, avatar = null, name = "송이네 중식", @@ -47,7 +45,7 @@ class BabsangListViewModel @Inject constructor() : ViewModel() { phone = "02-223-0220", favorite = true ), - BabsangListEntity( + LikesEntity( id = 4, avatar = "https://avatars.githubusercontent.com/u/166610834?s=400&u=568eacc2e4696d563a4fd732c148edba2196e4f6&v=4", name = "송이네 밥상", @@ -56,6 +54,5 @@ class BabsangListViewModel @Inject constructor() : ViewModel() { phone = "02-223-0220", favorite = true ) - ) } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileScreenItem.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileItem.kt similarity index 66% rename from app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileScreenItem.kt rename to app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileItem.kt index 0fa90f0..2d11fdc 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileScreenItem.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileItem.kt @@ -2,9 +2,7 @@ package com.hackathon.alddeul_babsang.presentation.profile.screen import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -12,51 +10,44 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.hackathon.alddeul_babsang.R -import com.hackathon.alddeul_babsang.core_ui.theme.Gray300 +import com.hackathon.alddeul_babsang.core_ui.theme.Font_B04 import com.hackathon.alddeul_babsang.core_ui.theme.Gray400 import com.hackathon.alddeul_babsang.core_ui.theme.body1Semi -import com.hackathon.alddeul_babsang.core_ui.theme.head5Semi -import com.hackathon.alddeul_babsang.core_ui.theme.head7Semi @Composable -fun ProfileScreenItem( +fun ProfileItem( + modifier: Modifier = Modifier, text: String, onClick: () -> Unit ) { Box( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .height(40.dp) .clickable { onClick() } .padding(vertical = 8.dp) ) { Text( modifier = Modifier - .align(Alignment.CenterStart) - , + .align(Alignment.CenterStart), text = text, color = Gray400, - style = head7Semi + style = body1Semi ) IconButton( modifier = Modifier - .align(Alignment.CenterEnd) - .graphicsLayer(scaleX = -1f), + .align(Alignment.CenterEnd), onClick = { onClick() } ) { Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_back), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_right_arrow), contentDescription = null, - tint = Gray300 + tint = Font_B04 ) } } @@ -65,7 +56,7 @@ fun ProfileScreenItem( @Preview @Composable fun ProfileScreenItemPreview() { - ProfileScreenItem( + ProfileItem( text = "개인정보 수정", onClick = {} ) diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileScreen.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileScreen.kt index bc08ce4..ef42227 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileScreen.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/ProfileScreen.kt @@ -3,8 +3,6 @@ package com.hackathon.alddeul_babsang.presentation.profile.screen import android.content.Context import android.content.Intent import android.net.Uri -import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -18,12 +16,15 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -32,36 +33,38 @@ import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController +import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.hackathon.alddeul_babsang.R import com.hackathon.alddeul_babsang.core_ui.theme.AlddeulBabsangTheme +import com.hackathon.alddeul_babsang.core_ui.theme.Font_B04 +import com.hackathon.alddeul_babsang.core_ui.theme.Gray200 import com.hackathon.alddeul_babsang.core_ui.theme.Gray300 -import com.hackathon.alddeul_babsang.core_ui.theme.Gray500 +import com.hackathon.alddeul_babsang.core_ui.theme.Gray50 import com.hackathon.alddeul_babsang.core_ui.theme.Orange900 import com.hackathon.alddeul_babsang.core_ui.theme.White -import com.hackathon.alddeul_babsang.core_ui.theme.body2Semi +import com.hackathon.alddeul_babsang.core_ui.theme.Yellow +import com.hackathon.alddeul_babsang.core_ui.theme.body2Regular import com.hackathon.alddeul_babsang.core_ui.theme.head4Bold -import com.hackathon.alddeul_babsang.core_ui.theme.head5Semi import com.hackathon.alddeul_babsang.core_ui.theme.head6Semi -import com.hackathon.alddeul_babsang.core_ui.theme.head7Bold import com.hackathon.alddeul_babsang.core_ui.theme.head7Semi +import com.hackathon.alddeul_babsang.core_ui.theme.title2Semi +import com.hackathon.alddeul_babsang.core_ui.theme.title4Bold +import com.hackathon.alddeul_babsang.presentation.auth.screen.LoginViewModel import com.hackathon.alddeul_babsang.presentation.profile.navigation.ProfileNavigator +import kotlinx.coroutines.launch @Composable @@ -69,6 +72,7 @@ fun ProfileRoute( navigator: ProfileNavigator ) { val systemUiController = rememberSystemUiController() + val loginViewModel: LoginViewModel = hiltViewModel() SideEffect { systemUiController.setStatusBarColor( @@ -77,161 +81,140 @@ fun ProfileRoute( } ProfileScreen( - navController = navigator.navController, - onBackClick = { navigator.navigateLogin() } + onLikeClick = { navigator.navigateLike() }, + onBackClick = { + loginViewModel.clear() + navigator.navigateLogin() + } ) } @Composable fun ProfileScreen( - navController: NavController, + onLikeClick: () -> Unit = {}, onBackClick: () -> Unit = {}, ) { val context = LocalContext.current var showBottomSheet by remember { mutableStateOf(false) } var bottomSheetKeyword by remember { mutableStateOf("") } + val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxSize() - .background(Color.White), - - ) { + .padding(start = 23.dp, end = 10.dp) + .verticalScroll(scrollState), + ) { Row( modifier = Modifier - .padding(top = 60.dp) .fillMaxWidth() + .padding(top = 60.dp, start = 10.dp), + verticalAlignment = Alignment.CenterVertically ) { - Spacer(modifier = Modifier.width(30.dp)) AsyncImage( modifier = Modifier - .size(107.dp) + .size(110.dp) .clip(CircleShape) - .border(5.dp, Color(0xFFFAB935), CircleShape), + .border(3.dp, Yellow, CircleShape), model = "", + placeholder = painterResource(id = R.drawable.ic_signup_profile), contentDescription = null, contentScale = ContentScale.Crop ) - - Spacer(modifier = Modifier.width(30.dp)) - - Column { - Spacer(modifier = Modifier.height(25.dp)) - Text(stringResource(R.string.tv_profile_nickname), style = head4Bold) - Spacer(modifier = Modifier.height(10.dp)) - Row { + Column( + modifier = Modifier.padding(start = 18.dp), + ) { + Text( + modifier = Modifier.padding(bottom = 18.dp), + text = stringResource(R.string.tv_profile_nickname), + style = head4Bold + ) + Row( + modifier = Modifier.clickable { onLikeClick() } + ) { Text( - stringResource(R.string.tv_profile_likelist), + modifier = Modifier.padding(end = 10.dp), + text = stringResource(R.string.tv_profile_likelist), style = head7Semi, - color = Gray300 + color = Font_B04 ) - Spacer(modifier = Modifier.width(10.dp)) - - Image( - painter = painterResource(id = R.drawable.ic_back), + Icon( + painter = painterResource(id = R.drawable.ic_right_arrow), contentDescription = null, - colorFilter = ColorFilter.tint(Gray300), - modifier = Modifier - .graphicsLayer(scaleX = -1f) // 좌우 반전 - .clickable(onClick = { - navController.navigate("profileLikeList") - }) + tint = Gray300, + modifier = Modifier.size(19.dp) ) } } } - Spacer(modifier = Modifier.height(80.dp)) - Column( modifier = Modifier - .height(240.dp) - .padding(horizontal = 30.dp) + .padding(top = 65.dp) + .fillMaxSize() ) { Text( - stringResource(R.string.tv_profile_userservice), style = head6Semi, modifier = Modifier + stringResource(R.string.tv_profile_notice), + style = head6Semi, + modifier = Modifier .fillMaxWidth() - .height(30.dp) + .padding(bottom = 16.dp) ) - - Spacer(modifier = Modifier.height(20.dp)) - ProfileScreenItem( - text = stringResource(R.string.tv_profile_userservice1), + ProfileItem( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.tv_profile_service), onClick = { navigateToWebsite( context = context, - "https://flowery-alloy-47e.notion.site/1336b8d0ab7b8019a7a9da0d80ff285b" + context.getString(R.string.url_profile_service) ) } ) - - Spacer(modifier = Modifier.height(20.dp)) - ProfileScreenItem( - text = stringResource(R.string.tv_profile_userservice2), + ProfileItem( + text = stringResource(R.string.tv_profile_privacy), onClick = { navigateToWebsite( context = context, - "https://flowery-alloy-47e.notion.site/1336b8d0ab7b80949a59fe847a7e94d8" + context.getString(R.string.url_profile_privacy) ) } ) - - Spacer(modifier = Modifier.height(20.dp)) - ProfileScreenItem( - text = stringResource(R.string.tv_profile_userservice3), + ProfileItem( + text = stringResource(R.string.tv_profile_version), onClick = {} ) - - Spacer(modifier = Modifier.height(30.dp)) - HorizontalDivider( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 10.dp) + .padding(top = 10.dp) + .fillMaxWidth(), + thickness = 1.dp, + color = Gray50 ) - - } - - Spacer(modifier = Modifier.height(30.dp)) - - Column( - modifier = Modifier - .height(220.dp) - .padding(horizontal = 30.dp) - ) { Text( - stringResource(R.string.tv_profile_userservice4), style = head6Semi, modifier = Modifier - + stringResource(R.string.tv_profile_etc), + style = head6Semi, + modifier = Modifier .fillMaxWidth() - .height(30.dp) + .padding(top = 30.dp, bottom = 16.dp), ) - - Spacer(modifier = Modifier.height(20.dp)) - ProfileScreenItem( - text = stringResource(R.string.tv_profile_userservice5), + ProfileItem( + text = stringResource(R.string.tv_profile_customer_center), onClick = {} ) - - Spacer(modifier = Modifier.height(20.dp)) - ProfileScreenItem( - text = stringResource(R.string.tv_profile_userservicelogout), + ProfileItem( + text = stringResource(R.string.tv_profile_logout), onClick = { bottomSheetKeyword = "로그아웃" showBottomSheet = true } ) - - Spacer(modifier = Modifier.height(20.dp)) - ProfileScreenItem( - text = stringResource(R.string.tv_profile_userservicedelete), + ProfileItem( + text = stringResource(R.string.tv_profile_quit), onClick = { bottomSheetKeyword = "회원 탈퇴" showBottomSheet = true } ) - - Spacer(modifier = Modifier.height(20.dp)) - } if (showBottomSheet) { BottomSheetContent( @@ -240,20 +223,13 @@ fun ProfileScreen( showBottomSheet = false }, onConfirm = { - //백엔드로 쿼리 전달 onBackClick() - showBottomSheet = false } ) } } -} - -fun navigateToWebsite(context: Context, url: String) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - context.startActivity(intent) } @OptIn(ExperimentalMaterial3Api::class) @@ -264,16 +240,19 @@ fun BottomSheetContent( onConfirm: () -> Unit ) { val sheetState = rememberModalBottomSheetState() - var tv1 = "" - var tv2 = "" - if (keyword == "로그아웃") { - tv1 = stringResource(R.string.tv_profile_asklogout) - tv2 = stringResource(R.string.tv_profile_asklogout2) - } + var title by remember { mutableStateOf("") } + var subtitle by remember { mutableStateOf("") } - if (keyword == "회원 탈퇴") { - tv1 = stringResource(R.string.tv_profile_askdelete) - tv2 = stringResource(R.string.tv_profile_askdelete2) + when (keyword) { + "로그아웃" -> { + title = stringResource(R.string.tv_profile_asklogout) + subtitle = stringResource(R.string.tv_profile_asklogout2) + } + + "회원 탈퇴" -> { + title = stringResource(R.string.tv_profile_askdelete) + subtitle = stringResource(R.string.tv_profile_askdelete2) + } } ModalBottomSheet( @@ -281,26 +260,26 @@ fun BottomSheetContent( onDismissRequest = { onDismiss() }, containerColor = White, ) { + val sheetState = rememberModalBottomSheetState() + val scope = rememberCoroutineScope() + Column( modifier = Modifier - .padding(16.dp) + .padding(horizontal = 16.dp) .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = tv1, - style = head5Semi, + text = title, + style = title2Semi, modifier = Modifier.padding(bottom = 2.dp) ) - Spacer(modifier = Modifier.height(20.dp)) - Text( - text = tv2, - style = body2Semi, + text = subtitle, + style = body2Regular, modifier = Modifier.padding(bottom = 2.dp) ) Spacer(modifier = Modifier.height(41.dp)) - Row( modifier = Modifier .fillMaxWidth(), @@ -309,23 +288,25 @@ fun BottomSheetContent( Button( modifier = Modifier.weight(1f), colors = ButtonDefaults.buttonColors( - containerColor = Gray500, + containerColor = Gray200, ), shape = RoundedCornerShape(40.dp), contentPadding = PaddingValues(vertical = 19.dp), onClick = { - onDismiss() + scope.launch { sheetState.hide() }.invokeOnCompletion { + if (!sheetState.isVisible) { + onDismiss() + } + } } ) { Text( text = stringResource(R.string.btn_profile_cancel), color = White, - style = head7Bold + style = title4Bold ) } - Spacer(modifier = Modifier.width(8.dp)) - Button( modifier = Modifier.weight(1f), colors = ButtonDefaults.buttonColors( @@ -333,27 +314,35 @@ fun BottomSheetContent( ), shape = RoundedCornerShape(40.dp), contentPadding = PaddingValues(vertical = 19.dp), - onClick = { onConfirm() } + onClick = { + scope.launch { sheetState.hide() }.invokeOnCompletion { + if (!sheetState.isVisible) { + onConfirm() + } + } + } ) { Text( text = keyword, color = White, - style = head7Bold + style = title4Bold ) } - Spacer(modifier = Modifier.width(8.dp)) } + Spacer(modifier = Modifier.height(30.dp)) } } } +fun navigateToWebsite(context: Context, url: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) +} -@Preview +@Preview(showBackground = true) @Composable fun ProfileScreenPreview() { AlddeulBabsangTheme { - ProfileScreen( - navController = rememberNavController() - ) + ProfileScreen() } } diff --git a/app/src/main/res/drawable/ic_right_arrow.xml b/app/src/main/res/drawable/ic_right_arrow.xml new file mode 100644 index 0000000..b938c6c --- /dev/null +++ b/app/src/main/res/drawable/ic_right_arrow.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c12c1f4..6f84ce9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -79,13 +79,21 @@ 로그아웃 하시겠습니까? 좋아요 목록 닉네임 - 이용안내 - 서비스 이용약관 - 개인정보 처리방침 - 버전 정보 - 기타 - 고객센터 - 로그아웃 - 회원 탈퇴 + 이용안내 + 서비스 이용약관 + 개인정보 처리방침 + 버전 정보 + 기타 + 고객센터 + 로그아웃 + 회원 탈퇴 + https://flowery-alloy-47e.notion.site/1336b8d0ab7b8019a7a9da0d80ff285b + https://flowery-alloy-47e.notion.site/1336b8d0ab7b80949a59fe847a7e94d8 + + + 좋아요 히스토리 + 를 바탕으로\n + 밥상을 추천 + 해드려요 \ No newline at end of file