diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c7ef95bc..02144d2c 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -5,11 +5,10 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
- id("kotlin-kapt")
- id("com.google.dagger.hilt.android")
+ alias(libs.plugins.hilt)
+ alias(libs.plugins.ksp)
}
-
val properties = Properties().apply {
load(project.rootProject.file("local.properties").inputStream())
}
@@ -39,11 +38,11 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_17 //윈도우에서 hilt 사용 시 java 1.8은 에러가 발생하는 이슈가 있다고함 -> 17 사용 시 해결
+ targetCompatibility = JavaVersion.VERSION_17 //윈도우에서 hilt 사용 시 java 1.8은 에러가 발생하는 이슈가 있다고함 -> 17 사용 시 해결
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "17" //윈도우에서 hilt 사용 시 java 1.8은 에러가 발생하는 이슈가 있다고함 -> 17 사용 시 해결
}
buildFeatures {
compose = true
@@ -61,8 +60,8 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
- implementation(libs.androidx.navigation.runtime.ktx)
- implementation(libs.androidx.runtime.livedata)
+ implementation(libs.androidx.espresso.core)
+ implementation(libs.androidx.compose.navigation)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@@ -70,9 +69,10 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
- implementation(libs.androidx.lifecycle.viewmodel.compose)
- implementation(libs.androidx.navigation.compose)
- implementation(libs.androidx.material)
+
+ //viewmodel
+ implementation (libs.androidx.lifecycle.viewmodel.compose)
+
// Network
implementation(platform(libs.okhttp.bom))
implementation(libs.okhttp)
@@ -80,12 +80,11 @@ dependencies {
implementation(libs.retrofit)
implementation(libs.retrofit.kotlin.serialization.converter)
implementation(libs.kotlinx.serialization.json)
- //hilt
- implementation(libs.hilt.android.v2511)
- kapt(libs.hilt.compiler.v2511)
- implementation(libs.androidx.hilt.navigation.compose)
-}
-kapt {
- correctErrorTypes = true
+ // hilt
+ implementation(libs.hilt.android)
+ implementation(libs.hilt.navigation.compose)
+ ksp(libs.hilt.compiler)
+ androidTestImplementation(libs.hilt.android.testing)
+ kspAndroidTest(libs.hilt.compiler)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 54588e7b..1f0c37f8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,7 +4,7 @@
-
-
+
-
+
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/MainActivity.kt b/app/src/main/java/org/sopt/and/MainActivity.kt
deleted file mode 100644
index d28aa84e..00000000
--- a/app/src/main/java/org/sopt/and/MainActivity.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package org.sopt.and
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.compose.runtime.Composable
-import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavGraph.Companion.findStartDestination
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.rememberNavController
-import org.sopt.and.navigation.SignNavigation
-import org.sopt.and.signup.SignUpScreen
-
-import org.sopt.and.signin.SignInScreen
-
-import org.sopt.and.ui.theme.ANDANDROIDTheme
-import org.sopt.and.viewmodel.SignViewModel
-
-import dagger.hilt.android.AndroidEntryPoint
-import dagger.hilt.android.HiltAndroidApp
-
-@AndroidEntryPoint
-class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- setContent {
- ANDANDROIDTheme {
- val signViewModel: SignViewModel = viewModel()
- MyApp(signViewModel)
- }
- }
- }
-}
-
-@Composable
-fun MyApp(signViewModel: SignViewModel) {
- val navController = rememberNavController()
-
- NavHost(navController, startDestination = SignNavigation.SignUp.route) {
- composable(SignNavigation.SignUp.route) {
- SignUpScreen(
- signViewModel = signViewModel,
-// onNavigateBack = { navController.popBackStack() },
- onNavigateToSignIn = { navController.navigate("signIn") },
- )
- }
- composable(SignNavigation.SignIn.route) {
- SignInScreen(
- signViewModel = signViewModel,
- onNavigateToMain = {navController.navigate("main"){
- popUpTo(navController.graph.findStartDestination().id) { saveState = true }
- launchSingleTop = true
- }},
- onNavigateToSignUp = {navController.navigate("signUp")}
-
- )
- }
-
- composable(SignNavigation.Main.route) {
- MainScreen(signViewModel = signViewModel)
- }
- }
-}
-
diff --git a/app/src/main/java/org/sopt/and/MainScreen.kt b/app/src/main/java/org/sopt/and/MainScreen.kt
deleted file mode 100644
index 81adb7a4..00000000
--- a/app/src/main/java/org/sopt/and/MainScreen.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-package org.sopt.and
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.NavigationBar
-import androidx.compose.material3.NavigationBarItem
-import androidx.compose.material3.Icon
-import androidx.compose.material3.NavigationBarItemDefaults
-import androidx.compose.material3.Text
-import androidx.compose.material3.Scaffold
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.sp
-import androidx.navigation.NavDestination.Companion.hierarchy
-import androidx.navigation.NavGraph.Companion.findStartDestination
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.currentBackStackEntryAsState
-import androidx.navigation.compose.rememberNavController
-import org.sopt.and.navigation.BottomNavigationGraph
-import org.sopt.and.navigation.BottomNavigation
-import org.sopt.and.viewmodel.SignViewModel
-
-@Composable
-fun MainScreen(signViewModel: SignViewModel) {
- val navController = rememberNavController()
-
- Scaffold(
- bottomBar = { BottomBar(navController = navController) }
- ) { innerPadding ->
- Box(Modifier.padding(innerPadding)) {
- BottomNavigationGraph(navController = navController, signViewModel = signViewModel)
- }
- }
-}
-
-@Composable
-fun BottomBar(navController: NavHostController) {
- val screens = listOf(
- BottomNavigation.Home,
- BottomNavigation.Search,
- BottomNavigation.MY
- )
- val navBackStackEntry by navController.currentBackStackEntryAsState()
- val currentDestination = navBackStackEntry?.destination
-
- NavigationBar{
- screens.forEach { screen ->
- BottomNavItem(
- item = screen,
- isSelected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
- navController = navController
- )
- }
- }
-}
-
-@Composable
-fun RowScope.BottomNavItem(
- item: BottomNavigation,
- isSelected: Boolean?,
- navController: NavHostController
-) {
- if (isSelected != null) {
- NavigationBarItem(
- label = { Text(text = stringResource(item.title), fontSize = 10.sp) },
- icon = {
- Icon(
- imageVector = item.icon,
- contentDescription = stringResource(item.title)
- )
- },
- selected = isSelected,
- colors = NavigationBarItemDefaults.colors(
- selectedIconColor = Color.White,
- unselectedIconColor = Color.Gray
- ),
- onClick = {
- navController.navigate(item.route) {
- popUpTo(navController.graph.findStartDestination().id) {
- saveState = true
- }
- launchSingleTop = true
- }
- }
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/Route.kt b/app/src/main/java/org/sopt/and/Route.kt
new file mode 100644
index 00000000..9bcb270b
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/Route.kt
@@ -0,0 +1,22 @@
+package org.sopt.and
+
+import kotlinx.serialization.Serializable
+
+
+@Serializable
+sealed class Route(val route: String) {
+ @Serializable
+ data object Home : Route("home")
+
+ @Serializable
+ data object SignIn : Route("signIn")
+
+ @Serializable
+ data object SignUp : Route("signUp")
+
+ @Serializable
+ data object Search : Route("search")
+
+ @Serializable
+ data object MyInfo : Route("myInfo")
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/MyApplication.kt b/app/src/main/java/org/sopt/and/WavveApplication.kt.kt
similarity index 74%
rename from app/src/main/java/org/sopt/and/MyApplication.kt
rename to app/src/main/java/org/sopt/and/WavveApplication.kt.kt
index e8980f3a..279903a1 100644
--- a/app/src/main/java/org/sopt/and/MyApplication.kt
+++ b/app/src/main/java/org/sopt/and/WavveApplication.kt.kt
@@ -4,4 +4,4 @@ import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
-class MyApplication : Application()
\ No newline at end of file
+class WavveApplication : Application()
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/api/Auth.kt b/app/src/main/java/org/sopt/and/api/Auth.kt
deleted file mode 100644
index a26a42db..00000000
--- a/app/src/main/java/org/sopt/and/api/Auth.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.sopt.and.api
-
-import org.sopt.and.dto.RequestLoginData
-import org.sopt.and.dto.RequestUserRegistrationData
-import org.sopt.and.dto.ResponseLogin
-import org.sopt.and.dto.ResponseUserRegistration
-import retrofit2.Response
-import retrofit2.http.Body
-import retrofit2.http.POST
-
-
-interface UserRegistrationService {
- @POST("/user")
- suspend fun postUserRegistration(
- @Body userRequest: RequestUserRegistrationData
- ): Response
-}
-
-interface LoginService {
- @POST("/login")
- suspend fun postLogin(
- @Body loginRequeset: RequestLoginData
- ): Response
-}
-
diff --git a/app/src/main/java/org/sopt/and/api/Hobby.kt b/app/src/main/java/org/sopt/and/api/Hobby.kt
deleted file mode 100644
index c1784eb0..00000000
--- a/app/src/main/java/org/sopt/and/api/Hobby.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.sopt.and.api
-
-import org.sopt.and.dto.ResponseMyHobbyData
-import retrofit2.Response
-import retrofit2.http.GET
-import retrofit2.http.Header
-
-interface HobbyService {
- @GET("/user/my-hobby")
- suspend fun getHobby(
- @Header("token") token: String?
- ): Response
-}
\ No newline at end of file
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/Route.kt b/app/src/main/java/org/sopt/and/core/Route.kt
new file mode 100644
index 00000000..f420dbaa
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/core/Route.kt
@@ -0,0 +1,3 @@
+package org.sopt.and.core
+
+interface Route
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/core/navigation/Screen.kt b/app/src/main/java/org/sopt/and/core/navigation/Screen.kt
new file mode 100644
index 00000000..3434d700
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/core/navigation/Screen.kt
@@ -0,0 +1,21 @@
+package org.sopt.and.core.navigation
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+sealed class Screen {
+ @Serializable
+ data object SignIn: Screen()
+
+ @Serializable
+ data object SignUp : Screen()
+
+ @Serializable
+ data object My: Screen()
+
+ @Serializable
+ data object Home : Screen()
+
+ @Serializable
+ data object Search : Screen()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/core/utils/PreferenceUtil.kt b/app/src/main/java/org/sopt/and/core/utils/PreferenceUtil.kt
new file mode 100644
index 00000000..23d1adeb
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/core/utils/PreferenceUtil.kt
@@ -0,0 +1,33 @@
+package org.sopt.and.core.utils
+
+import android.content.Context
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+
+
+class PreferenceUtil @Inject constructor(
+ @ApplicationContext private val context: Context
+) {
+ private val sharedPreferences =
+ context.getSharedPreferences("wavve_prefs", Context.MODE_PRIVATE)
+
+ fun saveUserToken(token: String) {
+ sharedPreferences.edit().putString(USER_TOKEN, token).apply()
+ }
+
+ fun getUserToken(): String? {
+ return sharedPreferences.getString(USER_TOKEN, null)
+ }
+
+ fun clearUserToken() {
+ sharedPreferences.edit().remove(USER_TOKEN).apply()
+ }
+
+ fun clearAll() {
+ sharedPreferences.edit().clear().apply()
+ }
+
+ companion object {
+ private const val USER_TOKEN = "user_token"
+ }
+}
diff --git a/app/src/main/java/org/sopt/and/core/utils/SnackBarUtils.kt b/app/src/main/java/org/sopt/and/core/utils/SnackBarUtils.kt
new file mode 100644
index 00000000..0335cb3d
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/core/utils/SnackBarUtils.kt
@@ -0,0 +1,33 @@
+package org.sopt.and.core.utils
+
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+
+object SnackBarUtils {
+ private lateinit var snackBarHostState: SnackbarHostState
+
+ fun init(snackBarHostState: SnackbarHostState) {
+ SnackBarUtils.snackBarHostState = snackBarHostState
+ }
+
+ suspend fun showSnackBar(
+ message: String,
+ actionLabel: String? = null,
+ onActionClick: (() -> Unit)? = null,
+ duration: SnackbarDuration = SnackbarDuration.Short,
+ ) {
+ if (this::snackBarHostState.isInitialized) {
+ val result = snackBarHostState.showSnackbar(
+ message = message,
+ actionLabel = actionLabel,
+ duration = duration
+ )
+ if (result == SnackbarResult.ActionPerformed) {
+ onActionClick?.invoke()
+ }
+ } else {
+ throw UninitializedPropertyAccessException("SnackBarHostState가 초기화 되지 않았습니다. init()을 먼저 호출하세요")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/core/utils/UiState.kt b/app/src/main/java/org/sopt/and/core/utils/UiState.kt
new file mode 100644
index 00000000..e4159383
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/core/utils/UiState.kt
@@ -0,0 +1,8 @@
+package org.sopt.and.core.utils
+
+sealed class UiState {
+ object Loading : UiState()
+ object Empty : UiState()
+ data class Success(val data: T) : UiState()
+ object Failure : UiState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/core/utils/extension/Modifier.kt b/app/src/main/java/org/sopt/and/core/utils/extension/Modifier.kt
new file mode 100644
index 00000000..cf406cc8
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/core/utils/extension/Modifier.kt
@@ -0,0 +1,14 @@
+package org.sopt.and.core.utils.extension
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+
+fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier = composed {
+ this.clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) { onClick() }
+}
diff --git a/app/src/main/java/org/sopt/and/data/api/UserService.kt b/app/src/main/java/org/sopt/and/data/api/UserService.kt
new file mode 100644
index 00000000..76ecf1cc
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/api/UserService.kt
@@ -0,0 +1,29 @@
+package org.sopt.and.data.api
+
+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.Header
+import retrofit2.http.POST
+
+interface UserService {
+ @POST("/user")
+ suspend fun registerUser(
+ @Body request: SignUpRequestDto
+ ): Response
+
+ @POST("/login")
+ suspend fun loginUser(
+ @Body request: SignInRequestDto
+ ): Response
+
+ @GET("/user/my-hobby")
+ suspend fun getMyHobby(
+ @Header("token") token: String
+ ): Response
+}
\ 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..7bdc3557
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/common/APICallType.kt
@@ -0,0 +1,11 @@
+package org.sopt.and.data.common
+
+
+object APICallType {
+ const val REGISTER_USER = "registerUser"
+ const val LOGIN_USER = "loginUser"
+ const val GET_MY_HOBBY = "getMyHobby"
+}
+object ErrorTypeWithMessage {
+ const val INVALID_TOKEN = "다시 로그인 해주세요." //토큰이 유효하지 않거나 없음, 다시 로그인 하도록 유도함
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/data/di/DataSourceModule.kt b/app/src/main/java/org/sopt/and/data/di/DataSourceModule.kt
new file mode 100644
index 00000000..3c1113a9
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/di/DataSourceModule.kt
@@ -0,0 +1,20 @@
+package org.sopt.and.data.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.sopt.and.data.api.UserService
+import retrofit2.Retrofit
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object DataSourceModule {
+
+ @Provides
+ @Singleton
+ fun provideUserService(retrofit: Retrofit): UserService {
+ return retrofit.create(UserService::class.java)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/data/di/PreferenceModule.kt b/app/src/main/java/org/sopt/and/data/di/PreferenceModule.kt
new file mode 100644
index 00000000..21b22816
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/di/PreferenceModule.kt
@@ -0,0 +1,20 @@
+package org.sopt.and.data.di
+
+import android.content.Context
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import org.sopt.and.core.utils.PreferenceUtil
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object PreferenceModule {
+ @Provides
+ @Singleton
+ fun providePreferenceUtil(@ApplicationContext context: Context): PreferenceUtil {
+ return PreferenceUtil(context)
+ }
+}
\ 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
new file mode 100644
index 00000000..c75c93dc
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/di/RepositoryModule.kt
@@ -0,0 +1,39 @@
+package org.sopt.and.data.di
+
+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.SignUpDataSource
+import org.sopt.and.data.repositoryimpl.MyHobbyRepositoryImpl
+import org.sopt.and.data.repositoryimpl.SignInRepositoryImpl
+import org.sopt.and.data.repositoryimpl.SignUpRepositoryImpl
+import org.sopt.and.domain.repository.MyHobbyRepository
+import org.sopt.and.domain.repository.SignUpRepository
+import org.sopt.and.domain.repository.SignInRepository
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object RepositoryModule {
+
+ @Provides
+ @Singleton
+ fun provideUserRegisterRepository(userService: UserService): SignUpRepository {
+ return SignUpRepositoryImpl(userService)
+ }
+
+ @Provides
+ @Singleton
+ fun provideUserLoginRepository(userService: UserService): SignInRepository {
+ return SignInRepositoryImpl(userService)
+ }
+
+ @Provides
+ @Singleton
+ fun provideGetMyHobbyRepository(userService: MyHobbyDataSource): MyHobbyRepository {
+ return MyHobbyRepositoryImpl(userService)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/api/Api.kt b/app/src/main/java/org/sopt/and/data/di/RetrofitModule.kt
similarity index 53%
rename from app/src/main/java/org/sopt/and/api/Api.kt
rename to app/src/main/java/org/sopt/and/data/di/RetrofitModule.kt
index 36ac5ad1..6878faf4 100644
--- a/app/src/main/java/org/sopt/and/api/Api.kt
+++ b/app/src/main/java/org/sopt/and/data/di/RetrofitModule.kt
@@ -1,4 +1,4 @@
-package org.sopt.and.api
+package org.sopt.and.data.di
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import dagger.Module
@@ -9,21 +9,25 @@ import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
-import org.sopt.and.BuildConfig
+import org.sopt.and.BuildConfig.BASE_URL
import retrofit2.Retrofit
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
-object ApiFactory {
- private const val BASE_URL: String = BuildConfig.BASE_URL
+object RetrofitModule {
@Provides
@Singleton
- fun provideOkHttpClient(): OkHttpClient {
- val loggingInterceptor = HttpLoggingInterceptor().apply {
+ fun provideLoggingInterceptor(): HttpLoggingInterceptor {
+ return HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
+ }
+
+ @Provides
+ @Singleton
+ fun provideOkHttpClient(loggingInterceptor: HttpLoggingInterceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
@@ -31,30 +35,11 @@ object ApiFactory {
@Provides
@Singleton
- fun provideRetrofit(client: OkHttpClient): Retrofit {
+ fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
- .client(client)
+ .client(okHttpClient)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.build()
}
-
- @Provides
- @Singleton
- fun provideUserRegistrationService(retrofit: Retrofit): UserRegistrationService {
- return retrofit.create(UserRegistrationService::class.java)
- }
-
- @Provides
- @Singleton
- fun provideLoginService(retrofit: Retrofit): LoginService {
- return retrofit.create(LoginService::class.java)
- }
-
- @Provides
- @Singleton
- fun provideHobbyService(retrofit: Retrofit): HobbyService {
- return retrofit.create(HobbyService::class.java)
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/data/di/UseCaseModule.kt b/app/src/main/java/org/sopt/and/data/di/UseCaseModule.kt
new file mode 100644
index 00000000..2dd55535
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/di/UseCaseModule.kt
@@ -0,0 +1,36 @@
+package org.sopt.and.data.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.sopt.and.domain.repository.MyHobbyRepository
+import org.sopt.and.domain.repository.SignUpRepository
+import org.sopt.and.domain.repository.SignInRepository
+import org.sopt.and.domain.usecase.MyHobbyUseCase
+import org.sopt.and.domain.usecase.SignInUseCase
+import org.sopt.and.domain.usecase.SignUpUseCase
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object UseCaseModule {
+
+ @Provides
+ @Singleton
+ fun provideRegisterUserUseCase(userRepository: SignUpRepository): SignUpUseCase {
+ return SignUpUseCase(userRepository)
+ }
+
+ @Provides
+ @Singleton
+ fun provideLoginUserUseCase(userRepository: SignInRepository): SignInUseCase {
+ return SignInUseCase(userRepository)
+ }
+
+ @Provides
+ @Singleton
+ fun provideGetMyHobbyUseCase(userRepository: MyHobbyRepository): MyHobbyUseCase {
+ return MyHobbyUseCase(userRepository)
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..e76077d6
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/mapper/Mapper.kt
@@ -0,0 +1,52 @@
+package org.sopt.and.data.mapper
+
+import org.sopt.and.data.model.request.SignInRequestDto
+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
+import org.sopt.and.domain.model.SignUpInformationEntity
+import org.sopt.and.domain.model.SignUpResponseEntity
+import retrofit2.Response
+
+object Mapper {
+ fun toMyHobbyEntity(getHobbyResponseResultDto: MyHobbyResponseResultDto) =
+ MyHobbyEntity(myHobby = getHobbyResponseResultDto.myHobby)
+
+ fun toSignUpResponseEntity(signUpResponseDto: Response) =
+ signUpResponseDto.body()?.result?.let {
+ SignUpResponseEntity(
+ no = it.no,
+ status = signUpResponseDto.code(),
+ code = signUpResponseDto.body()!!.code
+ )
+ }
+
+ fun toSignInResponseEntity(signInResponseDto: Response) =
+ signInResponseDto.body()?.result?.let {
+ SignInResponseEntity(
+ token = it.token,
+ status = signInResponseDto.code(),
+ code = signInResponseDto.body()!!.code
+ )
+ }
+
+ fun UserData.toUserLoginRequestDto(): SignInRequestDto = SignInRequestDto(
+ username = this.username,
+ password = this.password
+ )
+
+ 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/model/request/SignInRequestDto.kt b/app/src/main/java/org/sopt/and/data/model/request/SignInRequestDto.kt
new file mode 100644
index 00000000..b711c131
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/model/request/SignInRequestDto.kt
@@ -0,0 +1,9 @@
+package org.sopt.and.data.model.request
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SignInRequestDto(
+ val username: String,
+ val password: String
+)
diff --git a/app/src/main/java/org/sopt/and/data/model/request/SignUpRequestDto.kt b/app/src/main/java/org/sopt/and/data/model/request/SignUpRequestDto.kt
new file mode 100644
index 00000000..8b85066f
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/model/request/SignUpRequestDto.kt
@@ -0,0 +1,10 @@
+package org.sopt.and.data.model.request
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SignUpRequestDto(
+ val username: String,
+ val password: String,
+ val hobby: String
+)
diff --git a/app/src/main/java/org/sopt/and/data/model/response/MyHobbyResponseDto.kt b/app/src/main/java/org/sopt/and/data/model/response/MyHobbyResponseDto.kt
new file mode 100644
index 00000000..47663316
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/model/response/MyHobbyResponseDto.kt
@@ -0,0 +1,16 @@
+package org.sopt.and.data.model.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MyHobbyResponseDto(
+ val result: MyHobbyResponseResultDto? = null,
+ val code: String? = null
+)
+
+@Serializable
+data class MyHobbyResponseResultDto(
+ @SerialName("hobby")
+ val myHobby: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/data/model/response/SignInResponseDto.kt b/app/src/main/java/org/sopt/and/data/model/response/SignInResponseDto.kt
new file mode 100644
index 00000000..1e404b98
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/model/response/SignInResponseDto.kt
@@ -0,0 +1,14 @@
+package org.sopt.and.data.model.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SignInResponseDto(
+ val result: SignInResponseResultDto? = null,
+ val code: String? = null
+)
+
+@Serializable
+data class SignInResponseResultDto(
+ val token: String
+)
diff --git a/app/src/main/java/org/sopt/and/data/model/response/SignUpReponseDto.kt b/app/src/main/java/org/sopt/and/data/model/response/SignUpReponseDto.kt
new file mode 100644
index 00000000..9e2b938e
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/model/response/SignUpReponseDto.kt
@@ -0,0 +1,14 @@
+package org.sopt.and.data.model.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SignUpResponseDto(
+ val result: SignUpResponseResultDto? = null,
+ val code: String? = null
+)
+
+@Serializable
+data class SignUpResponseResultDto(
+ val no: Int
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/data/repositoryimpl/MyHobbyRepositoryImpl.kt b/app/src/main/java/org/sopt/and/data/repositoryimpl/MyHobbyRepositoryImpl.kt
new file mode 100644
index 00000000..02abea06
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/repositoryimpl/MyHobbyRepositoryImpl.kt
@@ -0,0 +1,15 @@
+package org.sopt.and.data.repositoryimpl
+
+import org.sopt.and.data.datasource.MyHobbyDataSource
+import org.sopt.and.data.mapper.Mapper
+import org.sopt.and.domain.model.MyHobbyEntity
+import org.sopt.and.domain.repository.MyHobbyRepository
+
+class MyHobbyRepositoryImpl(
+ private val getMyHobbyDataSource: MyHobbyDataSource
+) : MyHobbyRepository {
+ override suspend fun getMyHobby(): Result =
+ runCatching {
+ getMyHobbyDataSource.getMyHobby().result?.let { Mapper.toMyHobbyEntity(it) }!!
+ }
+}
\ 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
new file mode 100644
index 00000000..d880c0c5
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/repositoryimpl/SignInRepositoryImpl.kt
@@ -0,0 +1,63 @@
+package org.sopt.and.data.repositoryimpl
+
+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 @Inject constructor(
+ private val userService: UserService
+) : SignInRepository {
+ 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
new file mode 100644
index 00000000..6c01558d
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/data/repositoryimpl/SignUpRepositoryImpl.kt
@@ -0,0 +1,64 @@
+package org.sopt.and.data.repositoryimpl
+
+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 @Inject constructor(
+ private val userService: UserService
+) : SignUpRepository {
+ 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/domain/entity/BaseResult.kt b/app/src/main/java/org/sopt/and/domain/entity/BaseResult.kt
new file mode 100644
index 00000000..cca5795d
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/entity/BaseResult.kt
@@ -0,0 +1,9 @@
+package org.sopt.and.domain.entity
+
+sealed class BaseResult {
+ data class Success(val data: T) : BaseResult()
+ data class Error(
+ val message: String,
+ val errorCode: String? = null
+ ) : BaseResult()
+}
\ 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/entity/UserData.kt b/app/src/main/java/org/sopt/and/domain/entity/UserData.kt
new file mode 100644
index 00000000..077a5634
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/entity/UserData.kt
@@ -0,0 +1,7 @@
+package org.sopt.and.domain.entity
+
+data class UserData(
+ val username: String,
+ val password: String,
+ val hobby: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/domain/entity/UserLoginResult.kt b/app/src/main/java/org/sopt/and/domain/entity/UserLoginResult.kt
new file mode 100644
index 00000000..50395fcb
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/entity/UserLoginResult.kt
@@ -0,0 +1,5 @@
+package org.sopt.and.domain.entity
+
+data class UserLoginResult(
+ val token: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/domain/entity/UserRegisterResult.kt b/app/src/main/java/org/sopt/and/domain/entity/UserRegisterResult.kt
new file mode 100644
index 00000000..5ccccc1e
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/entity/UserRegisterResult.kt
@@ -0,0 +1,5 @@
+package org.sopt.and.domain.entity
+
+data class UserRegisterResult(
+ val no: Int?
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/domain/model/MyHobbyEntity.kt b/app/src/main/java/org/sopt/and/domain/model/MyHobbyEntity.kt
new file mode 100644
index 00000000..5d97c017
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/model/MyHobbyEntity.kt
@@ -0,0 +1,5 @@
+package org.sopt.and.domain.model
+
+data class MyHobbyEntity(
+ val myHobby: String
+)
diff --git a/app/src/main/java/org/sopt/and/domain/model/SignInEntity.kt b/app/src/main/java/org/sopt/and/domain/model/SignInEntity.kt
new file mode 100644
index 00000000..fa33f839
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/model/SignInEntity.kt
@@ -0,0 +1,6 @@
+package org.sopt.and.domain.model
+
+data class SignInInformationEntity(
+ val username: String,
+ val password: String
+)
diff --git a/app/src/main/java/org/sopt/and/domain/model/SignInResponseEntity.kt b/app/src/main/java/org/sopt/and/domain/model/SignInResponseEntity.kt
new file mode 100644
index 00000000..d73a3290
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/model/SignInResponseEntity.kt
@@ -0,0 +1,7 @@
+package org.sopt.and.domain.model
+
+data class SignInResponseEntity(
+ val token: String? = null,
+ val status: Int? = null,
+ val code: String? = null
+)
diff --git a/app/src/main/java/org/sopt/and/domain/model/SignUpEntity.kt b/app/src/main/java/org/sopt/and/domain/model/SignUpEntity.kt
new file mode 100644
index 00000000..dcfcaab3
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/model/SignUpEntity.kt
@@ -0,0 +1,7 @@
+package org.sopt.and.domain.model
+
+data class SignUpInformationEntity(
+ val username: String,
+ val password: String,
+ val hobby: String
+)
diff --git a/app/src/main/java/org/sopt/and/domain/model/SignUpReseponseEntity.kt b/app/src/main/java/org/sopt/and/domain/model/SignUpReseponseEntity.kt
new file mode 100644
index 00000000..16dde9de
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/model/SignUpReseponseEntity.kt
@@ -0,0 +1,7 @@
+package org.sopt.and.domain.model
+
+data class SignUpResponseEntity(
+ val no: Int? = null,
+ val code: String? = null,
+ val status: Int? = null
+)
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/domain/repository/MyHobbyRepository.kt b/app/src/main/java/org/sopt/and/domain/repository/MyHobbyRepository.kt
new file mode 100644
index 00000000..bcadaf4b
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/repository/MyHobbyRepository.kt
@@ -0,0 +1,20 @@
+package org.sopt.and.domain.repository
+
+import org.sopt.and.data.datasource.MyHobbyDataSource
+import org.sopt.and.data.repositoryimpl.MyHobbyRepositoryImpl
+import org.sopt.and.data.service.ServicePool
+import org.sopt.and.domain.model.MyHobbyEntity
+
+interface MyHobbyRepository {
+ suspend fun getMyHobby(): Result
+
+ companion object {
+ fun create(): MyHobbyRepositoryImpl {
+ return MyHobbyRepositoryImpl(
+ MyHobbyDataSource(
+ ServicePool.userService
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/domain/repository/SignInRepository.kt b/app/src/main/java/org/sopt/and/domain/repository/SignInRepository.kt
new file mode 100644
index 00000000..1a6182bf
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/repository/SignInRepository.kt
@@ -0,0 +1,10 @@
+package org.sopt.and.domain.repository
+
+
+import org.sopt.and.domain.entity.BaseResult
+import org.sopt.and.domain.entity.UserLoginResult
+
+
+interface SignInRepository {
+ suspend fun loginUser(user: org.sopt.and.domain.entity.UserData): BaseResult
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/domain/repository/SignUpRepository.kt b/app/src/main/java/org/sopt/and/domain/repository/SignUpRepository.kt
new file mode 100644
index 00000000..d3ffed5a
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/repository/SignUpRepository.kt
@@ -0,0 +1,10 @@
+package org.sopt.and.domain.repository
+
+import org.sopt.and.domain.entity.BaseResult
+import org.sopt.and.domain.entity.UserData
+import org.sopt.and.domain.entity.UserRegisterResult
+
+
+interface SignUpRepository {
+ suspend fun registerUser(user : UserData): BaseResult
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/domain/usecase/MyHobbyUseCase.kt b/app/src/main/java/org/sopt/and/domain/usecase/MyHobbyUseCase.kt
new file mode 100644
index 00000000..cbcdb6f7
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/usecase/MyHobbyUseCase.kt
@@ -0,0 +1,11 @@
+package org.sopt.and.domain.usecase
+
+import org.sopt.and.domain.model.MyHobbyEntity
+import org.sopt.and.domain.repository.MyHobbyRepository
+
+class MyHobbyUseCase(
+ private val getMyHobbyRepository: MyHobbyRepository
+) {
+ suspend operator fun invoke(): Result =
+ getMyHobbyRepository.getMyHobby()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/domain/usecase/SignInUseCase.kt b/app/src/main/java/org/sopt/and/domain/usecase/SignInUseCase.kt
new file mode 100644
index 00000000..ce8bc1f5
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/usecase/SignInUseCase.kt
@@ -0,0 +1,15 @@
+package org.sopt.and.domain.usecase
+
+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 javax.inject.Inject
+
+class SignInUseCase @Inject constructor(
+ private val userLoginRepository: SignInRepository
+) {
+ suspend operator fun invoke(user: UserData): BaseResult {
+ return userLoginRepository.loginUser(user)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/domain/usecase/SignUpUseCase.kt b/app/src/main/java/org/sopt/and/domain/usecase/SignUpUseCase.kt
new file mode 100644
index 00000000..949babc9
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/domain/usecase/SignUpUseCase.kt
@@ -0,0 +1,15 @@
+package org.sopt.and.domain.usecase
+
+import org.sopt.and.domain.entity.BaseResult
+import org.sopt.and.domain.entity.UserData
+import org.sopt.and.domain.entity.UserRegisterResult
+import org.sopt.and.domain.repository.SignUpRepository
+import javax.inject.Inject
+
+class SignUpUseCase @Inject constructor(
+ private val userRegisterRepository: SignUpRepository
+) {
+ suspend operator fun invoke(user: UserData): BaseResult {
+ return userRegisterRepository.registerUser(user)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/dto/Auth.kt b/app/src/main/java/org/sopt/and/dto/Auth.kt
deleted file mode 100644
index f26a1496..00000000
--- a/app/src/main/java/org/sopt/and/dto/Auth.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.sopt.and.dto
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-/* 유저 등록 */
-@Serializable
-data class RequestUserRegistrationData(
- @SerialName("username")
- val userName: String,
- @SerialName("password")
- val password: String,
- @SerialName("hobby")
- val hobby: String
-)
-
-@Serializable
-data class ResponseUserRegistration(
- @SerialName("result")
- val result: ResultUserNo
-)
-
-@Serializable
-data class ResultUserNo(
- @SerialName("no")
- val no: Int
-)
-
-/* 로그인 */
-@Serializable
-data class RequestLoginData(
- @SerialName("username")
- val userName: String,
- @SerialName("password")
- val password: String
-)
-
-@Serializable
-data class ResponseLogin(
- @SerialName("result")
- val result: ResultToken
-)
-
-@Serializable
-data class ResultToken(
- @SerialName("token")
- val token: String
-)
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/dto/My.kt b/app/src/main/java/org/sopt/and/dto/My.kt
deleted file mode 100644
index 99e47962..00000000
--- a/app/src/main/java/org/sopt/and/dto/My.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.sopt.and.dto
-
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-/* 내 취미 조회 */
-@Serializable
-data class ResponseMyHobbyData(
- @SerialName("result")
- val result: ResponseMyHobbyDataResult
-)
-
-@Serializable
-data class ResponseMyHobbyDataResult(
- @SerialName("hobby")
- val hobby: String
-)
diff --git a/app/src/main/java/org/sopt/and/home/HomeScreen.kt b/app/src/main/java/org/sopt/and/home/HomeScreen.kt
deleted file mode 100644
index f0a68ae7..00000000
--- a/app/src/main/java/org/sopt/and/home/HomeScreen.kt
+++ /dev/null
@@ -1,188 +0,0 @@
-package org.sopt.and.home
-
-import androidx.annotation.StringRes
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-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.layout.ContentScale
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import kotlinx.coroutines.delay
-import org.sopt.and.R
-
-val Posters = List(9) { R.drawable.bee }
-val categories = listOf("뉴클래식", "드라마", "예능", "영화", "애니", "해외시리즈")
-val textColor = Color.White
-val categoryColor = Color.Gray
-val posterSize = Modifier.size(120.dp, 180.dp)
-val roundedCorner = RoundedCornerShape(3.dp)
-
-@Composable
-fun HomeScreen() {
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .background(Color.Black),
- verticalArrangement = Arrangement.spacedBy(15.dp)
- ) {
- item {
- CategoryRow(categories = categories)
- }
-
- item {
- BannerView()
- }
- item {
- Section(title = R.string.recommendation_label) {
- PosterRow(
- moviePosters = Posters,
- itemComposable = { poster, _ -> RecommendPosterItem(poster) })
- }
- }
- item {
- Section(title = R.string.ranking_label) {
- PosterRow(moviePosters = Posters, itemComposable = { poster, rank ->
- rank?.let {
- TopPosterItem(
- poster,
- it
- )
- }
- })
- }
- }
- }
-}
-
-
-@Composable
-fun CategoryRow(categories: List) {
- LazyRow(
- modifier = Modifier
- .padding(top = 10.dp)
- .fillMaxWidth(),
- contentPadding = PaddingValues(horizontal = 15.dp),
- horizontalArrangement = Arrangement.spacedBy(20.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- items(categories) { category ->
- Text(
- text = category,
- color = Color.LightGray,
- fontSize = 18.sp
- )
- }
- }
-}
-
-@Composable
-fun Section(@StringRes title: Int, content: @Composable () -> Unit) {
- Column(modifier = Modifier.padding(start = 15.dp)) {
- Text(
- text = stringResource(id = title),
- color = textColor,
- fontSize = 18.sp,
- fontWeight = FontWeight.W600
- )
- content()
- }
-}
-
-@Composable
-fun BannerView() {
- val pagerState = rememberPagerState(pageCount = { 5 })
-
- LaunchedEffect(pagerState) {
- while (true) {
- delay(2500)
- val nextPage = (pagerState.currentPage + 1) % 5
- pagerState.animateScrollToPage(nextPage)
- }
- }
-
- HorizontalPager(
- state = pagerState,
- modifier = Modifier
- .fillMaxWidth()
- .height(400.dp),
- contentPadding = PaddingValues(horizontal = 16.dp),
- pageSpacing = 8.dp
- ) { _ ->
- Image(
- painter = painterResource(id = R.drawable.bee),
- contentDescription = "Banner Image",
- contentScale = ContentScale.Crop,
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(0.75f)
- .background(Color.DarkGray)
- )
- }
-}
-
-@Composable
-fun PosterRow(moviePosters: List, itemComposable: @Composable (Int, Int?) -> Unit) {
- LazyRow(
- contentPadding = PaddingValues(15.dp),
- horizontalArrangement = Arrangement.spacedBy(10.dp)
- ) {
- itemsIndexed(moviePosters) { index, poster ->
- itemComposable(poster, index + 1)
- }
- }
-}
-
-
-@Composable
-fun RecommendPosterItem(posterItem: Int) {
- Image(
- painter = painterResource(id = posterItem),
- contentDescription = "영화 포스터",
- modifier = posterSize.clip(roundedCorner),
- contentScale = ContentScale.Crop
- )
-}
-
-@Composable
-fun TopPosterItem(posterItem: Int, rank: Int) {
- Box(
- modifier = Modifier.size(170.dp, 255.dp)
- ) {
- Image(
- painter = painterResource(id = posterItem),
- contentDescription = "영화 포스터",
- modifier = Modifier
- .size(150.dp, 225.dp)
- .clip(roundedCorner),
- contentScale = ContentScale.Crop
- )
- Text(
- text = "$rank",
- modifier = Modifier
- .align(Alignment.BottomStart)
- .padding(start = 10.dp),
- color = textColor,
- fontSize = 40.sp,
- fontWeight = FontWeight.W800,
- fontStyle = FontStyle.Italic
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/myinfo/MyScreen.kt b/app/src/main/java/org/sopt/and/myinfo/MyScreen.kt
deleted file mode 100644
index e614936d..00000000
--- a/app/src/main/java/org/sopt/and/myinfo/MyScreen.kt
+++ /dev/null
@@ -1,180 +0,0 @@
-package org.sopt.and.myinfo
-
-import android.annotation.SuppressLint
-import android.util.Log
-import androidx.annotation.ColorRes
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AccountCircle
-import androidx.compose.material.icons.filled.Home
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-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.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import kotlinx.coroutines.launch
-import org.sopt.and.viewmodel.SignViewModel
-
-enum class BottomNavItem(val icon: ImageVector, val description: String) {
- Home(Icons.Default.Home, "홈"),
- SEARCH(Icons.Default.Search, "검색"),
- PROFILE(Icons.Default.AccountCircle, "내 정보")
-}
-
-@SuppressLint("UnrememberedMutableState")
-@Composable
-fun MyScreen(modifier: Modifier = Modifier, signViewModel: SignViewModel) {
- var hobby by mutableStateOf("") // 초기 값 확인
- val coroutineScope = rememberCoroutineScope()
-
- // 취미 데이터를 로드
- LaunchedEffect(Unit) {
- coroutineScope.launch {
- signViewModel.fetchHobby(
- onSuccess = { /* 성공 시 처리할 로직 필요 없음 - 이미 상태가 업데이트됨 */ },
- onFailure = { errorMessage ->
- Log.e("MyScreen", "취미 로드 실패: $errorMessage")
- }
- )
- }
- }
-
- Column(
- modifier = Modifier
- .fillMaxSize()
- .background(Color.Black)
- ) {
- MyHeader(hobby = hobby) // 최신 hobby 값을 전달
- Spacer(modifier = Modifier.height(20.dp))
- PurchseZone(title = "첫 결제 시 첫 달 100원!")
- Spacer(modifier = Modifier.height(15.dp))
- PurchseZone(title = "현재 보유하신 이용권이 없습니다.")
- Spacer(modifier = Modifier.height(20.dp))
- InfoZone(title = "전체 시청내역", message = "시청 내역이 없어요.")
- Spacer(modifier = Modifier.height(30.dp))
- InfoZone(title = "관심 프로그램", message = "관심 프로그램이 없어요.")
- Spacer(modifier = Modifier.weight(1f))
- BottomNavigation()
- }
-}
-
-@Composable
-fun MyHeader(hobby: String) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .background(Color.DarkGray)
- .padding(15.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Icon(
- imageVector = Icons.Default.AccountCircle,
- contentDescription = "프로필 이미지",
- modifier = Modifier.size(60.dp),
- tint = Color.White
- )
- Text(
- text = hobby.ifEmpty { "sport" }, // 초기 값 및 업데이트된 값 반영
- fontSize = 15.sp,
- color = Color.White,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(start = 10.dp)
- )
- Row {
- Text(
- text = "🔔",
- fontSize = 20.sp,
- color = Color.White,
- modifier = Modifier.padding(end = 20.dp)
- )
- Text(
- text = "⚙️",
- fontSize = 20.sp,
- color = Color.White
- )
- }
- }
-}
-
-@Composable
-fun PurchseZone(title: String) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .background(Color.DarkGray)
- .padding(vertical = 15.dp, horizontal = 15.dp)
- ) {
- Text(
- text = title,
- fontSize = 18.sp,
- color = Color.LightGray
- )
- Text(
- text = "구매하기 >",
- fontSize = 18.sp,
- color = Color.White
- )
- }
-}
-
-@Composable
-fun InfoZone(title: String, message: String) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .background(Color.Black)
- .padding(20.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = title,
- fontSize = 23.sp,
- fontWeight = FontWeight.W800,
- color = Color.White
- )
- Spacer(modifier = Modifier.height(50.dp))
- Text(
- text = "⚠️",
- fontSize = 50.sp,
- color = Color.Gray
- )
- Text(
- text = message,
- fontSize = 15.sp,
- color = Color.Gray,
- modifier = Modifier.padding(top = 15.dp)
- )
- }
-}
-
-@Composable
-fun BottomNavigation() {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- horizontalArrangement = Arrangement.SpaceAround
- ) {
- BottomNavItem.entries.forEach { item ->
- Icon(
- imageVector = item.icon,
- contentDescription = item.description,
- tint = Color.White
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/navigation/BottomNavigation.kt b/app/src/main/java/org/sopt/and/navigation/BottomNavigation.kt
deleted file mode 100644
index 3b1bd21f..00000000
--- a/app/src/main/java/org/sopt/and/navigation/BottomNavigation.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.sopt.and.navigation
-
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AccountCircle
-import androidx.compose.material.icons.filled.Home
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.ui.graphics.vector.ImageVector
-import org.sopt.and.R
-
-enum class BottomNavigation(
- val route: String,
- val title: Int,
- val icon: ImageVector
-) {
- Home(
- route = "Home",
- title = R.string.home_label,
- icon = Icons.Default.Home
- ),
- Search(
- route = "Search",
- title = R.string.search_label,
- icon = Icons.Default.Search
- ),
- MY(
- route = "MY",
- title = R.string.my_label,
- icon = Icons.Default.AccountCircle
- );
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/navigation/BottomNavigationGraph.kt b/app/src/main/java/org/sopt/and/navigation/BottomNavigationGraph.kt
deleted file mode 100644
index 32a73af0..00000000
--- a/app/src/main/java/org/sopt/and/navigation/BottomNavigationGraph.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.sopt.and.navigation
-
-import androidx.compose.runtime.Composable
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import org.sopt.and.home.HomeScreen
-import org.sopt.and.myinfo.MyScreen
-import org.sopt.and.search.SearchScreen
-import org.sopt.and.viewmodel.SignViewModel
-
-
-@Composable
-fun BottomNavigationGraph(navController: NavHostController, signViewModel: SignViewModel) {
- NavHost(navController = navController, startDestination = BottomNavigation.Home.route) {
- composable(route = BottomNavigation.Home.route) {
- HomeScreen()
- }
- composable(route = BottomNavigation.Search.route) {
- SearchScreen()
- }
- composable(route = BottomNavigation.MY.route) {
- MyScreen(signViewModel = signViewModel)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/components/CautionBox.kt b/app/src/main/java/org/sopt/and/presentation/components/CautionBox.kt
new file mode 100644
index 00000000..0d954aca
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/components/CautionBox.kt
@@ -0,0 +1,38 @@
+package org.sopt.and.presentation.components
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.sopt.and.ui.theme.Grey200
+
+@Composable
+fun CautionBox(
+ caution: Int,
+ contentDescription: Int
+) {
+ Row {
+ Icon(
+ imageVector = Icons.Outlined.Info,
+ contentDescription = stringResource(id = contentDescription),
+ tint = Grey200
+ )
+
+ Spacer(modifier = Modifier.width(5.dp))
+
+ Text(
+ text = stringResource(id = caution),
+ color = Grey200,
+ style = TextStyle(fontSize = 11.sp)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/components/EmptyInfoBox.kt b/app/src/main/java/org/sopt/and/presentation/components/EmptyInfoBox.kt
new file mode 100644
index 00000000..95f93fd3
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/components/EmptyInfoBox.kt
@@ -0,0 +1,64 @@
+package org.sopt.and.presentation.components
+
+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.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.sharp.Warning
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.sopt.and.ui.theme.White100
+
+@Composable
+fun EmptyInfoBox(
+ title: String,
+ description: String,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Text(
+ text = title,
+ color = White100,
+ style = TextStyle(
+ fontSize = 20.sp,
+ fontWeight = FontWeight(1000)
+ )
+ )
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ imageVector = Icons.Sharp.Warning,
+ contentDescription = description,
+ modifier = Modifier.size(40.dp),
+ tint = White100
+ )
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Text(
+ text = description,
+ color = White100
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/components/ShowOrToggle.kt b/app/src/main/java/org/sopt/and/presentation/components/ShowOrToggle.kt
new file mode 100644
index 00000000..91af6133
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/components/ShowOrToggle.kt
@@ -0,0 +1,25 @@
+package org.sopt.and.presentation.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import org.sopt.and.R
+import org.sopt.and.ui.theme.White100
+
+@Composable
+fun ShowOrHideToggle(
+ isVisible: Boolean,
+ onVisibilityChange: () -> Unit
+) {
+ Text(
+ text = stringResource(id = if (isVisible) R.string.hide_password_button else R.string.show_password_button),
+ color = White100,
+ modifier = Modifier
+ .padding(end = 12.dp)
+ .clickable(onClick = onVisibilityChange)
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/components/SignButton.kt b/app/src/main/java/org/sopt/and/presentation/components/SignButton.kt
similarity index 96%
rename from app/src/main/java/org/sopt/and/components/SignButton.kt
rename to app/src/main/java/org/sopt/and/presentation/components/SignButton.kt
index 3d93b97f..91e9d4a7 100644
--- a/app/src/main/java/org/sopt/and/components/SignButton.kt
+++ b/app/src/main/java/org/sopt/and/presentation/components/SignButton.kt
@@ -1,4 +1,4 @@
-package org.sopt.and.components
+package org.sopt.and.presentation.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
diff --git a/app/src/main/java/org/sopt/and/presentation/components/SignInOrSignUpTextField.kt b/app/src/main/java/org/sopt/and/presentation/components/SignInOrSignUpTextField.kt
new file mode 100644
index 00000000..6cff831e
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/components/SignInOrSignUpTextField.kt
@@ -0,0 +1,45 @@
+package org.sopt.and.presentation.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+import org.sopt.and.ui.theme.Grey100
+import org.sopt.and.ui.theme.Grey200
+
+@Composable
+fun SignInOrSignUpTextField(
+ information: String,
+ onValueChange: (String) -> Unit,
+ placeholder: Int,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ trailingIcon: @Composable (() -> Unit)? = null
+) {
+ TextField(
+ value = information,
+ onValueChange = onValueChange,
+ modifier = Modifier.fillMaxWidth(),
+ colors = TextFieldDefaults.colors(
+ unfocusedContainerColor = Grey100,
+ focusedContainerColor = Grey100,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent
+ ),
+ shape = RoundedCornerShape(10.dp),
+ placeholder = {
+ Text(
+ text = stringResource(id = placeholder),
+ color = Grey200
+ )
+ },
+ visualTransformation = visualTransformation,
+ trailingIcon = trailingIcon
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/components/SignTextField.kt b/app/src/main/java/org/sopt/and/presentation/components/SignTextField.kt
similarity index 98%
rename from app/src/main/java/org/sopt/and/components/SignTextField.kt
rename to app/src/main/java/org/sopt/and/presentation/components/SignTextField.kt
index c81538f6..fbd3a7a6 100644
--- a/app/src/main/java/org/sopt/and/components/SignTextField.kt
+++ b/app/src/main/java/org/sopt/and/presentation/components/SignTextField.kt
@@ -1,4 +1,4 @@
-package org.sopt.and.components
+package org.sopt.and.presentation.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
diff --git a/app/src/main/java/org/sopt/and/components/SignTopBar.kt b/app/src/main/java/org/sopt/and/presentation/components/SignTopBar.kt
similarity index 97%
rename from app/src/main/java/org/sopt/and/components/SignTopBar.kt
rename to app/src/main/java/org/sopt/and/presentation/components/SignTopBar.kt
index 605cb2e8..0c225e50 100644
--- a/app/src/main/java/org/sopt/and/components/SignTopBar.kt
+++ b/app/src/main/java/org/sopt/and/presentation/components/SignTopBar.kt
@@ -1,4 +1,4 @@
-package org.sopt.and.components
+package org.sopt.and.presentation.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
diff --git a/app/src/main/java/org/sopt/and/presentation/components/SnsBox.kt b/app/src/main/java/org/sopt/and/presentation/components/SnsBox.kt
new file mode 100644
index 00000000..f20f3525
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/components/SnsBox.kt
@@ -0,0 +1,92 @@
+package org.sopt.and.presentation.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+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.presentation.util.Utils
+import org.sopt.and.ui.theme.ANDANDROIDTheme
+import org.sopt.and.ui.theme.Grey200
+
+@Composable
+fun SnSBox(
+ title: String
+) {
+ Column {
+ Row(
+ Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ HorizontalDivider(
+ modifier = Modifier.weight(1f),
+ color = Grey200
+ )
+
+ Spacer(modifier = Modifier.size(4.dp))
+
+ Text(
+ text = title,
+ style = TextStyle(fontSize = 12.sp),
+ color = Grey200
+ )
+
+ Spacer(modifier = Modifier.size(3.dp))
+
+ HorizontalDivider(
+ modifier = Modifier.weight(1f),
+ color = Grey200
+ )
+ }
+
+ Spacer(modifier = Modifier.size(24.dp))
+
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ Utils.linkableSNS.forEach { item ->
+ Icon(
+ painter = painterResource(item.first),
+ contentDescription = stringResource(item.second),
+ tint = Color.Unspecified,
+ modifier = Modifier
+ .clip(CircleShape)
+ .size(42.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.size(16.dp))
+
+ Text(
+ text = stringResource(R.string.link_with_another_service_description),
+ color = Grey200,
+ style = TextStyle(
+ fontSize = 10.sp
+ )
+ )
+ }
+}
+
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
new file mode 100644
index 00000000..ef7de055
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt
@@ -0,0 +1,87 @@
+package org.sopt.and.presentation.home
+
+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.height
+import androidx.compose.foundation.lazy.LazyColumn
+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.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+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.RecommendList
+import org.sopt.and.presentation.home.components.Top20List
+
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun HomeScreen(
+ onContentTypeSelected: (ContentType) -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: HomeViewModel = hiltViewModel()
+) {
+ 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)
+ ) {
+
+ item {
+ HomeBannerPager(homeState.mainContents)
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+
+ item {
+ RecommendList(
+ title = stringResource(R.string.home_picks_of_editor_title),
+ items = homeState.commonContents
+ )
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+
+ item {
+ Top20List(homeState.rankingContents)
+ }
+ }
+}
+
+
+@Composable
+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/HomeUiState.kt b/app/src/main/java/org/sopt/and/presentation/home/HomeUiState.kt
new file mode 100644
index 00000000..7a3b3a99
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/home/HomeUiState.kt
@@ -0,0 +1,44 @@
+package org.sopt.and.presentation.home
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import org.sopt.and.R
+
+data class HomeUiState(
+ @StringRes
+ val genres: List = listOf(
+ R.string.genre_new_classic,
+ R.string.genre_drama,
+ R.string.genre_entertainment,
+ R.string.genre_movie,
+ R.string.genre_animation,
+ R.string.genre_foreign_country_series
+ ),
+ @DrawableRes
+ val banners: List = listOf(
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ ),
+ @DrawableRes
+ val recommends: List = listOf(
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ ),
+ @DrawableRes
+ val rankers: List = listOf(
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ R.drawable.bee,
+ )
+)
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
new file mode 100644
index 00000000..05f322a7
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/home/HomeViewModel.kt
@@ -0,0 +1,35 @@
+package org.sopt.and.presentation.home
+
+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
new file mode 100644
index 00000000..35a4700e
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/home/components/HomeBannerPager.kt
@@ -0,0 +1,133 @@
+package org.sopt.and.presentation.home.components
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import 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) {
+ val pagerState = rememberPagerState(pageCount = { banners.size })
+
+ HorizontalPager(
+ state = pagerState,
+ contentPadding = PaddingValues(horizontal = 10.dp),
+ pageSpacing = 10.dp
+ ) { page ->
+ HomeBannerPage(
+ index = page,
+ banners = banners
+ )
+ }
+}
+
+@Composable
+fun HomeBannerPage(
+ index: Int,
+ @DrawableRes banners: List
+) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .clip(shape = RoundedCornerShape(16.dp))
+ .border(1.dp, Grey200, shape = RoundedCornerShape(16.dp))
+ ) {
+ Image(
+ painter = painterResource(banners[index]),
+ contentDescription = "",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .fillMaxSize()
+ )
+
+ HomeBannerIndicator(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(6.dp),
+ index = index,
+ totalPage = banners.size
+ )
+ }
+}
+
+@Composable
+fun HomeBannerIndicator(
+ modifier: Modifier,
+ index: Int,
+ totalPage: Int
+) {
+ Box(
+ modifier = modifier
+ .clip(RoundedCornerShape(50))
+ .background(Color.Black)
+ .padding(horizontal = 8.dp, vertical = 4.dp)
+ ) {
+ Text(
+ buildAnnotatedString {
+ withStyle(
+ style = SpanStyle(
+ color = White100,
+ fontSize = 11.sp
+ )
+ ) {
+ append(
+ stringResource(
+ R.string.home_banner_indicator_front, index + 1
+ )
+ )
+ }
+ withStyle(
+ style = SpanStyle(
+ color = Grey200,
+ fontSize = 11.sp
+ )
+ ) {
+ append(
+ stringResource(
+ R.string.home_banner_indicator_back,
+ totalPage
+ )
+ )
+ }
+ }
+ )
+ }
+}
+
+@Preview
+@Composable
+fun HomeBannerPagerPreview() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ HomeBannerPager(listOf())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/home/components/HomeBottomCoupon.kt b/app/src/main/java/org/sopt/and/presentation/home/components/HomeBottomCoupon.kt
new file mode 100644
index 00000000..1ea9aa4e
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/home/components/HomeBottomCoupon.kt
@@ -0,0 +1,36 @@
+package org.sopt.and.presentation.home.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import org.sopt.and.R
+
+@Composable
+fun HomeBottomCoupon(modifier: Modifier = Modifier) {
+ Text(
+ text = stringResource(R.string.my_first_payment_text),
+ textAlign = TextAlign.Center,
+ color = Color.White,
+ modifier = modifier
+ .fillMaxWidth()
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ Color(0xFF0281ED),
+ Color(0xFF02B9B5)
+ )
+ ),
+ shape = RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp)
+ )
+ .padding(vertical = 15.dp)
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/home/components/HomeTopBar.kt b/app/src/main/java/org/sopt/and/presentation/home/components/HomeTopBar.kt
new file mode 100644
index 00000000..f5f5f60a
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/home/components/HomeTopBar.kt
@@ -0,0 +1,93 @@
+package org.sopt.and.presentation.home.components
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+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.layout.width
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+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.compose.ui.unit.sp
+import org.sopt.and.R
+import org.sopt.and.ui.theme.Grey200
+import org.sopt.and.ui.theme.White100
+
+@Composable
+fun HomeTopBar(
+ @StringRes genres: List
+) {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(R.drawable.wavve_logo),
+ contentDescription = "",
+ modifier = Modifier
+ .height(60.dp)
+ .width(100.dp)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Icon(
+ painter = painterResource(R.drawable.ic_cast_24),
+ contentDescription = "",
+ modifier = Modifier.size(30.dp),
+ tint = White100
+ )
+
+ Spacer(modifier = Modifier.size(16.dp))
+
+ Icon(
+ painter = painterResource(R.drawable.ic_live_tv_24),
+ contentDescription = "",
+ modifier = Modifier.size(30.dp),
+ tint = White100
+ )
+ }
+
+ LazyRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 10.dp),
+ horizontalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ items(
+ items = genres,
+ key = { it }
+ ) { genre ->
+ Text(
+ text = stringResource(genre),
+ color = Grey200,
+ fontSize = 14.sp
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun HomeTopBarPreview() {
+ HomeTopBar(genres = listOf(R.string.home_top20_title))
+}
+
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
new file mode 100644
index 00000000..68b44e32
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/home/components/RecommendList.kt
@@ -0,0 +1,76 @@
+package org.sopt.and.presentation.home.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+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.unit.dp
+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
+) {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = title,
+ color = White100,
+ fontWeight = FontWeight.W900
+ )
+
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
+ contentDescription = "",
+ tint = Grey200
+ )
+ }
+
+ Spacer(Modifier.height(10.dp))
+
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ items(
+ items = items,
+ key = { it }
+ ) { item ->
+ Image(
+ painter = painterResource(item),
+ contentDescription = "",
+ modifier = Modifier
+ .width(110.dp)
+ .clip(shape = RoundedCornerShape(15.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
new file mode 100644
index 00000000..63903473
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/home/components/Top20List.kt
@@ -0,0 +1,89 @@
+package org.sopt.and.presentation.home.components
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight.Companion.W900
+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: HomeCommonContent) {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.home_top20_title),
+ color = White100,
+ fontWeight = W900
+ )
+
+ Spacer(Modifier.height(10.dp))
+
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ items(
+ count = rankers.size,
+ key = { it }
+ ) { index ->
+ RankedItem(index, rankers)
+ }
+ }
+ }
+}
+
+@Composable
+fun RankedItem(
+ index: Int,
+ @DrawableRes rankers: List
+) {
+ Box(
+ Modifier.height(240.dp)
+ ) {
+ Image(
+ painter = painterResource(rankers[index]),
+ contentDescription = "",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .height(220.dp)
+ .clip(shape = RoundedCornerShape(15.dp))
+ .align(Alignment.TopStart)
+ )
+
+ Text(
+ text = stringResource(R.string.home_rank_of_item, index + 1),
+ fontSize = 50.sp,
+ fontWeight = W900,
+ color = White100,
+ modifier = Modifier
+ .align(Alignment.BottomStart)
+ .padding(start = 16.dp)
+ )
+ }
+}
+
+@Preview
+@Composable
+fun Top20ListPreview() {
+ Top20List(listOf())
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainActivity.kt b/app/src/main/java/org/sopt/and/presentation/main/MainActivity.kt
new file mode 100644
index 00000000..b7c918ce
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/main/MainActivity.kt
@@ -0,0 +1,18 @@
+package org.sopt.and.presentation.main
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ MainScreen()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainBottomNavigationBar.kt b/app/src/main/java/org/sopt/and/presentation/main/MainBottomNavigationBar.kt
new file mode 100644
index 00000000..a41e8485
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/main/MainBottomNavigationBar.kt
@@ -0,0 +1,83 @@
+package org.sopt.and.presentation.main
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.BottomAppBar
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationBarItemColors
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import org.sopt.and.R
+import org.sopt.and.core.navigation.Screen
+import org.sopt.and.ui.theme.WavveBg
+import org.sopt.and.ui.theme.WavveDisabled
+
+
+@Composable
+fun MainBottomNavigationBar(
+ navController: NavController,
+ currentRoute: String?,
+ colors: NavigationBarItemColors
+) {
+ BottomAppBar(
+ containerColor = WavveBg,
+ contentColor = WavveDisabled
+ ) {
+ MainBottomTabs.items.forEach { tab ->
+ BottomNavigationItem(
+ navController = navController,
+ tab = tab,
+ currentRoute = currentRoute,
+ colors = colors
+ )
+ }
+ }
+}
+
+@Composable
+fun RowScope.BottomNavigationItem(
+ navController: NavController,
+ tab: MainBottomTab,
+ currentRoute: String?,
+ colors: NavigationBarItemColors
+) {
+ NavigationBarItem(
+ icon = {
+ if (tab.isProfileImage) {
+ Image(
+ painter = painterResource(tab.iconResId),
+ contentDescription = stringResource(R.string.my_page_image_description_profile),
+ modifier = Modifier.size(32.dp)
+ )
+ } else {
+ Icon(
+ ImageVector.vectorResource(tab.iconResId),
+ contentDescription = stringResource(tab.labelResId),
+ modifier = Modifier.size(32.dp)
+ )
+ }
+ },
+ label = { Text(stringResource(tab.labelResId)) },
+ selected = currentRoute == tab.screen.javaClass.canonicalName,
+ onClick = { navigateToScreen(navController, tab.screen) },
+ colors = colors
+ )
+}
+
+private fun navigateToScreen(navController: NavController, screen: Screen) {
+ screen.javaClass.canonicalName?.let {
+ navController.navigate(it) {
+ screen.javaClass.canonicalName?.let { it1 -> popUpTo(it1) { inclusive = false } }
+ launchSingleTop = true
+ }
+ }
+}
diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainBottomTab.kt b/app/src/main/java/org/sopt/and/presentation/main/MainBottomTab.kt
new file mode 100644
index 00000000..be41d87f
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/main/MainBottomTab.kt
@@ -0,0 +1,34 @@
+package org.sopt.and.presentation.main
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import org.sopt.and.R
+import org.sopt.and.core.navigation.Screen
+
+data class MainBottomTab(
+ val screen: Screen,
+ @DrawableRes val iconResId: Int,
+ @StringRes val labelResId: Int,
+ val isProfileImage: Boolean = false
+)
+
+object MainBottomTabs {
+ val Home = MainBottomTab(
+ screen = Screen.Home,
+ iconResId = R.drawable.ic_home,
+ labelResId = R.string.title_home
+ )
+ val Search = MainBottomTab(
+ screen = Screen.Search,
+ iconResId = R.drawable.ic_search,
+ labelResId = R.string.title_search
+ )
+ val MyPage = MainBottomTab(
+ screen = Screen.My,
+ iconResId = R.drawable.profile_default,
+ labelResId = R.string.title_my,
+ isProfileImage = true
+ )
+
+ val items = listOf(Home, Search, MyPage)
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainContract.kt b/app/src/main/java/org/sopt/and/presentation/main/MainContract.kt
new file mode 100644
index 00000000..0ca4f193
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/main/MainContract.kt
@@ -0,0 +1,18 @@
+package org.sopt.and.presentation.main
+
+import org.sopt.and.presentation.util.UiEffect
+import org.sopt.and.presentation.util.UiEvent
+import org.sopt.and.presentation.util.UiState
+
+class MainContract {
+ data class MainUiState(
+ val userToken: String? = null,
+ val isLoading: Boolean = false
+ ) : UiState
+
+ sealed class MainUiEvent : UiEvent {
+ data object LoadUserToken : MainUiEvent()
+ }
+
+ sealed class MainUiEffect : UiEffect
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainNavHost.kt b/app/src/main/java/org/sopt/and/presentation/main/MainNavHost.kt
new file mode 100644
index 00000000..69392a2c
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/main/MainNavHost.kt
@@ -0,0 +1,78 @@
+package org.sopt.and.presentation.main
+
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.foundation.background
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import org.sopt.and.core.navigation.Screen
+import org.sopt.and.presentation.home.HomeScreen
+import org.sopt.and.presentation.myinfo.MyInfo
+import org.sopt.and.presentation.search.SearchScreen
+import org.sopt.and.presentation.signin.SignInScreen
+import org.sopt.and.presentation.signup.SignUpScreen
+import org.sopt.and.ui.theme.WavveBg
+
+@Composable
+fun MainNavHost(
+ navController: NavHostController,
+ startDestination: Screen
+) {
+ NavHost(
+ navController = navController,
+ startDestination = startDestination,
+ enterTransition = { EnterTransition.None },
+ exitTransition = { ExitTransition.None },
+ popEnterTransition = { EnterTransition.None },
+ popExitTransition = { ExitTransition.None }
+ ) {
+ composable {
+ SignInScreen(
+ navigateToMy = {
+ navController.navigate(Screen.My)
+ },
+ navigateToSignUp = {
+ navController.navigate(Screen.SignUp)
+ },
+ navigateUp = {
+ navController.navigateUp()
+ }
+ )
+ }
+ composable {
+ SignUpScreen(
+ navigateToSignIn = {
+ navController.navigate(Screen.SignIn) {
+ popUpTo { inclusive = true }
+ launchSingleTop = true
+ }
+ },
+ navigateUp = {
+ navController.navigateUp()
+ }
+ )
+ }
+ composable {
+ MyInfo(
+ navigateToSignIn = {
+ navController.navigate(Screen.SignIn) {
+ popUpTo(0) { inclusive = true }
+ launchSingleTop = true
+ }
+ }
+ )
+ }
+ composable {
+ HomeScreen(
+ onContentTypeSelected = { /* TODO: Screen 변경 가능 */ },
+ modifier = Modifier.background(WavveBg)
+ )
+ }
+ composable {
+ SearchScreen(modifier = Modifier.background(WavveBg))
+ }
+ }
+}
diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainScreen.kt b/app/src/main/java/org/sopt/and/presentation/main/MainScreen.kt
new file mode 100644
index 00000000..3eed222a
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/main/MainScreen.kt
@@ -0,0 +1,81 @@
+package org.sopt.and.presentation.main
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.NavigationBarItemDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.compose.rememberNavController
+import org.sopt.and.core.navigation.Screen
+import org.sopt.and.core.utils.SnackBarUtils
+import org.sopt.and.ui.theme.BottomNavigationItemUnselected
+import org.sopt.and.ui.theme.White
+
+@Composable
+fun MainScreen(
+ viewModel: MainScreenViewModel = hiltViewModel()
+) {
+ val mainState by viewModel.uiState.collectAsStateWithLifecycle()
+ val navController = rememberNavController()
+ val colors = NavigationBarItemDefaults.colors(
+ selectedIconColor = White,
+ unselectedIconColor = BottomNavigationItemUnselected,
+ selectedTextColor = White,
+ unselectedTextColor = BottomNavigationItemUnselected,
+ indicatorColor = Color.Transparent
+ )
+
+ LaunchedEffect(Unit) {
+ viewModel.sendEvent(MainContract.MainUiEvent.LoadUserToken)
+ }
+ val startDestination = if (mainState.userToken.isNullOrBlank()) Screen.SignIn else Screen.My
+
+ var currentRoute by remember { mutableStateOf(null) }
+ LaunchedEffect(navController) {
+ navController.currentBackStackEntryFlow.collect { backStackEntry ->
+ currentRoute = backStackEntry.destination.route
+ }
+ }
+
+ val bottomBarScreens = listOf(
+ Screen.Home.javaClass.canonicalName,
+ Screen.Search.javaClass.canonicalName,
+ Screen.My.javaClass.canonicalName
+ )
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ LaunchedEffect(Unit) {
+ SnackBarUtils.init(snackbarHostState)
+ }
+
+ Scaffold(
+ snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
+ bottomBar = {
+ if (currentRoute in bottomBarScreens) {
+ MainBottomNavigationBar(
+ navController = navController,
+ currentRoute = currentRoute,
+ colors = colors
+ )
+ }
+ }
+ ) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding)) {
+ MainNavHost(
+ navController = navController,
+ startDestination = startDestination
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainViewModel.kt b/app/src/main/java/org/sopt/and/presentation/main/MainViewModel.kt
new file mode 100644
index 00000000..96b54dcd
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/main/MainViewModel.kt
@@ -0,0 +1,39 @@
+package org.sopt.and.presentation.main
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import org.sopt.and.core.utils.PreferenceUtil
+import org.sopt.and.presentation.util.BaseViewModel
+import org.sopt.and.presentation.main.MainContract.MainUiEffect
+import org.sopt.and.presentation.main.MainContract.MainUiEvent
+import org.sopt.and.presentation.main.MainContract.MainUiState
+import javax.inject.Inject
+
+@HiltViewModel
+class MainScreenViewModel @Inject constructor(
+ private val preferenceUtil: PreferenceUtil
+) : BaseViewModel(MainUiState()) {
+ override fun reduceState(event: MainUiEvent) {
+ when (event) {
+ MainUiEvent.LoadUserToken -> loadUserToken()
+ }
+ }
+
+ private fun loadUserToken() {
+ updateState(
+ currentState.copy(
+ isLoading = true
+ )
+ )
+ viewModelScope.launch {
+ val token = preferenceUtil.getUserToken()
+ updateState(
+ currentState.copy(
+ isLoading = false,
+ userToken = token
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoContract.kt b/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoContract.kt
new file mode 100644
index 00000000..a414d401
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoContract.kt
@@ -0,0 +1,25 @@
+package org.sopt.and.presentation.myinfo
+
+import org.sopt.and.presentation.util.UiEffect
+import org.sopt.and.presentation.util.UiEvent
+import org.sopt.and.presentation.util.UiState
+
+class MyInfoContract {
+ data class MyPageUiState(
+ val hobby: String = "",
+ val isLoading: Boolean = false,
+ val isLoggedOut: Boolean = false,
+ val errorMessage: String? = null,
+ val tokenInvalid: Boolean = false
+ ) : UiState
+
+ sealed class MyPageUiEvent : UiEvent {
+ data object Logout : MyPageUiEvent()
+ data object LoadHobby : MyPageUiEvent()
+ }
+
+ sealed class MyPageUiEffect : UiEffect {
+ data class ShowErrorSnackBar(val message: String) : MyPageUiEffect()
+ data object NavigateToSignIn : MyPageUiEffect()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoScreen.kt b/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoScreen.kt
new file mode 100644
index 00000000..21ca7343
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoScreen.kt
@@ -0,0 +1,92 @@
+package org.sopt.and.presentation.myinfo
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+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 org.sopt.and.R
+import org.sopt.and.core.utils.SnackBarUtils
+import org.sopt.and.presentation.components.EmptyInfoBox
+import org.sopt.and.presentation.myinfo.components.MyInfoPaymentInducementBox
+import org.sopt.and.presentation.myinfo.components.MyInfoProfile
+import org.sopt.and.presentation.mypage.viewmodel.MyViewModel
+import org.sopt.and.ui.theme.ANDANDROIDTheme
+import org.sopt.and.ui.theme.Black100
+
+@Composable
+fun MyInfo(
+ navigateToSignIn : () -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: MyViewModel = hiltViewModel()
+){
+ val context = LocalContext.current
+
+ val myPageState by viewModel.uiState.collectAsStateWithLifecycle()
+ LaunchedEffect(Unit) {
+ viewModel.sendEvent(MyInfoContract.MyPageUiEvent.LoadHobby)
+ viewModel.effect.collectLatest { effect ->
+ when (effect) {
+ is MyInfoContract.MyPageUiEffect.ShowErrorSnackBar -> {
+ SnackBarUtils.showSnackBar(
+ message = effect.message,
+ actionLabel = context.getString(R.string.sign_in_snackbar_action_close)
+ )
+ }
+
+ MyInfoContract.MyPageUiEffect.NavigateToSignIn -> {
+ navigateToSignIn()
+ }
+ }
+ }
+ }
+
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(color = Black100)
+ .padding(paddingValues)
+ ) {
+ MyInfoProfile(
+ myHobby = myHobby,
+ getMyHobby = getMyHobby,
+ modifier = Modifier.weight(0.16f)
+ )
+
+ MyInfoPaymentInducementBox(
+ paymentInducementText = stringResource(id = R.string.my_first_payment_text),
+ modifier = Modifier.weight(0.12f)
+ )
+
+ Spacer(modifier = Modifier.height(2.dp))
+
+ MyInfoPaymentInducementBox(
+ paymentInducementText = stringResource(id = R.string.my_no_ticket_text),
+ modifier = Modifier.weight(0.12f)
+ )
+
+ EmptyInfoBox(
+ title = stringResource(R.string.my_viewing_history_box_title),
+ description = stringResource(R.string.my_viewing_history_box_empty_text),
+ modifier = Modifier.weight(0.3f)
+ )
+
+ EmptyInfoBox(
+ title = stringResource(R.string.my_program_of_interest_box_title),
+ description = stringResource(R.string.my_program_of_interest_empty_text),
+ modifier = Modifier.weight(0.3f)
+ )
+ }
+}
+
diff --git a/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoUiState.kt b/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoUiState.kt
new file mode 100644
index 00000000..f352a6ff
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoUiState.kt
@@ -0,0 +1,5 @@
+package org.sopt.and.presentation.myinfo
+
+data class MyInfoUiState(
+ val myHobby: String = "오류"
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoViewModel.kt b/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoViewModel.kt
new file mode 100644
index 00000000..e897c2ab
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/myinfo/MyInfoViewModel.kt
@@ -0,0 +1,30 @@
+package org.sopt.and.presentation.myinfo
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import org.sopt.and.domain.model.MyHobbyEntity
+import org.sopt.and.domain.usecase.MyHobbyUseCase
+
+class MyInfoViewModel(
+ private val getMyHobbyUseCase: MyHobbyUseCase
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(MyInfoUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ private fun setMyHobby(myHobby: String) {
+ _uiState.value = _uiState.value.copy(myHobby = myHobby)
+ }
+
+ fun getMyHobby() {
+ viewModelScope.launch {
+ getMyHobbyUseCase().onSuccess { myHobbyEntity: MyHobbyEntity ->
+ setMyHobby(myHobbyEntity.myHobby)
+ }.onFailure { }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/myinfo/MyViewModel.kt b/app/src/main/java/org/sopt/and/presentation/myinfo/MyViewModel.kt
new file mode 100644
index 00000000..56c8b31c
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/myinfo/MyViewModel.kt
@@ -0,0 +1,89 @@
+package org.sopt.and.presentation.mypage.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import org.sopt.and.core.utils.PreferenceUtil
+import org.sopt.and.data.common.ErrorTypeWithMessage
+import org.sopt.and.domain.entity.BaseResult
+import org.sopt.and.domain.usecase.MyHobbyUseCase
+import org.sopt.and.presentation.myinfo.MyInfoContract
+import org.sopt.and.presentation.util.BaseViewModel
+
+import javax.inject.Inject
+
+@HiltViewModel
+class MyViewModel @Inject constructor(
+ private val getMyHobbyUseCase: MyHobbyUseCase,
+ private val preferenceUtil: PreferenceUtil
+) : BaseViewModel(
+ MyInfoContract.MyPageUiState()
+) {
+ override fun reduceState(event: MyInfoContract.MyPageUiEvent) {
+ when (event) {
+ is MyInfoContract.MyPageUiEvent.LoadHobby -> {
+ loadHobby()
+ }
+
+ MyInfoContract.MyPageUiEvent.Logout -> {
+ logout()
+ }
+ }
+ }
+
+ private fun loadHobby() {
+ val token = preferenceUtil.getUserToken()
+ Log.d("my**", token.toString())
+ if (token.isNullOrEmpty()) {
+ updateState(
+ currentState.copy(
+ tokenInvalid = true
+ )
+ )
+ postEffect(MyInfoContract.MyPageUiEffect.NavigateToSignIn)
+ return
+ }
+ updateState(currentState.copy(isLoading = true))
+ viewModelScope.launch {
+ when (val result = getMyHobbyUseCase(token)) {
+ is BaseResult.Success -> {
+ updateState(
+ currentState.copy(
+ hobby = result.data.hobby,
+ isLoading = false,
+ errorMessage = null,
+ tokenInvalid = false
+ )
+ )
+ }
+
+ is BaseResult.Error -> {
+ updateState(
+ currentState.copy(
+ isLoading = false,
+ errorMessage = result.message
+ )
+ )
+ if (result.errorCode == ErrorTypeWithMessage.INVALID_TOKEN) {
+ preferenceUtil.clearUserToken()
+ updateState(
+ currentState.copy(
+ tokenInvalid = true
+ )
+ )
+ postEffect(MyInfoContract.MyPageUiEffect.NavigateToSignIn)
+ } else {
+ postEffect(MyInfoContract.MyPageUiEffect.ShowErrorSnackBar(result.message))
+ }
+ }
+ }
+ }
+ }
+
+ private fun logout() {
+ preferenceUtil.clearUserToken()
+ updateState(currentState.copy(isLoggedOut = true))
+ postEffect(MyInfoContract.MyPageUiEffect.NavigateToSignIn)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/myinfo/components/MyInfoPaymenIntroducementBox.kt b/app/src/main/java/org/sopt/and/presentation/myinfo/components/MyInfoPaymenIntroducementBox.kt
new file mode 100644
index 00000000..00ab126e
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/myinfo/components/MyInfoPaymenIntroducementBox.kt
@@ -0,0 +1,43 @@
+package org.sopt.and.presentation.myinfo.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import org.sopt.and.R
+import org.sopt.and.ui.theme.Grey100
+import org.sopt.and.ui.theme.Grey200
+import org.sopt.and.ui.theme.White100
+
+
+@Composable
+fun MyInfoPaymentInducementBox(
+ paymentInducementText: String,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = Grey100)
+ .padding(16.dp)
+ ) {
+ Text(
+ text = paymentInducementText,
+ color = Grey200
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = stringResource(id = R.string.my_to_payment_button),
+ color = White100
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/myinfo/components/MyInfoProfile.kt b/app/src/main/java/org/sopt/and/presentation/myinfo/components/MyInfoProfile.kt
new file mode 100644
index 00000000..9eb03038
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/myinfo/components/MyInfoProfile.kt
@@ -0,0 +1,72 @@
+package org.sopt.and.presentation.myinfo.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.outlined.Notifications
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import org.sopt.and.R
+import org.sopt.and.ui.theme.Grey100
+import org.sopt.and.ui.theme.White100
+
+@Composable
+fun MyInfoProfile(
+ myHobby: String,
+ getMyHobby: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ getMyHobby()
+
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = Grey100)
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.AccountCircle,
+ contentDescription = stringResource(id = R.string.my_account_icon_description),
+ modifier = Modifier.size(80.dp),
+ tint = White100
+ )
+
+ Spacer(modifier = Modifier.width(5.dp))
+
+ Text(
+ text = myHobby.ifEmpty { "Loading..." },
+ color = White100
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Icon(
+ imageVector = Icons.Outlined.Notifications,
+ contentDescription = stringResource(id = R.string.my_notification_icon_description),
+ modifier = Modifier.size(30.dp),
+ tint = White100
+ )
+
+ Spacer(modifier = Modifier.width(24.dp))
+
+ Icon(
+ imageVector = Icons.Outlined.Settings,
+ contentDescription = stringResource(id = R.string.my_setting_icon_description),
+ modifier = Modifier.size(30.dp),
+ tint = White100
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/navigation/BottomNavigation.kt b/app/src/main/java/org/sopt/and/presentation/navigation/BottomNavigation.kt
new file mode 100644
index 00000000..af53b4b2
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/navigation/BottomNavigation.kt
@@ -0,0 +1,111 @@
+package org.sopt.and.presentation.navigation
+
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationBarItemColors
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navOptions
+import org.sopt.and.R
+import org.sopt.and.ui.theme.Grey100
+import org.sopt.and.ui.theme.Grey200
+
+@Composable
+fun BottomNavigation(
+ items: List,
+ navController: NavController,
+ setNavigationSelectedScreenIndex: (Int) -> Unit,
+ navigationSelectedScreenIndex: Int
+) {
+ NavigationBar(
+ modifier = Modifier.height(60.dp),
+ containerColor = Color.Black
+ ) {
+ items.forEachIndexed { index, bottomNavigationItem ->
+ NavigationBarItem(
+ selected = index == navigationSelectedScreenIndex,
+ onClick = {
+ setNavigationSelectedScreenIndex(index)
+ navController.navigate(
+ bottomNavigationItem.route,
+ navOptions = navOptions {
+ launchSingleTop
+ }
+ )
+ },
+ icon = {
+ Icon(
+ imageVector = bottomNavigationItem.icon,
+ contentDescription = ""
+ )
+ },
+ label = {
+ Text(
+ text = stringResource(bottomNavigationItem.label),
+ style = TextStyle(
+ fontSize = 12.sp
+ )
+ )
+ },
+ colors = NavigationBarItemColors(
+ selectedIconColor = Color.White,
+ selectedTextColor = Color.White,
+ selectedIndicatorColor = Color.Transparent,
+ unselectedIconColor = Grey200,
+ unselectedTextColor = Grey200,
+ disabledIconColor = Grey100,
+ disabledTextColor = Grey100
+ )
+ )
+ }
+
+ }
+}
+
+@Preview
+@Composable
+fun WavveBottomNavigationPreview() {
+ val index = remember { mutableIntStateOf(0) }
+ BottomNavigation(
+ listOf(
+ BottomNavigationItem(
+ label = R.string.bottom_navigation_home_label,
+ icon = Icons.Default.Home,
+ route = Routes.Home,
+ index = 0
+ ),
+ BottomNavigationItem(
+ label = R.string.bottom_navigation_search_label,
+ icon = Icons.Default.Search,
+ route = Routes.Search,
+ index = 1
+ ),
+ BottomNavigationItem(
+ label = R.string.bottom_navigation_my_info_label,
+ icon = Icons.Default.AccountCircle,
+ route = Routes.MyInfo,
+ index = 2
+ )
+ ),
+ navController = rememberNavController(),
+ setNavigationSelectedScreenIndex = { index.intValue = it },
+ navigationSelectedScreenIndex = index.intValue,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/navigation/BottomNavigationItem.kt b/app/src/main/java/org/sopt/and/presentation/navigation/BottomNavigationItem.kt
new file mode 100644
index 00000000..87eb4a3f
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/navigation/BottomNavigationItem.kt
@@ -0,0 +1,10 @@
+package org.sopt.and.presentation.navigation
+
+import androidx.compose.ui.graphics.vector.ImageVector
+
+data class BottomNavigationItem(
+ val label: Int,
+ val icon: ImageVector,
+ val route: Routes,
+ val index: Int
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/navigation/Navigation.kt b/app/src/main/java/org/sopt/and/presentation/navigation/Navigation.kt
new file mode 100644
index 00000000..53ef1dd8
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/navigation/Navigation.kt
@@ -0,0 +1,96 @@
+package org.sopt.and.presentation.navigation
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navOptions
+import org.sopt.and.presentation.home.HomeScreen
+import org.sopt.and.presentation.myinfo.MyInfoScreen
+import org.sopt.and.presentation.myinfo.MyInfoViewModel
+import org.sopt.and.presentation.search.SearchScreen
+import org.sopt.and.presentation.signin.SignInScreen
+import org.sopt.and.presentation.signup.SignUpScreen
+import org.sopt.and.presentation.viewmodelfactory.MyInfoViewModelFactory
+import org.sopt.and.ui.theme.WavveBg
+
+@Composable
+fun Navigation(
+) {
+ val navigationViewModel = viewModel()
+ val navigationUiState by navigationViewModel.uiState.collectAsStateWithLifecycle()
+ val navController = rememberNavController()
+
+ val myInfoViewModel: MyInfoViewModel = viewModel(
+ factory = MyInfoViewModelFactory()
+ )
+ val myInfoUiState by myInfoViewModel.uiState.collectAsStateWithLifecycle()
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ bottomBar = {
+ if (navigationUiState.isBottomNavigationVisible) {
+ BottomNavigation(
+ items = navigationUiState.BottomNavigationItems,
+ navController = navController,
+ setNavigationSelectedScreenIndex = navigationViewModel::setNavigationSelectedIndex,
+ navigationSelectedScreenIndex = navigationUiState.navigationSelectedIndex
+ )
+ }
+ }
+ ) { innerPadding ->
+ NavHost(
+ navController = navController,
+ startDestination = Routes.SignIn
+ ) {
+ composable {
+ SignInScreen(
+ navigateToSignUp = { navController.navigate(route = Routes.SignUp) },
+ navigateToMyInfo = {
+ navigationViewModel.changeBottomNavigationVisibility()
+ navController.navigate(Routes.MyInfo)
+ }
+ )
+ }
+
+ composable {
+ SignUpScreen(
+ navigateToSignIn = {
+ navController.navigate(
+ route = Routes.SignIn,
+ navOptions = navOptions {
+ popUpTo {
+ inclusive = true
+ }
+ }
+ )
+ }
+ )
+ }
+
+ composable {
+ MyInfoScreen(
+ paddingValues = innerPadding,
+ myHobby = myInfoUiState.myHobby,
+ getMyHobby = myInfoViewModel::getMyHobby
+ )
+ }
+
+ composable {
+ HomeScreen(
+ innerPadding = innerPadding
+ )
+ }
+
+ composable {
+ SearchScreen(Modifier.Companion.background(WavveBg))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/navigation/NavigationUiState.kt b/app/src/main/java/org/sopt/and/presentation/navigation/NavigationUiState.kt
new file mode 100644
index 00000000..0e3f14ad
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/navigation/NavigationUiState.kt
@@ -0,0 +1,33 @@
+package org.sopt.and.presentation.navigation
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Search
+import org.sopt.and.R
+import org.sopt.and.presentation.util.Utils
+
+data class NavigationUiState(
+ val isBottomNavigationVisible: Boolean = false,
+ val navigationSelectedIndex: Int = Utils.MYINFO_SCREEN_INDEX,
+ val BottomNavigationItems: List = listOf(
+ BottomNavigationItem(
+ label = R.string.bottom_navigation_home_label,
+ icon = Icons.Default.Home,
+ route = Routes.Home,
+ index = 0
+ ),
+ BottomNavigationItem(
+ label = R.string.bottom_navigation_search_label,
+ icon = Icons.Default.Search,
+ route = Routes.Search,
+ index = 1
+ ),
+ BottomNavigationItem(
+ label = R.string.bottom_navigation_my_info_label,
+ icon = Icons.Default.AccountCircle,
+ route = Routes.MyInfo,
+ index = 2
+ )
+ )
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/navigation/NavigationViewModel.kt b/app/src/main/java/org/sopt/and/presentation/navigation/NavigationViewModel.kt
new file mode 100644
index 00000000..c9db0cf8
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/navigation/NavigationViewModel.kt
@@ -0,0 +1,23 @@
+package org.sopt.and.presentation.navigation
+
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class NavigationViewModel : ViewModel() {
+ private val _uiState = MutableStateFlow(NavigationUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ fun changeBottomNavigationVisibility() {
+ _uiState.value = _uiState.value.copy(
+ isBottomNavigationVisible = !_uiState.value.isBottomNavigationVisible
+ )
+ }
+
+ fun setNavigationSelectedIndex(index: Int) {
+ _uiState.value = _uiState.value.copy(
+ navigationSelectedIndex = index
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/navigation/Routes.kt b/app/src/main/java/org/sopt/and/presentation/navigation/Routes.kt
new file mode 100644
index 00000000..540f8ffc
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/navigation/Routes.kt
@@ -0,0 +1,20 @@
+package org.sopt.and.presentation.navigation
+
+import kotlinx.serialization.Serializable
+
+sealed class Routes {
+ @Serializable
+ object MyInfo : Routes()
+
+ @Serializable
+ object SignIn : Routes()
+
+ @Serializable
+ object SignUp : Routes()
+
+ @Serializable
+ object Home : Routes()
+
+ @Serializable
+ object Search : Routes()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/navigation/SignNavigation.kt b/app/src/main/java/org/sopt/and/presentation/navigation/SignNavigation.kt
similarity index 80%
rename from app/src/main/java/org/sopt/and/navigation/SignNavigation.kt
rename to app/src/main/java/org/sopt/and/presentation/navigation/SignNavigation.kt
index aa545daf..4bff8d0b 100644
--- a/app/src/main/java/org/sopt/and/navigation/SignNavigation.kt
+++ b/app/src/main/java/org/sopt/and/presentation/navigation/SignNavigation.kt
@@ -1,4 +1,4 @@
-package org.sopt.and.navigation
+package org.sopt.and.presentation.navigation
sealed class SignNavigation(val route: String) {
diff --git a/app/src/main/java/org/sopt/and/search/SearchScreen.kt b/app/src/main/java/org/sopt/and/presentation/search/SearchScreen.kt
similarity index 89%
rename from app/src/main/java/org/sopt/and/search/SearchScreen.kt
rename to app/src/main/java/org/sopt/and/presentation/search/SearchScreen.kt
index 6521cdea..d1b78c55 100644
--- a/app/src/main/java/org/sopt/and/search/SearchScreen.kt
+++ b/app/src/main/java/org/sopt/and/presentation/search/SearchScreen.kt
@@ -1,4 +1,4 @@
-package org.sopt.and.search
+package org.sopt.and.presentation.search
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.sp
@Composable
-fun SearchScreen() {
+fun SearchScreen(modifier: Modifier) {
Box(
modifier = Modifier
.fillMaxSize()
diff --git a/app/src/main/java/org/sopt/and/presentation/signin/SignInContract.kt b/app/src/main/java/org/sopt/and/presentation/signin/SignInContract.kt
new file mode 100644
index 00000000..b996a5c6
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signin/SignInContract.kt
@@ -0,0 +1,29 @@
+package org.sopt.and.presentation.auth.signin
+
+import org.sopt.and.presentation.util.UiEffect
+import org.sopt.and.presentation.util.UiEvent
+import org.sopt.and.presentation.util.UiState
+
+class SignInContract {
+ data class SignInUiState(
+ val username: String = "",
+ val password: String = "",
+ val isLoading: Boolean = false,
+ val errorMessage: String? = null
+ ) : UiState
+
+ sealed class SignInUiEvent : UiEvent {
+ data class UpdateUserName(val username: String) : SignInUiEvent()
+ data class UpdatePassword(val password: String) : SignInUiEvent()
+ data object SignInFormSubmit : SignInUiEvent()
+ data object NavigateUp : SignInUiEvent()
+ }
+
+ sealed class SignInUiEffect : UiEffect {
+ data object ShowSuccessSnackBar : SignInUiEffect()
+ data class ShowErrorSnackBar(val message: String) : SignInUiEffect()
+ data object NavigateToSignUp : SignInUiEffect()
+ data object NavigateToMy : SignInUiEffect()
+ data object NavigateUp : SignInUiEffect()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signin/SignInScreen.kt b/app/src/main/java/org/sopt/and/presentation/signin/SignInScreen.kt
new file mode 100644
index 00000000..57925281
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signin/SignInScreen.kt
@@ -0,0 +1,132 @@
+package org.sopt.and.presentation.signin
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.sopt.and.R
+import org.sopt.and.core.utils.SnackBarUtils
+import org.sopt.and.presentation.auth.signin.SignInContract
+import org.sopt.and.presentation.auth.signin.component.SignInButton
+import org.sopt.and.presentation.auth.signin.viewmodel.SignInViewModel
+import org.sopt.and.presentation.components.SnSBox
+import org.sopt.and.presentation.signin.components.*
+import org.sopt.and.ui.theme.Black100
+
+
+@Composable
+fun SignInScreen(
+ navigateToMy: () -> Unit,
+ navigateToSignUp: () -> Unit,
+ navigateUp: () -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: SignInViewModel = hiltViewModel(),
+) {
+ val signInState by viewModel.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+
+ // Effect handling
+ LaunchedEffect(Unit) {
+ viewModel.effect.collect { effect ->
+ when (effect) {
+ is SignInContract.SignInUiEffect.ShowSuccessSnackBar -> {
+ CoroutineScope(Dispatchers.Main).launch {
+ SnackBarUtils.showSnackBar(
+ message = context.getString(R.string.sign_in_snackbar_login_success),
+ actionLabel = context.getString(R.string.sign_in_snackbar_action_close)
+ )
+ }
+ navigateToMy()
+ }
+
+ is SignInContract.SignInUiEffect.ShowErrorSnackBar -> {
+ CoroutineScope(Dispatchers.Main).launch {
+ SnackBarUtils.showSnackBar(
+ message = effect.message,
+ actionLabel = context.getString(R.string.sign_in_snackbar_action_close)
+ )
+ }
+ }
+
+ is SignInContract.SignInUiEffect.NavigateToSignUp -> navigateToSignUp()
+ is SignInContract.SignInUiEffect.NavigateToMy -> navigateToMy()
+ is SignInContract.SignInUiEffect.NavigateUp -> navigateUp()
+ }
+ }
+ }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ snackbarHost = { viewModel.sendEvent(SignInContract.SignInUiEvent.NavigateUp) }
+ ) { innerPadding ->
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(color = Black100)
+ .padding(innerPadding)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ SignInTopBar()
+
+ Spacer(modifier = Modifier.height(60.dp))
+
+ SignInUsernameField(
+ signInUsername = signInState.username,
+ onSignInUsernameChange = {
+ viewModel.sendEvent(
+ SignInContract.SignInUiEvent.UpdateUserName(
+ it
+ )
+ )
+ }
+ )
+
+ Spacer(modifier = Modifier.height(5.dp))
+
+ SignInPasswordField(
+ signInPassword = signInState.password,
+ onSignInPasswordChange = {
+ viewModel.sendEvent(
+ SignInContract.SignInUiEvent.UpdatePassword(
+ it
+ )
+ )
+ },
+ isSignInPasswordVisible = false,
+ )
+
+ Spacer(modifier = Modifier.height(30.dp))
+
+ SignInButton(
+ text = stringResource(R.string.sign_in_text_login),
+ onClick = { viewModel.signIn() },
+ modifier = Modifier
+ )
+
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ SignInToAdditionalFeatures(navigateToSignUp = navigateToSignUp)
+
+ Spacer(modifier = Modifier.size(40.dp))
+
+ SnSBox(stringResource(R.string.sign_in_link_with_another_service_title))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signin/SignInUiState.kt b/app/src/main/java/org/sopt/and/presentation/signin/SignInUiState.kt
new file mode 100644
index 00000000..5398a6e0
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signin/SignInUiState.kt
@@ -0,0 +1,14 @@
+package org.sopt.and.presentation.signin
+
+data class SignInUiState(
+ val signInUsername: String = "",
+ val signInPassword: String = "",
+ val isSignInPasswordVisible: Boolean = false
+)
+
+sealed class SignInResult {
+ object Initial : SignInResult()
+ object Success : SignInResult()
+ object FailurePasswordLength : SignInResult()
+ object FailureWrongPassword : SignInResult()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signin/SignInViewModel.kt b/app/src/main/java/org/sopt/and/presentation/signin/SignInViewModel.kt
new file mode 100644
index 00000000..452cdb7f
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signin/SignInViewModel.kt
@@ -0,0 +1,82 @@
+package org.sopt.and.presentation.auth.signin.viewmodel
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import org.sopt.and.core.utils.PreferenceUtil
+import org.sopt.and.domain.entity.BaseResult
+import org.sopt.and.domain.entity.UserData
+import org.sopt.and.domain.usecase.SignInUseCase
+import org.sopt.and.presentation.auth.signin.SignInContract.SignInUiEffect
+import org.sopt.and.presentation.auth.signin.SignInContract.SignInUiEvent
+import org.sopt.and.presentation.auth.signin.SignInContract.SignInUiState
+import org.sopt.and.presentation.util.BaseViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class SignInViewModel @Inject constructor(
+ private val loginUseCase: SignInUseCase,
+ private val preferenceUtil: PreferenceUtil
+) : BaseViewModel(SignInUiState()) {
+ override fun reduceState(event: SignInUiEvent) {
+ when (event) {
+ is SignInUiEvent.UpdateUserName -> {
+ updateState(
+ currentState.copy(
+ username = event.username
+ )
+ )
+ }
+
+ is SignInUiEvent.UpdatePassword -> {
+ updateState(
+ currentState.copy(
+ password = event.password
+ )
+ )
+ }
+
+ is SignInUiEvent.SignInFormSubmit -> signIn()
+
+ is SignInUiEvent.NavigateUp -> postEffect(SignInUiEffect.NavigateUp)
+ }
+ }
+
+ fun signIn() {
+ updateState(
+ currentState.copy(
+ isLoading = true
+ )
+ )
+ viewModelScope.launch {
+ when (
+ val result = loginUseCase(
+ with(currentState) {
+ UserData(username, password, "")
+ }
+ )
+ ) {
+ is BaseResult.Success -> {
+ updateState(
+ currentState.copy(
+ isLoading = false
+ )
+ )
+ preferenceUtil.saveUserToken(result.data.token)
+ postEffect(SignInUiEffect.ShowSuccessSnackBar)
+ postEffect(SignInUiEffect.NavigateToMy)
+ }
+
+ is BaseResult.Error -> {
+ updateState(
+ currentState.copy(
+ isLoading = false,
+ errorMessage = result.message
+ )
+ )
+ postEffect(SignInUiEffect.ShowErrorSnackBar(result.message))
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/sopt/and/presentation/signin/components/SignInButton.kt b/app/src/main/java/org/sopt/and/presentation/signin/components/SignInButton.kt
new file mode 100644
index 00000000..a4a40816
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signin/components/SignInButton.kt
@@ -0,0 +1,37 @@
+package org.sopt.and.presentation.auth.signin.component
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.sopt.and.ui.theme.Blue100
+import org.sopt.and.ui.theme.White100
+
+@Composable
+fun SignInButton(
+ text: String,
+ onClick : () -> Unit,
+ modifier: Modifier
+) {
+ Button(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Blue100
+ ),
+ onClick = onClick,
+ ) {
+ Text(
+ text = text,
+ color = White100,
+ fontSize = 16.sp,
+ modifier = modifier.padding(vertical = 8.dp)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signin/components/SignInPasswordField.kt b/app/src/main/java/org/sopt/and/presentation/signin/components/SignInPasswordField.kt
new file mode 100644
index 00000000..e1fe0e15
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signin/components/SignInPasswordField.kt
@@ -0,0 +1,21 @@
+package org.sopt.and.presentation.signin.components
+
+import androidx.compose.runtime.Composable
+import org.sopt.and.R
+import org.sopt.and.presentation.components.ShowOrHideToggle
+import org.sopt.and.presentation.components.SignInOrSignUpTextField
+import org.sopt.and.presentation.util.Utils.transformationPasswordVisual
+
+@Composable
+fun SignInPasswordField(
+ signInPassword: String,
+ onSignInPasswordChange: (String) -> Unit,
+ isSignInPasswordVisible: Boolean,
+) {
+ SignInOrSignUpTextField(
+ information = signInPassword,
+ onValueChange = onSignInPasswordChange,
+ placeholder = R.string.sign_in_password_placeholder,
+ visualTransformation = transformationPasswordVisual(isSignInPasswordVisible),
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signin/components/SignInToAdditionalFeatures.kt b/app/src/main/java/org/sopt/and/presentation/signin/components/SignInToAdditionalFeatures.kt
new file mode 100644
index 00000000..de38708c
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signin/components/SignInToAdditionalFeatures.kt
@@ -0,0 +1,65 @@
+package org.sopt.and.presentation.signin.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.sp
+import org.sopt.and.R
+import org.sopt.and.ui.theme.Grey200
+
+@Composable
+fun SignInToAdditionalFeatures(
+ navigateToSignUp: () -> Unit
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(0.6f),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(id = R.string.sign_in_to_find_id_button),
+ color = Grey200,
+ style = TextStyle(
+ fontSize = 11.sp
+ )
+ )
+
+ Text(
+ text = stringResource(id = R.string.seperator),
+ color = Grey200,
+ style = TextStyle(
+ fontSize = 11.sp
+ )
+ )
+
+ Text(
+ text = stringResource(id = R.string.sign_in_to_reset_password_button),
+ color = Grey200,
+ style = TextStyle(
+ fontSize = 11.sp
+ )
+ )
+
+ Text(
+ text = stringResource(id = R.string.seperator),
+ color = Grey200,
+ style = TextStyle(
+ fontSize = 11.sp
+ )
+ )
+
+ Text(
+ text = stringResource(id = R.string.sign_in_to_sign_up_button),
+ color = Grey200,
+ modifier = Modifier.clickable { navigateToSignUp() },
+ style = TextStyle(
+ fontSize = 11.sp
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signin/components/SignInTopBar.kt b/app/src/main/java/org/sopt/and/presentation/signin/components/SignInTopBar.kt
new file mode 100644
index 00000000..75009e68
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signin/components/SignInTopBar.kt
@@ -0,0 +1,49 @@
+package org.sopt.and.presentation.signin.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.sopt.and.R
+import org.sopt.and.ui.theme.White100
+
+@Composable
+fun SignInTopBar() {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.KeyboardArrowLeft,
+ contentDescription = stringResource(id = R.string.sign_in_to_back_screen_description),
+ modifier = Modifier
+ .size(48.dp),
+ tint = White100
+ )
+
+ Text(
+ text = stringResource(id = R.string.app_name),
+ color = White100,
+ style = TextStyle(
+ fontSize = 30.sp,
+ fontWeight = FontWeight(800)
+ )
+ )
+
+ Spacer(modifier = Modifier.size(48.dp))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signin/components/SignInUsernameField.kt b/app/src/main/java/org/sopt/and/presentation/signin/components/SignInUsernameField.kt
new file mode 100644
index 00000000..ae46321c
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signin/components/SignInUsernameField.kt
@@ -0,0 +1,17 @@
+package org.sopt.and.presentation.signin.components
+
+import androidx.compose.runtime.Composable
+import org.sopt.and.R
+import org.sopt.and.presentation.components.SignInOrSignUpTextField
+
+@Composable
+fun SignInUsernameField(
+ signInUsername: String,
+ onSignInUsernameChange: (String) -> Unit
+) {
+ SignInOrSignUpTextField(
+ information = signInUsername,
+ onValueChange = onSignInUsernameChange,
+ placeholder = R.string.sign_in_username_placeholder
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signup/SignUpContract.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpContract.kt
new file mode 100644
index 00000000..3148e84c
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpContract.kt
@@ -0,0 +1,42 @@
+package org.sopt.and.presentation.auth.signup
+
+import org.sopt.and.presentation.util.UiEffect
+import org.sopt.and.presentation.util.UiEvent
+import org.sopt.and.presentation.util.UiState
+
+class SignUpContract {
+ data class SignUpUiState(
+ val username: String = "",
+ val password: String = "",
+ val hobby: String = "",
+ val isUserNameValid: Boolean = false,
+ val isPasswordValid: Boolean = false,
+ val isHobbyValid: Boolean = false,
+ val isUserNameFieldFocused: Boolean = false,
+ val isPasswordFieldFocused: Boolean = false,
+ val isHobbyFieldFocused: Boolean = false,
+ val isValid: Boolean = false,
+ val isLoading: Boolean = false,
+ val errorMessage: String? = null
+ ) : UiState
+
+ sealed class SignUpUiEvent : UiEvent {
+ data class UpdateUserName(val username: String) : SignUpUiEvent()
+ data class UpdatePassword(val password: String) : SignUpUiEvent()
+ data class UpdateHobby(val hobby: String) : SignUpUiEvent()
+ data class UpdateFieldFocus(val field: Field, val isFocused: Boolean) : SignUpUiEvent()
+ data object SignUpFormSubmit : SignUpUiEvent()
+ data object Close : SignUpUiEvent()
+ }
+
+ sealed class SignUpUiEffect : UiEffect {
+ data object ShowSuccessToast : SignUpUiEffect()
+ data class ShowErrorToast(val message: String) : SignUpUiEffect()
+ data object NavigateToSignIn : SignUpUiEffect()
+ data object NavigateUp : SignUpUiEffect()
+ }
+
+ enum class Field {
+ UserName, Password, Hobby
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt
new file mode 100644
index 00000000..52e22dc2
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt
@@ -0,0 +1,145 @@
+package org.sopt.and.presentation.signup
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import org.sopt.and.R
+import org.sopt.and.core.utils.extension.noRippleClickable
+import org.sopt.and.presentation.auth.signup.SignUpContract
+import org.sopt.and.presentation.auth.signup.viewmodel.SignUpViewModel
+import org.sopt.and.presentation.components.SnSBox
+import org.sopt.and.presentation.signup.components.SignUpButton
+import org.sopt.and.presentation.signup.components.SignUpGreetingText
+import org.sopt.and.presentation.signup.components.SignUpHobbyField
+import org.sopt.and.presentation.signup.components.SignUpPasswordField
+import org.sopt.and.presentation.signup.components.SignUpTopBar
+import org.sopt.and.presentation.signup.components.SignUpUsernameField
+import org.sopt.and.presentation.util.Utils.showToast
+import org.sopt.and.ui.theme.Black100
+import org.sopt.and.ui.theme.Blue100
+import org.sopt.and.ui.theme.WavveDisabled
+import org.sopt.and.ui.theme.White100
+
+@Composable
+fun SignUpScreen(
+ navigateToSignIn: () -> Unit,
+ navigateUp: () -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: SignUpViewModel = hiltViewModel()
+) {
+
+ val signUpState by viewModel.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+
+ LaunchedEffect(Unit) {
+ viewModel.effect.collect { effect ->
+ when (effect) {
+ is SignUpContract.SignUpUiEffect.ShowSuccessToast -> {
+ context.showToast(context.getString(R.string.sign_up_toast_success))
+ navigateToSignIn()
+ }
+
+ is SignUpContract.SignUpUiEffect.ShowErrorToast -> {
+ context.showToast(effect.message)
+ }
+
+ is SignUpContract.SignUpUiEffect.NavigateToSignIn -> navigateToSignIn()
+ is SignUpContract.SignUpUiEffect.NavigateUp -> navigateUp()
+ }
+ }
+ }
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(color = Black100)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f)
+ .padding(16.dp)
+ ) {
+ SignUpTopBar()
+
+ Spacer(modifier = Modifier.height(30.dp))
+
+ SignUpGreetingText(fontSize = 24)
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ SignUpUsernameField(
+ signUpUsername = signUpState.username,
+ onSignUpUsernameChange = {
+ viewModel.sendEvent(
+ SignUpContract.SignUpUiEvent.UpdateUserName(
+ it
+ )
+ )
+ }
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+
+ SignUpPasswordField(
+ signUpPassword = signUpState.password,
+ onSignUpPasswordChange = {
+ viewModel.sendEvent(
+ SignUpContract.SignUpUiEvent.UpdatePassword(
+ it
+ )
+ )
+ }
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+
+ SignUpHobbyField(
+ signUpHobby = signUpState.hobby,
+ onSignUpHobbyChange = {
+ viewModel.sendEvent(
+ SignUpContract.SignUpUiEvent.UpdateHobby(it)
+ )
+ }
+ )
+
+ Spacer(modifier = Modifier.size(40.dp))
+
+ SnSBox(stringResource(R.string.sign_in_link_with_another_service_title))
+ }
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ color = if (signUpState.isValid) Blue100 else WavveDisabled
+ )
+ .wrapContentHeight()
+ .noRippleClickable { viewModel.sendEvent(SignUpContract.SignUpUiEvent.SignUpFormSubmit) }
+ .padding(vertical = 14.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.sign_up_text_wavve_sign_up),
+ color = White100
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signup/SignUpUiState.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpUiState.kt
new file mode 100644
index 00000000..c5b93fd9
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpUiState.kt
@@ -0,0 +1,15 @@
+package org.sopt.and.presentation.signup
+
+data class SignUpUiState(
+ val signUpUsername: String = "",
+ val signUpPassword: String = "",
+ val signUpHobby: String = "",
+ val isSignUpPasswordVisible: Boolean = false
+)
+
+sealed class SignUpResult {
+ object Initial : SignUpResult()
+ object Success : SignUpResult()
+ object FailureInformationLength : SignUpResult()
+ object FailureDuplicateUsername : SignUpResult()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signup/SignUpViewModel.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpViewModel.kt
new file mode 100644
index 00000000..1d4ea656
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpViewModel.kt
@@ -0,0 +1,134 @@
+package org.sopt.and.presentation.auth.signup.viewmodel
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import org.sopt.and.domain.entity.BaseResult
+import org.sopt.and.domain.entity.UserData
+import org.sopt.and.domain.usecase.SignUpUseCase
+import org.sopt.and.presentation.auth.signup.SignUpContract
+import org.sopt.and.presentation.auth.signup.SignUpContract.SignUpUiEffect
+import org.sopt.and.presentation.auth.signup.SignUpContract.SignUpUiState
+import org.sopt.and.presentation.auth.signup.SignUpContract.SignUpUiEvent
+import org.sopt.and.presentation.util.BaseViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class SignUpViewModel @Inject constructor(
+ private val registerUserUseCase: SignUpUseCase
+) : BaseViewModel(SignUpUiState()) {
+ override fun reduceState(event: SignUpUiEvent) {
+ when (event) {
+ is SignUpUiEvent.UpdateUserName -> {
+ val isValid = validateUserName(event.username)
+ updateState(
+ currentState.copy(
+ username = event.username,
+ isUserNameValid = isValid,
+ isValid = isValid &&
+ currentState.isPasswordValid &&
+ currentState.isHobbyValid
+ )
+ )
+ }
+
+ is SignUpUiEvent.UpdatePassword -> {
+ val isValid = validatePassword(event.password)
+ updateState(
+ currentState.copy(
+ password = event.password,
+ isPasswordValid = isValid,
+ isValid = isValid &&
+ currentState.username.isNotBlank() &&
+ currentState.hobby.isNotBlank()
+ )
+ )
+ }
+
+ is SignUpUiEvent.UpdateHobby -> {
+ val isValid = validateHobby(event.hobby)
+ updateState(
+ currentState.copy(
+ hobby = event.hobby,
+ isHobbyValid = isValid,
+ isValid = isValid &&
+ currentState.username.isNotBlank() &&
+ currentState.password.isNotBlank()
+ )
+ )
+ }
+
+ is SignUpUiEvent.UpdateFieldFocus -> {
+ when (event.field) {
+ SignUpContract.Field.UserName -> updateState(
+ currentState.copy(
+ isUserNameFieldFocused = event.isFocused
+ )
+ )
+
+ SignUpContract.Field.Password -> updateState(
+ currentState.copy(
+ isPasswordFieldFocused = event.isFocused
+ )
+ )
+
+ SignUpContract.Field.Hobby -> updateState(
+ currentState.copy(
+ isHobbyFieldFocused = event.isFocused
+ )
+ )
+ }
+ }
+
+ is SignUpUiEvent.SignUpFormSubmit -> signUp()
+
+ is SignUpUiEvent.Close -> postEffect(SignUpUiEffect.NavigateUp)
+ }
+ }
+
+ private fun signUp() {
+ updateState(
+ currentState.copy(
+ isLoading = true
+ )
+ )
+ viewModelScope.launch {
+ when (val result = registerUserUseCase(
+ UserData(
+ username = currentState.username,
+ password = currentState.password,
+ hobby = currentState.hobby
+ )
+ )) {
+ is BaseResult.Success -> {
+ updateState(
+ currentState.copy(
+ isLoading = false
+ )
+ )
+ postEffect(SignUpUiEffect.ShowSuccessToast)
+ postEffect(SignUpUiEffect.NavigateToSignIn)
+ }
+
+ is BaseResult.Error -> {
+ updateState(
+ currentState.copy(
+ isLoading = false,
+ errorMessage = result.message
+ )
+ )
+ postEffect(SignUpUiEffect.ShowErrorToast(result.message))
+ }
+ }
+ }
+ }
+
+ private fun validateUserName(username: String) =
+ username.isNotBlank() && username.length <= 8
+
+ private fun validatePassword(password: String) =
+ password.isNotBlank() && password.length <= 8
+
+ private fun validateHobby(hobby: String) =
+ hobby.isNotBlank() && hobby.length <= 8
+}
diff --git a/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpButton.kt b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpButton.kt
new file mode 100644
index 00000000..cbe6d095
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpButton.kt
@@ -0,0 +1,64 @@
+package org.sopt.and.presentation.signup.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import org.sopt.and.R
+import org.sopt.and.presentation.signup.SignUpViewModel
+import org.sopt.and.ui.theme.Grey200
+import org.sopt.and.ui.theme.White100
+
+@Composable
+fun SignUpButton(
+ signUpUsername: String,
+ signUpPassword: String,
+ signUpHobby: String,
+ onSignUpComplete: () -> Unit,
+ signUpViewModel: SignUpViewModel
+) {
+ val signUpResult by signUpViewModel.signUpResult.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+
+ LaunchedEffect(signUpResult) {
+ signUpViewModel.confirmSignUp(
+ context = context,
+ onSignUpComplete = onSignUpComplete
+ )
+ }
+
+ Button(
+ onClick = {
+ signUpViewModel.signUp(
+ signUpUsername = signUpUsername,
+ signUpPassword = signUpPassword,
+ signUpHobby = signUpHobby
+ )
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(60.dp),
+ shape = RoundedCornerShape(0.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Grey200,
+ contentColor = White100
+ )
+ ) {
+ Text(
+ text = stringResource(id = R.string.sign_up_button),
+ style = TextStyle(fontSize = 18.sp)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpEmailField.kt b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpEmailField.kt
new file mode 100644
index 00000000..7ee66d72
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpEmailField.kt
@@ -0,0 +1,32 @@
+package org.sopt.and.presentation.signup.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.sopt.and.R
+import org.sopt.and.presentation.components.CautionBox
+import org.sopt.and.presentation.components.SignInOrSignUpTextField
+
+@Composable
+fun SignUpUsernameField(
+ signUpUsername: String,
+ onSignUpUsernameChange: (String) -> Unit
+) {
+ Column {
+ SignInOrSignUpTextField(
+ information = signUpUsername,
+ onValueChange = onSignUpUsernameChange,
+ placeholder = R.string.sign_up_username_placeholder
+ )
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ CautionBox(
+ contentDescription = R.string.sign_up_username_description,
+ caution = R.string.sign_up_username_caution
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpGreeting.kt b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpGreeting.kt
new file mode 100644
index 00000000..90bb89f6
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpGreeting.kt
@@ -0,0 +1,71 @@
+package org.sopt.and.presentation.signup.components
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ParagraphStyle
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import org.sopt.and.R
+import org.sopt.and.presentation.util.Utils
+import org.sopt.and.ui.theme.Grey200
+import org.sopt.and.ui.theme.White100
+
+@Composable
+fun SignUpGreetingText(fontSize: Int) {
+ Text(
+ AnnotatedString(
+ text = stringResource(R.string.sign_up_welcome_text),
+ spanStyles = listOf(
+ AnnotatedString.Range(
+ item = SpanStyle(
+ color = White100,
+ fontSize = fontSize.sp
+ ),
+ start = Utils.GREETING_FIRST_LINE_FOCUS_START_INDEX,
+ end = Utils.GREETING_FIRST_LINE_FOCUS_END_INDEX
+ ),
+ AnnotatedString.Range(
+ item = SpanStyle(
+ color = Grey200,
+ fontSize = fontSize.sp
+ ),
+ start = Utils.GREETING_FIRST_LINE_FOCUS_END_INDEX,
+ end = Utils.GREETING_FIRST_LINE_END_INDEX
+ ),
+ AnnotatedString.Range(
+ item = SpanStyle(
+ color = White100,
+ fontSize = fontSize.sp
+ ),
+ start = Utils.GREETING_SECOND_LINE_FOCUS_START_INDEX,
+ end = Utils.GREETING_SECOND_LINE_FOCUS_END_INDEX
+ ),
+ AnnotatedString.Range(
+ item = SpanStyle(
+ color = Grey200,
+ fontSize = fontSize.sp
+ ),
+ start = Utils.GREETING_SECOND_LINE_FOCUS_END_INDEX,
+ end = Utils.GREETING_SECOND_LINE_END_INDEX
+ ),
+ ),
+ paragraphStyles = listOf(
+ AnnotatedString.Range(
+ item = ParagraphStyle(
+ lineHeight = 2.em,
+ lineHeightStyle = LineHeightStyle(
+ alignment = LineHeightStyle.Alignment.Top,
+ trim = LineHeightStyle.Trim.Both
+ )
+ ),
+ start = Utils.GREETING_FIRST_LINE_FOCUS_START_INDEX,
+ end = Utils.GREETING_SECOND_LINE_END_INDEX
+ )
+ )
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpHobbyField.kt b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpHobbyField.kt
new file mode 100644
index 00000000..0ca9551d
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpHobbyField.kt
@@ -0,0 +1,17 @@
+package org.sopt.and.presentation.signup.components
+
+import androidx.compose.runtime.Composable
+import org.sopt.and.R
+import org.sopt.and.presentation.components.SignInOrSignUpTextField
+
+@Composable
+fun SignUpHobbyField(
+ signUpHobby: String,
+ onSignUpHobbyChange: (String) -> Unit
+) {
+ SignInOrSignUpTextField(
+ information = signUpHobby,
+ onValueChange = onSignUpHobbyChange,
+ placeholder = R.string.sign_up_hobby_placeholder
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpPasswordField.kt b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpPasswordField.kt
new file mode 100644
index 00000000..56da9d63
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpPasswordField.kt
@@ -0,0 +1,34 @@
+package org.sopt.and.presentation.signup.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.sopt.and.R
+import org.sopt.and.presentation.components.CautionBox
+import org.sopt.and.presentation.components.ShowOrHideToggle
+import org.sopt.and.presentation.components.SignInOrSignUpTextField
+import org.sopt.and.presentation.util.Utils.transformationPasswordVisual
+
+@Composable
+fun SignUpPasswordField(
+ signUpPassword: String,
+ onSignUpPasswordChange: (String) -> Unit,
+) {
+ Column {
+ SignInOrSignUpTextField(
+ information = signUpPassword,
+ onValueChange = onSignUpPasswordChange,
+ placeholder = R.string.sign_up_password_placeholder,
+ )
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ CautionBox(
+ contentDescription = R.string.sign_up_password_description,
+ caution = R.string.sign_up_password_caution
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpTopBar.kt b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpTopBar.kt
new file mode 100644
index 00000000..50de578e
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/signup/components/SignUpTopBar.kt
@@ -0,0 +1,47 @@
+package org.sopt.and.presentation.signup.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.sopt.and.R
+import org.sopt.and.ui.theme.White100
+
+@Composable
+fun SignUpTopBar() {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Spacer(
+ modifier = Modifier.width(36.dp)
+ )
+
+ Text(
+ text = stringResource(id = R.string.sign_up_screen_title),
+ color = White100,
+ style = TextStyle(fontSize = 18.sp)
+ )
+
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = stringResource(id = R.string.sign_up_close_description),
+ tint = White100,
+ modifier = Modifier.size(36.dp)
+ )
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..2f6289d7
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/util/Utils.kt
@@ -0,0 +1,131 @@
+package org.sopt.and.presentation.util
+
+import android.annotation.SuppressLint
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import android.content.Context
+import android.widget.Toast
+import androidx.annotation.StringRes
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.sopt.and.R
+
+
+// Utility object for common functions
+object Utils {
+ const val MIN_PASSWORD_LENGTH = 8
+ const val MAX_PASSWORD_LENGTH = 20
+
+ const val MYINFO_SCREEN_INDEX = 2
+ const val SEARCH_SCREEN_INDEX = 1
+ const val HOME_SCREEN_INDEX = 0
+
+ const val GREETING_FIRST_LINE_FOCUS_START_INDEX = 0
+ const val GREETING_FIRST_LINE_FOCUS_END_INDEX = 9
+ const val GREETING_FIRST_LINE_END_INDEX = 12
+ const val GREETING_SECOND_LINE_FOCUS_START_INDEX = 13
+ const val GREETING_SECOND_LINE_FOCUS_END_INDEX = 24
+ const val GREETING_SECOND_LINE_END_INDEX = 29
+
+ val linkableSNS = listOf>(
+ Pair(R.drawable.kakao_talk_icon, R.string.link_kakao_icon_description),
+ Pair(R.drawable.t_world_icon, R.string.link_tworld_icon_description),
+ Pair(R.drawable.naver_icon, R.string.link_naver_icon_description),
+ Pair(R.drawable.facebook_icon, R.string.link_facebook_icon_description),
+ Pair(R.drawable.apple_icon, R.string.link_apple_icon_description),
+ )
+
+ // Visual transformation for password visibility
+ fun transformationPasswordVisual(isVisible: Boolean): VisualTransformation =
+ if (isVisible) VisualTransformation.None else PasswordVisualTransformation()
+
+ // Show a Toast message
+// 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(
+ scope: CoroutineScope,
+ snackbarHostState: SnackbarHostState,
+ @StringRes message: Int
+ ) = scope.launch {
+ snackbarHostState.showSnackbar(message = getString(message))
+ }
+}
+
+
+// UiState, UiEvent, and UiEffect interfaces
+interface UiState
+interface UiEvent
+interface UiEffect
+
+// Base ViewModel class with generic state, event, and effect
+abstract class BaseViewModel(
+ initialState: State
+) : ViewModel() {
+
+ // UI State management
+ private val _uiState: MutableStateFlow = MutableStateFlow(initialState)
+ val currentState: State
+ get() = _uiState.value
+
+ val uiState = _uiState.asStateFlow()
+
+ // Event handling
+ private val _event: MutableSharedFlow = MutableSharedFlow()
+ val event = _event.asSharedFlow()
+
+ // Effect handling
+ private val _effect: Channel = Channel()
+ val effect = _effect.receiveAsFlow()
+
+ init {
+ subscribeEvents()
+ }
+
+ // Abstract function to handle state changes based on events
+ protected abstract fun reduceState(event: Event)
+
+ // Function to post effects (to be observed)
+ protected fun postEffect(effect: Effect) {
+ viewModelScope.launch {
+ _effect.send(effect)
+ }
+ }
+
+ // Subscribe to events and call reduceState
+ private fun subscribeEvents() {
+ viewModelScope.launch {
+ event.collect {
+ reduceState(it)
+ }
+ }
+ }
+
+ // Update the current state
+ protected fun updateState(currentState: State) {
+ _uiState.update {
+ currentState
+ }
+ }
+
+ // Send events to trigger state changes
+ fun sendEvent(event: Event) {
+ viewModelScope.launch { _event.emit(event) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/MyInfoViewModelFactory.kt b/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/MyInfoViewModelFactory.kt
new file mode 100644
index 00000000..d4445661
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/MyInfoViewModelFactory.kt
@@ -0,0 +1,24 @@
+package org.sopt.and.presentation.viewmodelfactory
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import org.sopt.and.domain.repository.MyHobbyRepository
+import org.sopt.and.domain.usecase.MyHobbyUseCase
+import org.sopt.and.presentation.myinfo.MyInfoViewModel
+
+class MyInfoViewModelFactory : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return when (modelClass) {
+
+ MyInfoViewModel::class.java -> {
+ MyInfoViewModel(
+ MyHobbyUseCase(
+ getMyHobbyRepository = MyHobbyRepository.create()
+ )
+ ) as T
+ }
+
+ else -> throw IllegalArgumentException("Unknown ViewModel Class")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignInViewModelFactory.kt b/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignInViewModelFactory.kt
new file mode 100644
index 00000000..ed2c8ff9
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignInViewModelFactory.kt
@@ -0,0 +1,24 @@
+package org.sopt.and.presentation.viewmodelfactory
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import org.sopt.and.domain.repository.SignInRepository
+import org.sopt.and.domain.usecase.SignInUseCase
+import org.sopt.and.presentation.signin.SignInViewModel
+
+class SignInViewModelFactory : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return when (modelClass) {
+
+ SignInViewModel::class.java -> {
+ SignInViewModel(
+ SignInUseCase(
+ signInRepository = SignInRepository.create()
+ )
+ ) as T
+ }
+
+ else -> throw IllegalArgumentException("Unknown ViewModel Class")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignUpViewModelFactory.kt b/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignUpViewModelFactory.kt
new file mode 100644
index 00000000..59c7ec86
--- /dev/null
+++ b/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignUpViewModelFactory.kt
@@ -0,0 +1,24 @@
+package org.sopt.and.presentation.viewmodelfactory
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import org.sopt.and.domain.repository.SignUpRepository
+import org.sopt.and.domain.usecase.SignUpUseCase
+import org.sopt.and.presentation.signup.SignUpViewModel
+
+class SignUpViewModelFactory : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return when (modelClass) {
+
+ SignUpViewModel::class.java -> {
+ SignUpViewModel(
+ SignUpUseCase(
+ signUpRepository = SignUpRepository.create()
+ )
+ ) as T
+ }
+
+ else -> throw IllegalArgumentException("Unknown ViewModel Class")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/signin/SignInScreen.kt b/app/src/main/java/org/sopt/and/signin/SignInScreen.kt
deleted file mode 100644
index fcf209b9..00000000
--- a/app/src/main/java/org/sopt/and/signin/SignInScreen.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package org.sopt.and.signin
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.launch
-
-import org.sopt.and.R
-import org.sopt.and.components.AuthSignButton
-import org.sopt.and.viewmodel.SignViewModel
-
-import org.sopt.and.components.CustomTextField
-import org.sopt.and.components.SignTopBar
-
-@Composable
-fun SignInScreen(
- signViewModel: SignViewModel,
- onNavigateToMain: () -> Unit,
- onNavigateToSignUp: () -> Unit
-) {
- val snackbarHostState = remember { SnackbarHostState() }
- val coroutineScope = rememberCoroutineScope()
-
- var isPasswordVisible by remember { mutableStateOf(false) }
-
- Scaffold(
- snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
- ) { paddingValues ->
- Column(
- modifier = Modifier
- .fillMaxSize()
- .background(Color.Black)
- .padding(paddingValues)
- .padding(15.dp)
- ) {
- SignTopBar(isSignUp = false)
- Spacer(modifier = Modifier.height(10.dp))
-
- // 이메일 입력 필드
- CustomTextField(
- labelResId = R.string.email_label,
- textValue = signViewModel.email,
- onTextChanged = { signViewModel.email = it },
- modifier = Modifier.fillMaxWidth()
- )
-
- Spacer(modifier = Modifier.height(10.dp))
-
- // 비밀번호 입력 필드 및 show/hide 버튼
- CustomTextField(
- labelResId = R.string.password_label,
- textValue = signViewModel.password,
- onTextChanged = { signViewModel.password = it },
- isPasswordField = true,
- isPasswordVisible = isPasswordVisible,
- onPasswordToggle = { isPasswordVisible = !isPasswordVisible },
- modifier = Modifier.fillMaxWidth()
- )
-
- Spacer(modifier = Modifier.height(20.dp))
-
- // 로그인 버튼
- AuthSignButton(
- buttonText = "로그인",
- validateAction = {
- // 입력 검증
- signViewModel.email.text.isNotEmpty() && signViewModel.password.text.isNotEmpty()
- },
- onSuccess = {
- coroutineScope.launch {
- signViewModel.performLogin(
- onSuccess = {
- // 로그인 성공 시 snackbar 호출
- coroutineScope.launch {
- snackbarHostState.showSnackbar("로그인 성공!")
- onNavigateToMain()
- }
- },
- onFailure = { errorMessage ->
- // 로그인 실패 시 snackbar 호출
- coroutineScope.launch {
- snackbarHostState.showSnackbar(errorMessage)
- }
- }
- )
- }
- },
- onFailure = {
- coroutineScope.launch {
- snackbarHostState.showSnackbar("입력 값을 확인해주세요.")
- }
- }
- )
-
- Spacer(modifier = Modifier.height(20.dp))
-
- // 하단 링크
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Center
- ) {
- Text(
- text = "아이디 찾기",
- color = Color.White
- )
-
- Text(
- text = " | ",
- modifier = Modifier.padding(horizontal = 8.dp),
- color = Color.White
- )
-
- Text(
- text = "비밀번호 재설정",
- color = Color.White
- )
-
- Text(
- text = " | ",
- color = Color.White
- )
-
- Text(
- text = "회원가입",
- modifier = Modifier.clickable(onClick = onNavigateToSignUp),
- color = Color.White
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt b/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt
deleted file mode 100644
index a4ea66d2..00000000
--- a/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt
+++ /dev/null
@@ -1,208 +0,0 @@
-package org.sopt.and.signup
-
-import android.widget.Toast
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.sopt.and.R
-import org.sopt.and.components.AuthSignButton
-import org.sopt.and.viewmodel.SignViewModel
-import org.sopt.and.components.CustomTextField
-import org.sopt.and.components.SignTopBar
-
-@Composable
-fun SignUpScreen(
- signViewModel: SignViewModel,
- onNavigateToSignIn: () -> Unit
-) {
- val context = LocalContext.current
- val snackbarHostState = remember { SnackbarHostState() }
-
- var snackbarMessage by remember { mutableStateOf(null) }
-
- LaunchedEffect(snackbarMessage) {
- snackbarMessage?.let {
- snackbarHostState.showSnackbar(it)
- snackbarMessage = null // 메시지 초기화
- }
- }
-
- val coroutineScope = rememberCoroutineScope()
-
- // 상태값 관리
- var username by remember { mutableStateOf(TextFieldValue("")) }
- var password by remember { mutableStateOf(TextFieldValue("")) }
- var hobby by remember { mutableStateOf(TextFieldValue("")) }
-
- var usernameError by remember { mutableStateOf(false) }
- var passwordError by remember { mutableStateOf(false) }
- var hobbyError by remember { mutableStateOf(false) }
-
- Scaffold(
- snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
- ) { paddingValues ->
- Column(
- modifier = Modifier
- .fillMaxSize()
- .background(Color.Black)
- .padding(paddingValues)
- .padding(15.dp)
- ) {
- SignUpTobBar (onNavigateToSignIn)
- Spacer(modifier = Modifier.height(20.dp))
- SignTopBar(isSignUp = true)
- Spacer(modifier = Modifier.height(30.dp))
-
- // Username 입력 필드
- CustomTextField(
- labelResId = R.string.username_label,
- textValue = username,
- onTextChanged = {
- username = it
- usernameError = username.text.length > 8
- },
- showHint = true,
- hintResId = R.string.sign_up_username_hint,
- modifier = Modifier.fillMaxWidth()
- )
- if (usernameError) {
- Text(
- text = "8자보다 크면 안됩니다.",
- color = Color.Red,
- modifier = Modifier.padding(top = 4.dp)
- )
- }
-
- Spacer(modifier = Modifier.height(10.dp))
-
- // Password 입력 필드
- CustomTextField(
- labelResId = R.string.password_label,
- textValue = password,
- onTextChanged = {
- password = it
- passwordError = password.text.length > 8
- },
- isPasswordField = true,
- isPasswordVisible = signViewModel.isPasswordVisible,
- onPasswordToggle = {
- signViewModel.isPasswordVisible = !signViewModel.isPasswordVisible
- },
- showHint = true,
- hintResId = R.string.sign_up_password_hint,
- modifier = Modifier.fillMaxWidth()
- )
- if (passwordError) {
- Text(
- text = "8자보다 크면 안됩니다.",
- color = Color.Red,
- modifier = Modifier.padding(top = 4.dp)
- )
- }
-
- Spacer(modifier = Modifier.height(10.dp))
-
- // Hobby 입력 필드
- CustomTextField(
- labelResId = R.string.hobby_label,
- textValue = hobby,
- onTextChanged = {
- hobby = it
- hobbyError = hobby.text.length > 8
- },
- showHint = true,
- hintResId = R.string.sign_up_hobby_hint,
- modifier = Modifier.fillMaxWidth()
- )
- if (hobbyError) {
- Text(
- text = "8자보다 크면 안됩니다.",
- color = Color.Red,
- modifier = Modifier.padding(top = 4.dp)
- )
- }
-
- Spacer(modifier = Modifier.weight(1f))
-
- // 회원가입 버튼
- AuthSignButton(
- buttonText = "회원가입",
- validateAction = {
- // 전체 유효성 검사
-
-
- // 모든 조건을 만족해야 회원가입 요청 실행
- !usernameError && !passwordError && !hobbyError
- },
- onSuccess = {
- signViewModel.performSignUp(
- username.text,
- password.text,
- hobby.text,
- onSuccess = {
- coroutineScope.launch {
- withContext(Dispatchers.Main) {
- snackbarHostState.showSnackbar("회원가입 성공") // 메인 스레드에서 호출
- }
- }
- onNavigateToSignIn()
- },
- onFailure = { errorMessage ->
- coroutineScope.launch {
- withContext(Dispatchers.Main) {
- snackbarHostState.showSnackbar(errorMessage) // 메인 스레드에서 호출
- }
- }
- }
- )
- },
- onFailure = {
- coroutineScope.launch {
- withContext(Dispatchers.Main) {
- snackbarHostState.showSnackbar("입력 값을 확인해주세요.") // 메인 스레드에서 호출
- }
- }
- }
- )
- }
- }
-}
-
-@Composable
-fun SignUpTobBar(onNavigateToSignIn: () -> Unit) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(10.dp),
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = "회원가입",
- color = Color.White,
- fontSize = 20.sp,
- textAlign = TextAlign.Center
- )
- Text(
- text = "❌",
- color = Color.White,
- modifier = Modifier
- .align(Alignment.CenterEnd)
- .clickable { onNavigateToSignIn() }
- )
- }
-}
-
-
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 ecdcf5e3..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
@@ -8,4 +8,32 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
+val Pink40 = Color(0xFF7D5260)
+
+val Black100 = Color(0xFF1B1B1B)
+val Grey100 = Color(0xFF252525)
+val Grey200 = Color(0xFFa8a8a8)
+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
diff --git a/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt b/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt
deleted file mode 100644
index 833f4e08..00000000
--- a/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt
+++ /dev/null
@@ -1,165 +0,0 @@
-package org.sopt.and.viewmodel
-
-import android.app.Application
-import android.content.SharedPreferences
-import android.util.Log
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import org.sopt.and.api.HobbyService
-import org.sopt.and.api.LoginService
-import org.sopt.and.api.UserRegistrationService
-import org.sopt.and.dto.RequestLoginData
-import org.sopt.and.dto.RequestUserRegistrationData
-import org.sopt.and.dto.ResponseUserRegistration
-import javax.inject.Inject
-
-@HiltViewModel
-class SignViewModel @Inject constructor(
- application: Application,
- private val userRegistrationService: UserRegistrationService, // Hilt로 주입
- private val loginService: LoginService, // Hilt로 주입
- private val hobbyService: HobbyService // Hilt로 주입
-) : AndroidViewModel(application) {
-
- private val preferences: SharedPreferences by lazy {
- application.getSharedPreferences("user_prefs", Application.MODE_PRIVATE)
- }
-
- var email by mutableStateOf(TextFieldValue("")) // TextFieldValue 사용
- var password by mutableStateOf(TextFieldValue("")) // TextFieldValue 사용
- var hobby by mutableStateOf("") // hobby 추가
- var isPasswordVisible by mutableStateOf(false)
-
- private var emailError by mutableStateOf("")
- private var passwordError by mutableStateOf("")
-
- /** 회원가입 API 요청 */
- fun performSignUp(
- username: String,
- password: String,
- hobby: String,
- onSuccess: (ResponseUserRegistration) -> Unit,
- onFailure: (String) -> Unit
- ) {
- viewModelScope.launch(Dispatchers.IO) {
- try {
- val request = RequestUserRegistrationData(username, password, hobby)
- val response = userRegistrationService.postUserRegistration(request) // 주입된 서비스 사용
- if (response.isSuccessful) {
- response.body()?.let { onSuccess(it) } ?: onFailure("서버 응답이 비어있습니다.")
- } else {
- onFailure("회원가입 실패: ${response.code()} - ${response.message()}")
- }
- } catch (e: Exception) {
- onFailure("에러 발생: ${e.localizedMessage}")
- }
- }
- }
-
- /** 로그인 API 요청 */
- fun performLogin(
- onSuccess: () -> Unit,
- onFailure: (String) -> Unit
- ) {
- viewModelScope.launch(Dispatchers.IO) {
- try {
- val request = RequestLoginData(
- userName = email.text,
- password = password.text
- )
- Log.d("SignViewModel", "로그인 요청 데이터: $request") // 요청 데이터 로그
-
- val response = loginService.postLogin(request) // Hilt로 주입된 서비스 사용
- if (response.isSuccessful) {
- response.body()?.let {
- val token = it.result.token
- Log.d("SignViewModel", "로그인 성공, 토큰: $token") // 성공 응답 로그
- preferences.edit().putString("auth_token", token).apply()
- onSuccess()
- } ?: onFailure("서버 응답이 비어있습니다.")
- } else {
- Log.e(
- "SignViewModel",
- "로그인 실패: ${response.code()} - ${response.message()} - ${response.errorBody()?.string()}"
- ) // 실패 로그
- onFailure("로그인 실패: ${response.code()} - ${response.message()}")
- }
- } catch (e: Exception) {
- Log.e("SignViewModel", "로그인 요청 중 에러 발생: ${e.localizedMessage}", e) // 예외 로그
- onFailure("에러 발생: ${e.localizedMessage}")
- }
- }
- }
-
- /** 취미 조회 API 요청 */
- fun fetchHobby(
- onSuccess: () -> Unit,
- onFailure: (String) -> Unit
- ) {
- val token = preferences.getString("auth_token", null)
- if (token.isNullOrEmpty()) {
- onFailure("유효한 토큰이 없습니다.")
- return
- }
-
- viewModelScope.launch(Dispatchers.IO) {
- try {
- val response = hobbyService.getHobby(token)
- if (response.isSuccessful) {
- response.body()?.let {
- hobby = it.result.hobby // 상태값 업데이트
- Log.d("SignViewModel", "취미 조회 성공: $hobby")
- onSuccess()
- } ?: onFailure("서버 응답이 비어있습니다.")
- } else {
- Log.e(
- "SignViewModel",
- "취미 조회 실패: ${response.code()} - ${response.message()} - ${response.errorBody()?.string()}"
- )
- onFailure("취미 조회 실패: ${response.code()} - ${response.message()}")
- }
- } catch (e: Exception) {
- Log.e("SignViewModel", "취미 조회 요청 중 에러 발생: ${e.localizedMessage}", e)
- onFailure("에러 발생: ${e.localizedMessage}")
- }
- }
- }
-
- /** Validate inputs for sign-up */
- fun validateSignUpInputs(username: String, password: String, hobby: String): Boolean {
- return username.length <= 8 && password.length <= 8 && hobby.length <= 8
- }
-
- /** Validate email and password during sign-in */
- fun validateSignInInputs(): Boolean {
- return email.text.isNotEmpty() && password.text.isNotEmpty()
- }
-
- companion object Constants {
- const val MIN_PASSWORD_LENGTH = 8
- const val MAX_PASSWORD_LENGTH = 20
- const val PASSWORD_CRITERIA_COUNT = 3
- val EMAIL_REGEX = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$")
- val LOWER_CASE_REGEX = Regex("[a-z]")
- val UPPER_CASE_REGEX = Regex("[A-Z]")
- val DIGIT_REGEX = Regex("[0-9]")
- val SPECIAL_REGEX = Regex("[!@#\$%^&*(),.?\\\":{}|<>]")
- }
-
- private fun isPasswordComplexEnough(password: String): Boolean {
- val criteriaCount = listOf(
- Constants.LOWER_CASE_REGEX.containsMatchIn(password),
- Constants.UPPER_CASE_REGEX.containsMatchIn(password),
- Constants.DIGIT_REGEX.containsMatchIn(password),
- Constants.SPECIAL_REGEX.containsMatchIn(password)
- ).count { it }
- return criteriaCount >= Constants.PASSWORD_CRITERIA_COUNT
- }
-}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/apple_icon.png b/app/src/main/res/drawable/apple_icon.png
new file mode 100644
index 00000000..552babf0
Binary files /dev/null and b/app/src/main/res/drawable/apple_icon.png differ
diff --git a/app/src/main/res/drawable/facebook_icon.png b/app/src/main/res/drawable/facebook_icon.png
new file mode 100644
index 00000000..8be54be7
Binary files /dev/null and b/app/src/main/res/drawable/facebook_icon.png differ
diff --git a/app/src/main/res/drawable/ic_cast_24.xml b/app/src/main/res/drawable/ic_cast_24.xml
new file mode 100644
index 00000000..7e998416
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cast_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_live_tv_24.xml b/app/src/main/res/drawable/ic_live_tv_24.xml
new file mode 100644
index 00000000..e878e129
--- /dev/null
+++ b/app/src/main/res/drawable/ic_live_tv_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/kakao_talk_icon.png b/app/src/main/res/drawable/kakao_talk_icon.png
new file mode 100644
index 00000000..90f7a6ff
Binary files /dev/null and b/app/src/main/res/drawable/kakao_talk_icon.png differ
diff --git a/app/src/main/res/drawable/logo_apple.jpg b/app/src/main/res/drawable/logo_apple.jpg
deleted file mode 100644
index 1b4d5a39..00000000
Binary files a/app/src/main/res/drawable/logo_apple.jpg and /dev/null differ
diff --git a/app/src/main/res/drawable/logo_facebook.jpg b/app/src/main/res/drawable/logo_facebook.jpg
deleted file mode 100644
index bd29916b..00000000
Binary files a/app/src/main/res/drawable/logo_facebook.jpg and /dev/null differ
diff --git a/app/src/main/res/drawable/logo_kakao.jpg b/app/src/main/res/drawable/logo_kakao.jpg
deleted file mode 100644
index 6931b374..00000000
Binary files a/app/src/main/res/drawable/logo_kakao.jpg and /dev/null differ
diff --git a/app/src/main/res/drawable/logo_naver.jpg b/app/src/main/res/drawable/logo_naver.jpg
deleted file mode 100644
index dea9c1a0..00000000
Binary files a/app/src/main/res/drawable/logo_naver.jpg and /dev/null differ
diff --git a/app/src/main/res/drawable/logo_wavve.jpg b/app/src/main/res/drawable/logo_wavve.jpg
deleted file mode 100644
index 697eee38..00000000
Binary files a/app/src/main/res/drawable/logo_wavve.jpg and /dev/null differ
diff --git a/app/src/main/res/drawable/naver_icon.jpeg b/app/src/main/res/drawable/naver_icon.jpeg
new file mode 100644
index 00000000..de5277c5
Binary files /dev/null and b/app/src/main/res/drawable/naver_icon.jpeg differ
diff --git a/app/src/main/res/drawable/t_world_icon.png b/app/src/main/res/drawable/t_world_icon.png
new file mode 100644
index 00000000..4a0fa45a
Binary files /dev/null and b/app/src/main/res/drawable/t_world_icon.png differ
diff --git a/app/src/main/res/drawable/wavve_logo.png b/app/src/main/res/drawable/wavve_logo.png
new file mode 100644
index 00000000..46949c00
Binary files /dev/null and b/app/src/main/res/drawable/wavve_logo.png differ
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bb5a9d97..a57cddee 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
- AND-ANDROID
+
이메일 주소 또는 아이디
비밀번호
로그인
@@ -20,4 +20,172 @@
취미를 입력해주세요.
취미를 입력해주세요.
+
+
+ 홈
+ 검색
+ My
+
+
+
+ wavve
+ |
+ hide
+ show
+ 카카오 계정과 연동하는 아이콘
+ T World 계정과 연동하는 아이콘
+ 네이버 계정과 연동하는 아이콘
+ 페이스북 계정과 연동하는 아이콘
+ 애플 계정과 연동하는 아이콘
+ • SNS계정으로 간편하게 가입하여 서비스를 이용하실 수 있습니다.\n
+ 기존 POOQ 계정 또는 Wavve 계정과는 연동되지 않으니 이용에 참고하세요.
+
+
+
+ 회원가입
+ wavve Username 설정
+ wavve 비밀번호 설정
+ 취미입력 ex)soccer
+ Wavve 회원가입
+ 회원가입에 성공했습니다~
+ Username이 중복되었습니다!
+ username, 비밀번호, 취미는 8자 이하로 입력해주세요!
+ 비밀번호는 8자 이하로 입력해 주세요
+ Username는 8자 이하로 입력해 주세요
+ Username 입력시 주의사항
+ 비밀번호 입력시 주의사항
+ 회원가입 화면 닫기
+ 이메일과 비밀번호만으로\nWavve를 즐길 수 있어요!
+ 또는 다른 서비스 계정으로 가입
+
+
+ 로그인
+ 아이디 찾기
+ 비밀번호 재설정
+ 회원가입
+ Username
+ 비밀번호
+ 로그인 성공. 환영합니다~
+ 로그인 실패. 비밀번호는 8자리 이하입니다!
+ 로그인 실패. 비밀번호가 일치하지 않습니다!
+ 이전 화면으로 가기
+ 또는 다른 서비스 계정으로 로그인
+
+
+
+ 내 프로필
+ 알림
+ 설정
+ 구매하기 >
+ 현재 보유하신 이용권이 없습니다.
+ 첫 결제 시 첫 달 100원!
+ 전체 시청내역
+ 시청내역이 없어요.
+ 관심 프로그램
+ 관심 프로그램이 없어요.
+
+
+ 믿고 보는 웨이브 에디터 추천작
+ 오늘의 TOP 20
+ %1$s
+ | %1$s
+
+
+
+
+
+ 뉴클래식
+ 드라마
+ 예능
+ 영화
+ 애니
+ 해외시리즈
+ %1$s
+
+
+
+
+ 사용자 이름
+ 비밀번호
+ 로그인
+ 아이디 찾기
+ |
+ 비밀번호 재설정
+ 회원가입
+ 또는 다른 서비스 계정으로 로그인
+ SNS계정으로 간편하게 가입하여 서비스를 이용하실 수 있습니다. 기존 POOQ 계정 또는 Wavve 계정과는 연동되지 않으니 이용에 참고하세요.
+ 로그인 성공
+ 아이디와 비밀번호를 입력해주세요
+ 아이디 혹은 비밀번호가 틀렸습니다.
+ 닫기
+ hide
+ show
+
+
+ 회원가입
+ 입력하신 아이디가 형식에 맞지 않습니다. 아이디는 8자 이내로 입력해주세요.
+ 아이디는 8자 이내로 입력해주세요.
+ 입력하신 비밀번호가 형식에 맞지 않습니다. 비밀번호는 8자 이내로 입력해주세요.
+ 비밀번호는 8자 이내로 입력해주세요.
+ 입력하신 취미가 형식에 맞지 않습니다. 취미는 8자 이내로 입력해 주세요.
+ 취미는 8자 이내로 입력해주세요.
+ Wavve 비밀번호 설정
+ 또는 다른 서비스 계정으로 로그인
+ SNS계정으로 간편하게 가입하여 서비스를 이용하실 수 있습니다. 기존 POOQ 계정 또는 Wavve 계정과는 연동되지 않으니 이용에 참고하세요.
+ 회원가입을 완료하였습니다.
+ 아이디, 비밀번호, 취미를 양식에 맞게 작성해주세요
+ Wavve 회원가입
+ 아이디와 비밀번호, 취미 만으로\nWavve를 즐길 수 있어요!
+ hide
+ show
+ 아이디
+ 취미
+
+ KaKao Account
+ T Account
+ Naver Account
+ FaceBook Account
+ Apple Account
+
+
+ Logo
+ back
+ Close
+
+
+ profile image
+ notification
+ setting
+ 첫 결제 시 첫 달 100원!
+ 현재 보유하신 이용권이 없습니다.
+ 전체 시청내역
+ 시청내역이 없어요.
+ 관심 프로그램
+ 관심 프로그램이 없어요.
+ 로그아웃
+ 로그아웃 되었습니다.
+
+ 구매하기
+ purchase
+ No Content
+ more
+
+
+ 회원가입
+ 홈
+ MY
+ 검색
+ 뉴클래식
+ 드라마
+ 예능
+ 영화
+ 애니
+ 해외시리즈
+ 시사교양
+ 키즈
+ 영화플러스
+ live button
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 8952fb35..a064be53 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,11 +3,7 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
- id("com.google.dagger.hilt.android") version "2.51.1" apply false
-}
-
-buildscript {
- dependencies {
- classpath(libs.hilt.android.gradle.plugin)
- }
-}
+ alias(libs.plugins.kotlin.serialization) apply false
+ alias(libs.plugins.hilt) apply false
+ alias(libs.plugins.ksp) apply false
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3ff8a150..af7ed861 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,43 +1,30 @@
[versions]
-agp = "8.7.1"
-hiltAndroid = "2.51.1"
-hiltAndroidGradlePlugin = "2.38.1"
-hiltCompiler = "2.51.1"
-hiltNavigationCompose = "1.2.0"
+agp = "8.7.0"
kotlin = "2.0.0"
-coreKtx = "1.13.1"
+coreKtx = "1.10.1"
junit = "4.13.2"
-junitVersion = "1.2.1"
-espressoCore = "3.6.1"
-lifecycleRuntimeKtx = "2.8.6"
-activityCompose = "1.9.3"
-composeBom = "2024.10.00"
-lifecycleViewmodelCompose = "2.8.6"
-material = "1.7.4"
-navigationCompose = "2.8.3"
-navigationRuntimeKtx = "2.8.3"
-# Third Party
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+lifecycleRuntimeKtx = "2.6.1"
+activityCompose = "1.8.0"
+composeBom = "2024.04.01"
+androidxComposeNavigation = "2.8.2"
okhttp = "4.11.0"
retrofit = "2.9.0"
retrofitKotlinSerializationConverter = "1.0.0"
-kotlinxSerializationJson = "1.6.3"
-runtimeLivedata = "1.7.5"
+kotlinxSerializationJson = "1.7.3"
+hilt = "2.51.1"
+hilt-navigation-compose = "1.2.0"
+ksp = "2.0.0-1.0.22"
+
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
-androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
-androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
-androidx-material = { module = "androidx.compose.material:material", version.ref = "material" }
-androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroidGradlePlugin" }
-hilt-android-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hiltAndroidGradlePlugin" }
-hilt-android-v2511 = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltAndroidGradlePlugin" }
-hilt-compiler-v2511 = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltCompiler" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
@@ -47,7 +34,11 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
-androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" }
+androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxComposeNavigation" }
+hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
+hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
+hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
+hilt-navigation-compose = {group="androidx.hilt", name="hilt-navigation-compose", version.ref = "hilt-navigation-compose"}
# Third Party
okhttp-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp" }
@@ -55,10 +46,11 @@ okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-i
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-kotlin-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinSerializationConverter" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
-androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
\ No newline at end of file