diff --git a/app/src/main/java/org/sopt/and/core/ContentType.kt b/app/src/main/java/org/sopt/and/core/ContentType.kt new file mode 100644 index 00000000..a89345a0 --- /dev/null +++ b/app/src/main/java/org/sopt/and/core/ContentType.kt @@ -0,0 +1,18 @@ +package org.sopt.and.core + +import androidx.annotation.StringRes +import org.sopt.and.R + +enum class ContentType( + @StringRes val titleResId: Int +) { + NEW_CLASSIC(R.string.type_new_classic), + DRAMA(R.string.type_drama), + ENTERTAINMENT(R.string.type_entertainment), + MOVIE(R.string.type_movie), + ANIMATION(R.string.type_animation), + ABROAD_SERIES(R.string.type_abroad_series), + INFORMATION_CULTURE(R.string.type_information_culture), + KIDS(R.string.type_kids), + MOVIE_PLUS(R.string.type_movie_plus), +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/core/component/CloseTopBar.kt b/app/src/main/java/org/sopt/and/core/component/CloseTopBar.kt new file mode 100644 index 00000000..8c9c9cd8 --- /dev/null +++ b/app/src/main/java/org/sopt/and/core/component/CloseTopBar.kt @@ -0,0 +1,41 @@ +package org.sopt.and.core.component + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color.Companion.White +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.sp +import org.sopt.and.R +import org.sopt.and.ui.theme.WavveBg + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CloseTopBar( + title: String, + onCloseClicked: () -> Unit) { + CenterAlignedTopAppBar( + title = { + Text(text = title, fontSize = 20.sp) + }, + actions = { + IconButton(onClick = onCloseClicked) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.close_top_bar_icon_description_close), + tint = White + ) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = WavveBg, + titleContentColor = White + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/core/component/HomeTobBar.kt b/app/src/main/java/org/sopt/and/core/component/HomeTobBar.kt new file mode 100644 index 00000000..44009feb --- /dev/null +++ b/app/src/main/java/org/sopt/and/core/component/HomeTobBar.kt @@ -0,0 +1,44 @@ +package org.sopt.and.core.component + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color.Companion.White +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import org.sopt.and.R +import org.sopt.and.ui.theme.WavveBg + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeTopBar( + onLiveButtonClick: () -> Unit +) { + TopAppBar( + title = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.wavve_logo), + contentDescription = stringResource(R.string.back_button_top_bar_icon_description_logo), + tint = White + ) + }, + actions = { + IconButton(onClick = onLiveButtonClick) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_cast_24), + contentDescription = stringResource(R.string.common_top_bar_icon_description_live), + tint = White + ) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = WavveBg, + titleContentColor = White + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/core/component/HomeTopBar.kt b/app/src/main/java/org/sopt/and/core/component/HomeTopBar.kt new file mode 100644 index 00000000..44009feb --- /dev/null +++ b/app/src/main/java/org/sopt/and/core/component/HomeTopBar.kt @@ -0,0 +1,44 @@ +package org.sopt.and.core.component + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color.Companion.White +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import org.sopt.and.R +import org.sopt.and.ui.theme.WavveBg + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeTopBar( + onLiveButtonClick: () -> Unit +) { + TopAppBar( + title = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.wavve_logo), + contentDescription = stringResource(R.string.back_button_top_bar_icon_description_logo), + tint = White + ) + }, + actions = { + IconButton(onClick = onLiveButtonClick) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_cast_24), + contentDescription = stringResource(R.string.common_top_bar_icon_description_live), + tint = White + ) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = WavveBg, + titleContentColor = White + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/common/APICallType.kt b/app/src/main/java/org/sopt/and/data/common/APICallType.kt new file mode 100644 index 00000000..2fe80dba --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/common/APICallType.kt @@ -0,0 +1,8 @@ +package org.sopt.and.data.common + + +object APICallType { + const val REGISTER_USER = "registerUser" + const val LOGIN_USER = "loginUser" + const val GET_MY_HOBBY = "getMyHobby" +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/datasource/MyHobbyDataSource.kt b/app/src/main/java/org/sopt/and/data/datasource/MyHobbyDataSource.kt deleted file mode 100644 index 628ef055..00000000 --- a/app/src/main/java/org/sopt/and/data/datasource/MyHobbyDataSource.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.sopt.and.data.datasource - -import org.sopt.and.data.model.response.MyHobbyResponseDto -import org.sopt.and.data.service.UserService - -class MyHobbyDataSource( - private val userService: UserService -) { - suspend fun getMyHobby(): MyHobbyResponseDto = userService.getMyHobby() -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/datasource/SignInDataSource.kt b/app/src/main/java/org/sopt/and/data/datasource/SignInDataSource.kt deleted file mode 100644 index 952c8783..00000000 --- a/app/src/main/java/org/sopt/and/data/datasource/SignInDataSource.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.sopt.and.data.datasource - -import org.sopt.and.data.model.request.SignInRequestDto -import org.sopt.and.data.model.response.SignInResponseDto -import org.sopt.and.data.service.UserService -import retrofit2.Response - -class SignInDataSource( - private val userService: UserService -) { - suspend fun signIn(request: SignInRequestDto): Response = - userService.signIn(request = request) -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/datasource/SignUpDataSource.kt b/app/src/main/java/org/sopt/and/data/datasource/SignUpDataSource.kt deleted file mode 100644 index 9f649e26..00000000 --- a/app/src/main/java/org/sopt/and/data/datasource/SignUpDataSource.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.sopt.and.data.datasource - -import org.sopt.and.data.model.request.SignUpRequestDto -import org.sopt.and.data.model.response.SignUpResponseDto -import org.sopt.and.data.service.UserService -import retrofit2.Response - -class SignUpDataSource( - private val userService: UserService -) { - suspend fun signUp(request: SignUpRequestDto): Response = - userService.signUp(request = request) -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/di/RepositoryModule.kt b/app/src/main/java/org/sopt/and/data/di/RepositoryModule.kt index c8e4e5f7..c75c93dc 100644 --- a/app/src/main/java/org/sopt/and/data/di/RepositoryModule.kt +++ b/app/src/main/java/org/sopt/and/data/di/RepositoryModule.kt @@ -4,8 +4,8 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import org.sopt.and.data.api.UserService import org.sopt.and.data.datasource.MyHobbyDataSource -import org.sopt.and.data.datasource.SignInDataSource import org.sopt.and.data.datasource.SignUpDataSource import org.sopt.and.data.repositoryimpl.MyHobbyRepositoryImpl import org.sopt.and.data.repositoryimpl.SignInRepositoryImpl @@ -21,13 +21,13 @@ object RepositoryModule { @Provides @Singleton - fun provideUserRegisterRepository(userService: SignUpDataSource): SignUpRepository { + fun provideUserRegisterRepository(userService: UserService): SignUpRepository { return SignUpRepositoryImpl(userService) } @Provides @Singleton - fun provideUserLoginRepository(userService: SignInDataSource): SignInRepository { + fun provideUserLoginRepository(userService: UserService): SignInRepository { return SignInRepositoryImpl(userService) } diff --git a/app/src/main/java/org/sopt/and/data/mapper/ErrorMapper.kt b/app/src/main/java/org/sopt/and/data/mapper/ErrorMapper.kt new file mode 100644 index 00000000..0b41d2c6 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/mapper/ErrorMapper.kt @@ -0,0 +1,32 @@ +package org.sopt.and.data.mapper + +import org.sopt.and.data.common.APICallType + +object ErrorMapper { + private val errorMapByApi = mapOf( + APICallType.REGISTER_USER to mapOf( + Pair(400, "01") to "잘못된 요청입니다.", + Pair(400, "01") to "아이디, 비밀번호, 취미를 올바르게 입력해주세요.", + Pair(404, null) to "잘못된 요청입니다.", + Pair(409, "00") to "중복된 아이디입니다." + ), + APICallType.LOGIN_USER to mapOf( + Pair(400, "01") to "비밀번호를 올바르게 입력해주세요.", + Pair(400, "02") to "비밀번호를 올바르게 입력해주세요.", + Pair(403, "01") to "아이디 혹은 비밀번호가 틀렸습니다.", + Pair(404, "00") to "잘못된 요청입니다." + ), + APICallType.GET_MY_HOBBY to mapOf( + Pair(401, "00") to "잘못된 요청입니다.", + Pair(403, "00") to "잘못된 요청입니다.", + Pair(404, "00") to "잘못된 요청입니다." + ), + + ) + fun getErrorMessage(apiName: String, statusCode: Int?, errorCode: String?): String { + val apiErrorMap = errorMapByApi[apiName] + val message = apiErrorMap?.get(Pair(statusCode, errorCode)) + + return message ?: "알 수 없는 에러" + } +} diff --git a/app/src/main/java/org/sopt/and/data/mapper/Mapper.kt b/app/src/main/java/org/sopt/and/data/mapper/Mapper.kt index 684e029e..e76077d6 100644 --- a/app/src/main/java/org/sopt/and/data/mapper/Mapper.kt +++ b/app/src/main/java/org/sopt/and/data/mapper/Mapper.kt @@ -5,6 +5,7 @@ import org.sopt.and.data.model.request.SignUpRequestDto import org.sopt.and.data.model.response.MyHobbyResponseResultDto import org.sopt.and.data.model.response.SignInResponseDto import org.sopt.and.data.model.response.SignUpResponseDto +import org.sopt.and.domain.entity.UserData import org.sopt.and.domain.model.MyHobbyEntity import org.sopt.and.domain.model.SignInInformationEntity import org.sopt.and.domain.model.SignInResponseEntity @@ -34,14 +35,18 @@ object Mapper { ) } - fun toSignInRequestDto(signInInformationEntity: SignInInformationEntity) = SignInRequestDto( - username = signInInformationEntity.username, - password = signInInformationEntity.password + fun UserData.toUserLoginRequestDto(): SignInRequestDto = SignInRequestDto( + username = this.username, + password = this.password ) - fun toSignUpRequestDto(signUpInformationEntity: SignUpInformationEntity) = SignUpRequestDto( - username = signUpInformationEntity.username, - password = signUpInformationEntity.password, - hobby = signUpInformationEntity.hobby - ) -} \ No newline at end of file + fun UserData.toRegisterRequestDto(): SignUpRequestDto { + return SignUpRequestDto( + username = this.username, + password = this.password, + hobby = this.hobby + ) + } + +} + diff --git a/app/src/main/java/org/sopt/and/data/model/BaseResponse.kt b/app/src/main/java/org/sopt/and/data/model/BaseResponse.kt new file mode 100644 index 00000000..d81f16d5 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/model/BaseResponse.kt @@ -0,0 +1,25 @@ +package org.sopt.and.data.model + +import kotlinx.serialization.Serializable +import org.sopt.and.domain.entity.BaseResult + +@Serializable +sealed class BaseResponse { + data class Success(val data: T) : BaseResponse() + data class Failure( + val statusCode: Int?, + val errorCode: String?, + val message: String + ) : BaseResponse() +} +@Serializable +data class ErrorResponse( + val code: String +) + +fun BaseResponse.toBaseResult(): BaseResult { + return when (this) { + is BaseResponse.Success -> BaseResult.Success(this.data) + is BaseResponse.Failure -> BaseResult.Error(this.message, this.errorCode) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/repositoryimpl/SignInRepositoryImpl.kt b/app/src/main/java/org/sopt/and/data/repositoryimpl/SignInRepositoryImpl.kt index e3819221..d880c0c5 100644 --- a/app/src/main/java/org/sopt/and/data/repositoryimpl/SignInRepositoryImpl.kt +++ b/app/src/main/java/org/sopt/and/data/repositoryimpl/SignInRepositoryImpl.kt @@ -1,22 +1,63 @@ package org.sopt.and.data.repositoryimpl -import org.sopt.and.data.datasource.SignInDataSource -import org.sopt.and.data.mapper.Mapper -import org.sopt.and.domain.model.SignInInformationEntity -import org.sopt.and.domain.model.SignInResponseEntity +import kotlinx.serialization.json.Json +import org.sopt.and.data.api.UserService +import org.sopt.and.data.common.APICallType +import org.sopt.and.data.mapper.ErrorMapper +import org.sopt.and.data.mapper.Mapper.toUserLoginRequestDto +import org.sopt.and.data.model.BaseResponse +import org.sopt.and.data.model.ErrorResponse +import org.sopt.and.data.model.toBaseResult +import org.sopt.and.domain.entity.BaseResult +import org.sopt.and.domain.entity.UserData +import org.sopt.and.domain.entity.UserLoginResult import org.sopt.and.domain.repository.SignInRepository +import retrofit2.HttpException +import javax.inject.Inject -class SignInRepositoryImpl( - private val signInDataSource: SignInDataSource +class SignInRepositoryImpl @Inject constructor( + private val userService: UserService ) : SignInRepository { - override suspend fun signIn(request: SignInInformationEntity): Result = - runCatching { - Mapper.toSignInResponseEntity( - signInDataSource.signIn( - Mapper.toSignInRequestDto( - request - ) + override suspend fun loginUser(user: UserData): BaseResult { + val apiResult : BaseResponse = try { + val response = userService.loginUser(user.toUserLoginRequestDto()) + if (response.isSuccessful) { + response.body()?.result?.let { + BaseResponse.Success(UserLoginResult(it.token)) + } ?: BaseResponse.Failure(null, null, "응답에 실패했습니다.") + } else { + val errorCode = response.errorBody()?.string()?.let { errorBodyString -> + try { + Json.decodeFromString(errorBodyString).code + } catch (e: Exception) { + null + } + } + val errorMessage = ErrorMapper.getErrorMessage( + APICallType.LOGIN_USER, + response.code(), + errorCode ) - )!! + BaseResponse.Failure(response.code(), errorCode, errorMessage) + } + } catch (e: HttpException) { + val errorCode = e.response()?.errorBody()?.string()?.let { errorBodyString -> + try { + Json.decodeFromString(errorBodyString).code + } catch (e: Exception) { + null + } + } + val errorMessage = ErrorMapper.getErrorMessage( + APICallType.LOGIN_USER, + e.response()?.code(), + errorCode + ) + BaseResponse.Failure(e.code(), errorCode, errorMessage) + } catch (e: Exception) { + BaseResponse.Failure(null, null, "네트워크 연결을 확인해주세요.") } + + return apiResult.toBaseResult() + } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/repositoryimpl/SignUpRepositoryImpl.kt b/app/src/main/java/org/sopt/and/data/repositoryimpl/SignUpRepositoryImpl.kt index 91567a91..6c01558d 100644 --- a/app/src/main/java/org/sopt/and/data/repositoryimpl/SignUpRepositoryImpl.kt +++ b/app/src/main/java/org/sopt/and/data/repositoryimpl/SignUpRepositoryImpl.kt @@ -1,22 +1,64 @@ package org.sopt.and.data.repositoryimpl -import org.sopt.and.data.datasource.SignUpDataSource -import org.sopt.and.data.mapper.Mapper -import org.sopt.and.domain.model.SignUpInformationEntity -import org.sopt.and.domain.model.SignUpResponseEntity +import kotlinx.serialization.json.Json +import org.sopt.and.data.api.UserService +import org.sopt.and.data.common.APICallType +import org.sopt.and.data.mapper.ErrorMapper +import org.sopt.and.data.mapper.Mapper.toRegisterRequestDto +import org.sopt.and.data.model.BaseResponse +import org.sopt.and.data.model.ErrorResponse +import org.sopt.and.data.model.toBaseResult +import org.sopt.and.domain.entity.UserData +import org.sopt.and.domain.entity.UserRegisterResult import org.sopt.and.domain.repository.SignUpRepository +import org.sopt.and.domain.entity.BaseResult +import retrofit2.HttpException +import javax.inject.Inject -class SignUpRepositoryImpl( - private val signUpDataSource: SignUpDataSource + +class SignUpRepositoryImpl @Inject constructor( + private val userService: UserService ) : SignUpRepository { - override suspend fun signUp(request: SignUpInformationEntity): Result = - runCatching { - Mapper.toSignUpResponseEntity( - signUpDataSource.signUp( - Mapper.toSignUpRequestDto( - request - ) + override suspend fun registerUser(user: UserData): BaseResult { + val apiResult : BaseResponse = try { + val response = userService.registerUser(user.toRegisterRequestDto()) + if (response.isSuccessful) { + response.body()?.result?.let { + BaseResponse.Success(UserRegisterResult(it.no)) + } ?: BaseResponse.Failure(null, null, "응답에 실패했습니다.") + } else { + val errorCode = response.errorBody()?.string()?.let { errorBodyString -> + try { + Json.decodeFromString(errorBodyString).code + } catch (e: Exception) { + null + } + } + val errorMessage = ErrorMapper.getErrorMessage( + APICallType.REGISTER_USER, + response.code(), + errorCode ) - )!! + BaseResponse.Failure(response.code(), errorCode, errorMessage) + } + } catch (e: HttpException) { + val errorCode = e.response()?.errorBody()?.string()?.let { errorBodyString -> + try { + Json.decodeFromString(errorBodyString).code + } catch (e: Exception) { + null + } + } + val errorMessage = ErrorMapper.getErrorMessage( + APICallType.REGISTER_USER, + e.response()?.code(), + errorCode + ) + BaseResponse.Failure(e.code(), errorCode, errorMessage) + } catch (e: Exception) { + BaseResponse.Failure(null, null, "네트워크 연결을 확인해주세요.") } + + return apiResult.toBaseResult() + } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/service/ApiFactory.kt b/app/src/main/java/org/sopt/and/data/service/ApiFactory.kt deleted file mode 100644 index a4a1b68a..00000000 --- a/app/src/main/java/org/sopt/and/data/service/ApiFactory.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.sopt.and.data.service - -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import org.sopt.and.BuildConfig -import retrofit2.Retrofit - -object ApiFactory { - private const val BASE_URL: String = BuildConfig.BASE_URL - - private val okHttpClient: OkHttpClient by lazy { - OkHttpClient.Builder() - .addInterceptor(AuthInterceptor(AppContext.get())) - .addInterceptor(HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - }) - .build() - } - - val retrofit: Retrofit by lazy { - Retrofit.Builder() - .baseUrl(BASE_URL) - .client(okHttpClient) - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .build() - } - - inline fun create(): T = retrofit.create(T::class.java) -} - -object ServicePool { - val userService: UserService by lazy { ApiFactory.create() } -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/service/AppContext.kt b/app/src/main/java/org/sopt/and/data/service/AppContext.kt deleted file mode 100644 index 29eb7ae9..00000000 --- a/app/src/main/java/org/sopt/and/data/service/AppContext.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.sopt.and.data.service - -import android.content.Context - -object AppContext { - private lateinit var applicationContext: Context - - fun init(context: Context) { - applicationContext = context.applicationContext - } - - fun get(): Context = applicationContext -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/service/AuthInterceptor.kt b/app/src/main/java/org/sopt/and/data/service/AuthInterceptor.kt deleted file mode 100644 index c9f89e44..00000000 --- a/app/src/main/java/org/sopt/and/data/service/AuthInterceptor.kt +++ /dev/null @@ -1,32 +0,0 @@ -package org.sopt.and.data.service - -import android.content.Context -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking -import okhttp3.Interceptor -import okhttp3.Response - -class AuthInterceptor(context: Context) : Interceptor { - private val tokenManager = TokenManager(context) - - override fun intercept(chain: Interceptor.Chain): Response { - var token: String? - - runBlocking { - token = tokenManager.getToken().first() - } - - val request = chain.request().newBuilder() - .apply { - if (token != null) { - addHeader(HEADER_NAME, token!!) - } - } - .build() - return chain.proceed(request) - } - - companion object { - const val HEADER_NAME = "token" - } -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/service/TokenManager.kt b/app/src/main/java/org/sopt/and/data/service/TokenManager.kt deleted file mode 100644 index 6f8407b8..00000000 --- a/app/src/main/java/org/sopt/and/data/service/TokenManager.kt +++ /dev/null @@ -1,32 +0,0 @@ -package org.sopt.and.data.service - -import android.content.Context -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import org.sopt.and.data.service.TokenManager.Companion.DATASTORE_NAME - -val Context.dataStore by preferencesDataStore(DATASTORE_NAME) - -class TokenManager(private val context: Context) { - private val TOKEN_KEY = stringPreferencesKey(TOKEN_NAME) - - fun getToken(): Flow { - return context.dataStore.data.map { preferences -> - preferences[TOKEN_KEY] - } - } - - suspend fun saveToken(token: String) { - context.dataStore.edit { preferences -> - preferences[TOKEN_KEY] = token - } - } - - companion object { - const val DATASTORE_NAME = "token" - const val TOKEN_NAME = "auth_token" - } -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/service/UserService.kt b/app/src/main/java/org/sopt/and/data/service/UserService.kt deleted file mode 100644 index 34d6c719..00000000 --- a/app/src/main/java/org/sopt/and/data/service/UserService.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.sopt.and.data.service - -import org.sopt.and.data.model.request.SignInRequestDto -import org.sopt.and.data.model.request.SignUpRequestDto -import org.sopt.and.data.model.response.MyHobbyResponseDto -import org.sopt.and.data.model.response.SignInResponseDto -import org.sopt.and.data.model.response.SignUpResponseDto -import retrofit2.Response -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST - -interface UserService { - suspend fun signUp(@Body request: SignUpRequestDto): Response - - @POST("/login") - suspend fun signIn(@Body request: SignInRequestDto): Response - - @GET("/user/my-hobby") - suspend fun getMyHobby(): MyHobbyResponseDto -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/entity/HomcCommonContent.kt b/app/src/main/java/org/sopt/and/domain/entity/HomcCommonContent.kt new file mode 100644 index 00000000..dde49486 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/entity/HomcCommonContent.kt @@ -0,0 +1,6 @@ +package org.sopt.and.domain.entity + +data class HomeCommonContent( + val mainTitle: String, + val contentStates: List, +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/entity/HomeContent.kt b/app/src/main/java/org/sopt/and/domain/entity/HomeContent.kt new file mode 100644 index 00000000..81d81a19 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/entity/HomeContent.kt @@ -0,0 +1,8 @@ +package org.sopt.and.domain.entity + +data class HomeContent( + val id: Int, + val title: String, + val image: Int, + val description: String, +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/repository/DummyHomeRepository.kt b/app/src/main/java/org/sopt/and/domain/repository/DummyHomeRepository.kt new file mode 100644 index 00000000..30da163e --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/repository/DummyHomeRepository.kt @@ -0,0 +1,10 @@ +package org.sopt.and.domain.repository + +import org.sopt.and.domain.entity.HomeCommonContent +import org.sopt.and.domain.entity.HomeContent + +interface DummyHomeRepository { + fun getDummyMainContents(): List + fun getDummyCommonContents(): List + fun getDummyRankingContents(): HomeCommonContent +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/home/HomeContract.kt b/app/src/main/java/org/sopt/and/presentation/home/HomeContract.kt new file mode 100644 index 00000000..0200cc36 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/home/HomeContract.kt @@ -0,0 +1,28 @@ +package org.sopt.and.presentation.home + +import org.sopt.and.core.ContentType +import org.sopt.and.domain.entity.HomeCommonContent +import org.sopt.and.domain.entity.HomeContent +import org.sopt.and.presentation.util.UiEffect +import org.sopt.and.presentation.util.UiEvent +import org.sopt.and.presentation.util.UiState + +class HomeContract { + data class HomeUiState( + val mainContents: List = emptyList(), + + val commonContents: List = emptyList(), + + val rankingContents: HomeCommonContent = HomeCommonContent( + mainTitle = "", + contentStates = emptyList() + ), + val selectedContentType: ContentType? = null + ) : UiState + + sealed class HomeUiEvent : UiEvent { + data class SetContentType(val contentType: ContentType) : HomeUiEvent() + } + + sealed class HomeUiEffect : UiEffect +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt b/app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt index 91e10516..ef7de055 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt @@ -1,81 +1,87 @@ package org.sopt.and.presentation.home -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues +import org.sopt.and.core.ContentType +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Spacer -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.foundation.lazy.LazyColumn -import androidx.compose.material3.Scaffold +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue 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 androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext import org.sopt.and.R import org.sopt.and.presentation.home.components.HomeBannerPager -import org.sopt.and.presentation.home.components.HomeBottomCoupon -import org.sopt.and.presentation.home.components.HomeTopBar import org.sopt.and.presentation.home.components.RecommendList import org.sopt.and.presentation.home.components.Top20List -import org.sopt.and.ui.theme.Grey100 + +@OptIn(ExperimentalFoundationApi::class) @Composable fun HomeScreen( - innerPadding: PaddingValues + onContentTypeSelected: (ContentType) -> Unit, + modifier: Modifier = Modifier, + viewModel: HomeViewModel = hiltViewModel() ) { - val homeViewModel = viewModel() - val homeUiState by homeViewModel.uiState.collectAsStateWithLifecycle() - - Column( - modifier = Modifier - .fillMaxSize() - .background(color = Grey100) - .padding(innerPadding) + val homeState by viewModel.uiState.collectAsStateWithLifecycle() + val mainPagerState = rememberPagerState(initialPage = Int.MAX_VALUE / 2) { + Int.MAX_VALUE // 페이지 수가 무한대 + } + LaunchedEffect(Unit) { + viewModel.getDummyHomeContent() + } + LazyColumn( + modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp) ) { - HomeTopBar(genres = homeUiState.genres) - - LazyColumn( - modifier = Modifier.fillMaxWidth() - ) { - item { - HomeBannerPager(homeUiState.banners) - } - item { - Spacer(modifier = Modifier.height(20.dp)) - } + item { + HomeBannerPager(homeState.mainContents) + } - item { - RecommendList( - title = stringResource(R.string.home_picks_of_editor_title), - items = homeUiState.recommends - ) - } + item { + Spacer(modifier = Modifier.height(20.dp)) + } - item { - Spacer(modifier = Modifier.height(20.dp)) - } + item { + RecommendList( + title = stringResource(R.string.home_picks_of_editor_title), + items = homeState.commonContents + ) + } - item { - Top20List(homeUiState.rankers) - } + item { + Spacer(modifier = Modifier.height(20.dp)) } - HomeBottomCoupon() + item { + Top20List(homeState.rankingContents) + } } } -@Preview + @Composable -fun HomeScreenPreview() { - Scaffold { innerPadding -> - HomeScreen(innerPadding = innerPadding) +fun AutoScrollEffect(pagerState: PagerState) { + LaunchedEffect(pagerState.currentPage) { + while (true) { + delay(3000) + withContext(NonCancellable) { + pagerState.animateScrollToPage( + page = pagerState.currentPage + 1, + animationSpec = spring(stiffness = Spring.StiffnessLow) + ) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/home/HomeViewModel.kt b/app/src/main/java/org/sopt/and/presentation/home/HomeViewModel.kt index a2659d93..05f322a7 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/HomeViewModel.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/HomeViewModel.kt @@ -1,11 +1,35 @@ package org.sopt.and.presentation.home -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow - -class HomeViewModel : ViewModel() { - private val _uiState = MutableStateFlow(HomeUiState()) - val uiState: StateFlow = _uiState.asStateFlow() +import dagger.hilt.android.lifecycle.HiltViewModel +import org.sopt.and.domain.repository.DummyHomeRepository +import org.sopt.and.presentation.home.HomeContract.HomeUiEffect +import org.sopt.and.presentation.home.HomeContract.HomeUiEvent +import org.sopt.and.presentation.home.HomeContract.HomeUiState +import org.sopt.and.presentation.util.BaseViewModel +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( + private val dummyHomeContentRepository: DummyHomeRepository +) : BaseViewModel(HomeUiState()) { + + override fun reduceState(event: HomeUiEvent) { + when (event) { + is HomeUiEvent.SetContentType -> { + updateState( + currentState.copy( + selectedContentType = event.contentType + ) + ) + } + } + } + + fun getDummyHomeContent() = updateState( + currentState.copy( + mainContents = dummyHomeContentRepository.getDummyMainContents(), + commonContents = dummyHomeContentRepository.getDummyCommonContents(), + rankingContents = dummyHomeContentRepository.getDummyRankingContents() + ) + ) } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/home/components/HomeBannerPager.kt b/app/src/main/java/org/sopt/and/presentation/home/components/HomeBannerPager.kt index 64d8f5da..35a4700e 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/components/HomeBannerPager.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/components/HomeBannerPager.kt @@ -28,11 +28,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.sopt.and.R +import org.sopt.and.domain.entity.HomeContent import org.sopt.and.ui.theme.Grey200 import org.sopt.and.ui.theme.White100 @Composable -fun HomeBannerPager(@DrawableRes banners: List) { +fun HomeBannerPager(@DrawableRes banners: List) { val pagerState = rememberPagerState(pageCount = { banners.size }) HorizontalPager( diff --git a/app/src/main/java/org/sopt/and/presentation/home/components/RecommendList.kt b/app/src/main/java/org/sopt/and/presentation/home/components/RecommendList.kt index e9ceeb3c..68b44e32 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/components/RecommendList.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/components/RecommendList.kt @@ -22,16 +22,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.sopt.and.R +import org.sopt.and.domain.entity.HomeCommonContent import org.sopt.and.ui.theme.Grey200 import org.sopt.and.ui.theme.White100 @Composable fun RecommendList( title: String, - items: List + items: List ) { Column( modifier = Modifier.padding(horizontal = 16.dp) diff --git a/app/src/main/java/org/sopt/and/presentation/home/components/Top20List.kt b/app/src/main/java/org/sopt/and/presentation/home/components/Top20List.kt index f05579db..63903473 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/components/Top20List.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/components/Top20List.kt @@ -23,10 +23,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.sopt.and.R +import org.sopt.and.domain.entity.HomeCommonContent import org.sopt.and.ui.theme.White100 @Composable -fun Top20List(@DrawableRes rankers: List) { +fun Top20List(@DrawableRes rankers: HomeCommonContent) { Column( modifier = Modifier.padding(horizontal = 16.dp) ) { diff --git a/app/src/main/java/org/sopt/and/presentation/util/Utils.kt b/app/src/main/java/org/sopt/and/presentation/util/Utils.kt index 6f95b13f..2f6289d7 100644 --- a/app/src/main/java/org/sopt/and/presentation/util/Utils.kt +++ b/app/src/main/java/org/sopt/and/presentation/util/Utils.kt @@ -1,5 +1,6 @@ package org.sopt.and.presentation.util +import android.annotation.SuppressLint import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import android.content.Context @@ -49,13 +50,13 @@ object Utils { if (isVisible) VisualTransformation.None else PasswordVisualTransformation() // Show a Toast message - fun Context.showToast( - @StringRes message: String - ) = Toast.makeText( - this, - this.getString(message), - Toast.LENGTH_SHORT - ).show() +// fun Context.showToast( +// @SuppressLint("SupportAnnotationUsage") @StringRes message: String +// ) = Toast.makeText( +// this, +// this.getString(message), +// Toast.LENGTH_SHORT +// ).show() // Show a Snackbar message fun Context.showSnackbar( @@ -67,6 +68,7 @@ object Utils { } } + // UiState, UiEvent, and UiEffect interfaces interface UiState interface UiEvent diff --git a/app/src/main/java/org/sopt/and/ui/theme/Color.kt b/app/src/main/java/org/sopt/and/ui/theme/Color.kt index eb728e37..0e47d672 100644 --- a/app/src/main/java/org/sopt/and/ui/theme/Color.kt +++ b/app/src/main/java/org/sopt/and/ui/theme/Color.kt @@ -17,3 +17,23 @@ val White100 = Color(0xFFF2F2F2) val Blue100 = Color(0xFF4557F0) val WavveDisabled = Color(0xFF717171) + +val WavvePrimary = Color(0xFF1351F9) +val WavveBg = Color(0xFF121212) +val BottomNavigationItemUnselected = Color(0xFFA5A5A5) + +val White = Color(0xFFFFFFFF) +val Black = Color(0xFF000000) + +val Gray = Color(0xFFF0F0F0) +val Gray1 = Color(0xFFF1F1F1) +val Gray2 = Color(0xFFDDDDDD) +val Gray3 = Color(0xFF8B8B8B) +val Gray4 = Color(0xFF434343) +val Gray5 = Color(0xFF252525) + +val KaKaoBg = Color(0xFFF6E405) +val TBg = Color(0xFF3617CD) +val NaverBg = Color(0xFF27AF11) +val AppleBg = Color(0xFFFFFFFF) +val FacebookBg = Color(0xFF3A5899) \ No newline at end of file