diff --git a/Nyong/week7/nikecompose/app/build.gradle.kts b/Nyong/week7/nikecompose/app/build.gradle.kts index 4d13796c..9abeee9a 100644 --- a/Nyong/week7/nikecompose/app/build.gradle.kts +++ b/Nyong/week7/nikecompose/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose) @@ -19,6 +21,15 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + val properties = Properties() + properties.load(rootProject.file("local.properties").inputStream()) + + buildConfigField( + "String", + "REQRES_API_KEY", + "\"${properties.getProperty("REQRES_API_KEY")}\"" + ) } buildTypes { @@ -36,6 +47,7 @@ android { } buildFeatures { compose = true + buildConfig = true } } @@ -57,4 +69,17 @@ dependencies { androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) + // ViewModel을 Compose에서 사용하기 위한 의존성 + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0") + + // Retrofit + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + + // OkHttp 로그 확인용 + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + // Coil: Compose 이미지 로딩용 + implementation("io.coil-kt.coil3:coil-compose:3.0.4") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.4") } \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/AndroidManifest.xml b/Nyong/week7/nikecompose/app/src/main/AndroidManifest.xml index a4fe22dc..64c6697d 100644 --- a/Nyong/week7/nikecompose/app/src/main/AndroidManifest.xml +++ b/Nyong/week7/nikecompose/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/MainActivity.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/MainActivity.kt deleted file mode 100644 index 215915a0..00000000 --- a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/MainActivity.kt +++ /dev/null @@ -1,655 +0,0 @@ -package com.example.nikecompose - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsBottomHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationBarItemDefaults -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -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.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import com.example.nikecompose.ui.theme.NikeComposeTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - - setContent { - NikeComposeTheme { - NikeComposeApp() - } - } - } -} - -sealed class BottomRoute( - val route: String, - val label: String, - @DrawableRes val iconResId: Int -) { - data object Home : BottomRoute("home", "홈", R.drawable.ic_home) - data object Shop : BottomRoute("shop", "구매하기", R.drawable.ic_buy) - data object Wishlist : BottomRoute("wishlist", "위시리스트", R.drawable.ic_wishlist) - data object Cart : BottomRoute("cart", "장바구니", R.drawable.ic_cart) - data object Profile : BottomRoute("profile", "프로필", R.drawable.ic_profile) -} - -data class ProductItem( - val id: Int, - @DrawableRes val imageResId: Int, - val name: String, - val subTitle: String, - val colors: String, - val price: String, - val isLiked: Boolean = false, - val badge: String? = null -) - -val productList = listOf( - ProductItem( - id = 1, - imageResId = R.drawable.img_shoe_1, - name = "Nike Everyday Plus Cushioned", - subTitle = "Training Ankle Socks (6 Pairs)", - colors = "5 Colours", - price = "US$10", - isLiked = true - ), - ProductItem( - id = 2, - imageResId = R.drawable.img_shoe_2, - name = "Nike Elite Crew", - subTitle = "Basketball Socks", - colors = "7 Colours", - price = "US$16", - isLiked = false - ), - ProductItem( - id = 3, - imageResId = R.drawable.img_shoe_1, - name = "Nike Air Force 1 '07", - subTitle = "Women's Shoes", - colors = "5 Colours", - price = "US$115", - isLiked = false, - badge = "BestSeller" - ), - ProductItem( - id = 4, - imageResId = R.drawable.img_shoe_2, - name = "Jordan ENike Air Force 1 '07ssentials", - subTitle = "Men's Shoes", - colors = "2 Colours", - price = "US$115", - isLiked = false, - badge = "BestSeller" - ) -) - -@Composable -fun NikeComposeApp() { - val navController = rememberNavController() - - val bottomItems = listOf( - BottomRoute.Home, - BottomRoute.Shop, - BottomRoute.Wishlist, - BottomRoute.Cart, - BottomRoute.Profile - ) - - Scaffold( - modifier = Modifier.fillMaxSize(), - containerColor = Color.White, - bottomBar = { - NikeBottomBar( - items = bottomItems, - currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route, - onItemClick = { item -> - navController.navigate(item.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - } - ) - } - ) { innerPadding -> - NavHost( - navController = navController, - startDestination = BottomRoute.Home.route, - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { - composable(BottomRoute.Home.route) { - HomeScreen() - } - - composable(BottomRoute.Shop.route) { - ShopScreen() - } - - composable(BottomRoute.Wishlist.route) { - WishlistScreen() - } - - composable(BottomRoute.Cart.route) { - CartScreen( - onOrderClick = { - navController.navigate(BottomRoute.Shop.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - } - ) - } - - composable(BottomRoute.Profile.route) { - ProfileScreen() - } - } - } -} - -@Composable -fun NikeBottomBar( - items: List, - currentRoute: String?, - onItemClick: (BottomRoute) -> Unit -) { - NavigationBar( - containerColor = Color.White, - tonalElevation = 0.dp - ) { - items.forEach { item -> - val selected = currentRoute == item.route - val iconColor = if (selected) Color.Black else Color.Gray - - NavigationBarItem( - selected = selected, - onClick = { onItemClick(item) }, - icon = { - Icon( - painter = painterResource(id = item.iconResId), - contentDescription = item.label, - modifier = Modifier.size(22.dp), - tint = iconColor - ) - }, - label = { - Text( - text = item.label, - fontSize = 10.sp - ) - }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = Color.Black, - selectedTextColor = Color.Black, - unselectedIconColor = Color.Gray, - unselectedTextColor = Color.Gray, - indicatorColor = Color.Transparent - ) - ) - } - } -} - -@Composable -fun HomeScreen() { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .background(Color.White), - contentPadding = PaddingValues(top = 32.dp, bottom = 24.dp) - ) { - item { - Column( - modifier = Modifier.padding(horizontal = 24.dp) - ) { - Text( - text = "Discover", - fontSize = 26.sp, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = "9월 4일 목요일", - fontSize = 13.sp, - color = Color.Gray - ) - - Spacer(modifier = Modifier.height(28.dp)) - - Image( - painter = painterResource(id = R.drawable.img_home_logo), - contentDescription = "홈 배너 이미지", - modifier = Modifier.fillMaxWidth(), - contentScale = ContentScale.FillWidth - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Text( - text = "What's new", - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - color = Color.Gray - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = "나이키 최신 상품", - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - - Spacer(modifier = Modifier.height(16.dp)) - } - } - - item { - LazyRow( - contentPadding = PaddingValues(horizontal = 24.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - items( - items = productList.take(2), - key = { product -> product.id } - ) { product -> - HomeProductCard(product = product) - } - } - } - } -} - -@Composable -fun HomeProductCard( - product: ProductItem -) { - Column( - modifier = Modifier.width(230.dp) - ) { - Image( - painter = painterResource(id = product.imageResId), - contentDescription = product.name, - modifier = Modifier - .fillMaxWidth() - .height(230.dp) - .background(Color(0xFFF5F5F5)), - contentScale = ContentScale.Crop - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = product.name, - fontSize = 11.sp, - fontWeight = FontWeight.Bold, - color = Color.Black, - lineHeight = 14.sp - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = product.price, - fontSize = 11.sp, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - } -} - -@Composable -fun ShopScreen() { - var selectedTabIndex by remember { mutableIntStateOf(0) } - val tabs = listOf("전체", "Tops & T-shirts", "sale") - - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - ) { - Spacer(modifier = Modifier.height(28.dp)) - - TabRow( - selectedTabIndex = selectedTabIndex, - containerColor = Color.White, - contentColor = Color.Black - ) { - tabs.forEachIndexed { index, title -> - Tab( - selected = selectedTabIndex == index, - onClick = { selectedTabIndex = index }, - text = { - Text( - text = title, - fontSize = 12.sp - ) - } - ) - } - } - - LazyVerticalGrid( - columns = GridCells.Fixed(2), - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 24.dp, vertical = 24.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(36.dp) - ) { - items( - items = productList, - key = { product -> product.id } - ) { product -> - ShopProductCard(product = product) - } - } - } -} - -@Composable -fun ShopProductCard( - product: ProductItem -) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - Box( - modifier = Modifier.fillMaxWidth() - ) { - Image( - painter = painterResource(id = product.imageResId), - contentDescription = product.name, - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - .background(Color(0xFFF5F5F5)), - contentScale = ContentScale.Crop - ) - - Icon( - painter = painterResource( - id = if (product.isLiked) R.drawable.ic_heart_on else R.drawable.ic_heart_off - ), - contentDescription = "좋아요", - modifier = Modifier - .align(Alignment.TopEnd) - .padding(8.dp) - .size(22.dp), - tint = Color.Unspecified - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - product.badge?.let { - Text( - text = it, - fontSize = 11.sp, - color = Color(0xFFFF6A00), - fontWeight = FontWeight.Bold - ) - } - - Text( - text = product.name, - fontSize = 11.sp, - fontWeight = FontWeight.Bold, - color = Color.Black, - lineHeight = 14.sp - ) - - Text( - text = product.subTitle, - fontSize = 10.sp, - color = Color.Gray, - lineHeight = 13.sp - ) - - Text( - text = product.colors, - fontSize = 10.sp, - color = Color.Gray, - lineHeight = 13.sp - ) - - Spacer(modifier = Modifier.height(2.dp)) - - Text( - text = product.price, - fontSize = 11.sp, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - } -} - -@Composable -fun WishlistScreen() { - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - ) { - Text( - text = "위시리스트", - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = Color.Black, - modifier = Modifier.padding(start = 24.dp, top = 40.dp) - ) - - LazyVerticalGrid( - columns = GridCells.Fixed(2), - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 24.dp, vertical = 28.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(28.dp) - ) { - items( - items = productList, - key = { product -> product.id } - ) { product -> - WishlistProductCard(product = product) - } - } - } -} - -@Composable -fun WishlistProductCard( - product: ProductItem -) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - Image( - painter = painterResource(id = product.imageResId), - contentDescription = product.name, - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - .background(Color(0xFFF5F5F5)), - contentScale = ContentScale.Crop - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = product.name, - fontSize = 11.sp, - fontWeight = FontWeight.Bold, - color = Color.Black, - lineHeight = 14.sp - ) - - Text( - text = product.price, - fontSize = 11.sp, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - - if (product.id == 2) { - Text( - text = product.subTitle, - fontSize = 10.sp, - color = Color.Gray, - lineHeight = 13.sp - ) - - Text( - text = product.colors, - fontSize = 10.sp, - color = Color.Gray, - lineHeight = 13.sp - ) - - Text( - text = "US$10", - fontSize = 11.sp, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - } - } -} - -@Composable -fun CartScreen( - onOrderClick: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - .padding(horizontal = 24.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = painterResource(id = R.drawable.ic_cartcircle), - contentDescription = "장바구니 아이콘", - modifier = Modifier.size(56.dp) - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = "장바구니가 비어 있습니다.\n제품을 추가하면 여기에 표시됩니다.", - fontSize = 12.sp, - color = Color.Black, - textAlign = TextAlign.Center, - lineHeight = 18.sp - ) - } - } - - Button( - onClick = onOrderClick, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Black, - contentColor = Color.White - ) - ) { - Text( - text = "주문하기", - fontSize = 15.sp, - fontWeight = FontWeight.Bold - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) - } -} - -@Composable -fun ProfileScreen() { - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - .padding(horizontal = 24.dp, vertical = 32.dp) - ) { - Text( - text = "프로필", - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - } -} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/core/component/NikeBottomBar.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/core/component/NikeBottomBar.kt new file mode 100644 index 00000000..ec69f724 --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/core/component/NikeBottomBar.kt @@ -0,0 +1,58 @@ +package com.example.nikecompose.core.component + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.nikecompose.navigation.BottomRoute + +@Composable +fun NikeBottomBar( + items: List, + currentRoute: String?, + onItemClick: (BottomRoute) -> Unit +) { + NavigationBar( + containerColor = Color.White, + tonalElevation = 0.dp + ) { + items.forEach { item -> + val selected = currentRoute == item.route + val iconColor = if (selected) Color.Black else Color.Gray + + NavigationBarItem( + selected = selected, + onClick = { onItemClick(item) }, + icon = { + Icon( + painter = painterResource(id = item.iconResId), + contentDescription = item.label, + modifier = Modifier.size(22.dp), + tint = iconColor + ) + }, + label = { + Text( + text = item.label, + fontSize = 10.sp + ) + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = Color.Black, + selectedTextColor = Color.Black, + unselectedIconColor = Color.Gray, + unselectedTextColor = Color.Gray, + indicatorColor = Color.Transparent + ) + ) + } + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/api/ReqresApiService.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/api/ReqresApiService.kt new file mode 100644 index 00000000..7b340f5d --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/api/ReqresApiService.kt @@ -0,0 +1,24 @@ +package com.example.nikecompose.data.api + +import com.example.nikecompose.data.dto.response.UserListResponse +import com.example.nikecompose.data.dto.response.UserResponse +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Path +import retrofit2.http.Query + +interface ReqresApiService { + + @GET("api/users/{userId}") + suspend fun getUser( + @Header("x-api-key") apiKey: String, + @Path("userId") userId: Int + ): Response + + @GET("api/users") + suspend fun getUsers( + @Header("x-api-key") apiKey: String, + @Query("page") page: Int + ): Response +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/api/ReqresClient.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/api/ReqresClient.kt new file mode 100644 index 00000000..2bcd18cb --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/api/ReqresClient.kt @@ -0,0 +1,28 @@ +package com.example.nikecompose.data.api + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object ReqresClient { + + private const val BASE_URL = "https://reqres.in/" + + private val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + private val okHttpClient = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + + val apiService: ReqresApiService by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(ReqresApiService::class.java) + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/dto/response/UserListResponse.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/dto/response/UserListResponse.kt new file mode 100644 index 00000000..904a06c7 --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/dto/response/UserListResponse.kt @@ -0,0 +1,9 @@ +package com.example.nikecompose.data.dto.response + +data class UserListResponse( + val page: Int, + val per_page: Int, + val total: Int, + val total_pages: Int, + val data: List +) \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/dto/response/UserResponse.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/dto/response/UserResponse.kt new file mode 100644 index 00000000..b1e1fa61 --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/dto/response/UserResponse.kt @@ -0,0 +1,17 @@ +package com.example.nikecompose.data.dto.response + +import com.google.gson.annotations.SerializedName + +data class UserResponse( + val data: ReqresUserDto +) + +data class ReqresUserDto( + val id: Int, + val email: String, + @SerializedName("first_name") + val firstName: String, + @SerializedName("last_name") + val lastName: String, + val avatar: String +) \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/dummy/ProductDummyData.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/dummy/ProductDummyData.kt new file mode 100644 index 00000000..ab0b9cdf --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/dummy/ProductDummyData.kt @@ -0,0 +1,45 @@ +package com.example.nikecompose.data.dummy + +import com.example.nikecompose.R +import com.example.nikecompose.domain.model.ProductItem + +val productList = listOf( + ProductItem( + id = 1, + imageResId = R.drawable.img_shoe_1, + name = "Nike Everyday Plus Cushioned", + subTitle = "Training Ankle Socks (6 Pairs)", + colors = "5 Colours", + price = "US$10", + isLiked = true + ), + ProductItem( + id = 2, + imageResId = R.drawable.img_shoe_2, + name = "Nike Elite Crew", + subTitle = "Basketball Socks", + colors = "7 Colours", + price = "US$16", + isLiked = false + ), + ProductItem( + id = 3, + imageResId = R.drawable.img_shoe_1, + name = "Nike Air Force 1 '07", + subTitle = "Women's Shoes", + colors = "5 Colours", + price = "US$115", + isLiked = false, + badge = "BestSeller" + ), + ProductItem( + id = 4, + imageResId = R.drawable.img_shoe_2, + name = "Jordan ENike Air Force 1 '07ssentials", + subTitle = "Men's Shoes", + colors = "2 Colours", + price = "US$115", + isLiked = false, + badge = "BestSeller" + ) +) \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/repository/ProfileRepository.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/repository/ProfileRepository.kt new file mode 100644 index 00000000..4be7b71e --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/data/repository/ProfileRepository.kt @@ -0,0 +1,52 @@ +package com.example.nikecompose.data.repository + +import com.example.nikecompose.BuildConfig +import com.example.nikecompose.data.api.ReqresClient +import com.example.nikecompose.data.dto.response.ReqresUserDto + +class ProfileRepository { + + private val apiService = ReqresClient.apiService + private val apiKey = BuildConfig.REQRES_API_KEY + + suspend fun getProfileUser(): Result { + return try { + val response = apiService.getUser( + apiKey = apiKey, + userId = 1 + ) + + if (response.isSuccessful) { + val user = response.body()?.data + + if (user != null) { + Result.success(user) + } else { + Result.failure(Exception("유저 정보가 비어 있습니다.")) + } + } else { + Result.failure(Exception("프로필 요청 실패: ${response.code()}")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun getFollowingUsers(): Result> { + return try { + val response = apiService.getUsers( + apiKey = apiKey, + page = 1 + ) + + if (response.isSuccessful) { + val users = response.body()?.data.orEmpty() + Result.success(users.take(6)) + } else { + Result.failure(Exception("팔로잉 목록 요청 실패: ${response.code()}")) + } + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/domain/model/ProductItem.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/domain/model/ProductItem.kt new file mode 100644 index 00000000..fe8dfcb5 --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/domain/model/ProductItem.kt @@ -0,0 +1,14 @@ +package com.example.nikecompose.domain.model + +import androidx.annotation.DrawableRes + +data class ProductItem( + val id: Int, + @DrawableRes val imageResId: Int, + val name: String, + val subTitle: String, + val colors: String, + val price: String, + val isLiked: Boolean = false, + val badge: String? = null +) \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/cart/presentation/CartScreen.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/cart/presentation/CartScreen.kt new file mode 100644 index 00000000..4bd15193 --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/cart/presentation/CartScreen.kt @@ -0,0 +1,87 @@ +package com.example.nikecompose.feature.cart.presentation + +import androidx.compose.foundation.Image +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.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.nikecompose.R + +@Composable +fun CartScreen( + onOrderClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 24.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.ic_cartcircle), + contentDescription = "장바구니 아이콘", + modifier = Modifier.size(56.dp) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = "장바구니가 비어 있습니다.\n제품을 추가하면 여기에 표시됩니다.", + fontSize = 12.sp, + color = Color.Black, + textAlign = TextAlign.Center, + lineHeight = 18.sp + ) + } + } + + Button( + onClick = onOrderClick, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ) + ) { + Text( + text = "주문하기", + fontSize = 15.sp, + fontWeight = FontWeight.Bold + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/home/presentation/HomeScreen.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/home/presentation/HomeScreen.kt new file mode 100644 index 00000000..1179d0fc --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/home/presentation/HomeScreen.kt @@ -0,0 +1,101 @@ +package com.example.nikecompose.feature.home.presentation + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.nikecompose.R +import com.example.nikecompose.data.dummy.productList +import com.example.nikecompose.feature.home.presentation.component.HomeProductCard + +@Composable +fun HomeScreen() { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + contentPadding = PaddingValues(top = 32.dp, bottom = 24.dp) + ) { + item { + Column( + modifier = Modifier.padding(horizontal = 24.dp) + ) { + Text( + text = "Discover", + fontSize = 26.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "9월 4일 목요일", + fontSize = 13.sp, + color = Color.Gray + ) + + Spacer(modifier = Modifier.height(28.dp)) + + Image( + painter = painterResource(id = R.drawable.img_home_logo), + contentDescription = "홈 배너 이미지", + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.FillWidth + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "What's new", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color.Gray + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "나이키 최신 상품", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(16.dp)) + } + } + + item { + LazyRow( + contentPadding = PaddingValues(horizontal = 24.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + items( + items = productList.take(2), + key = { product -> product.id } + ) { product -> + HomeProductCard(product = product) + } + } + } + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/home/presentation/component/HomeProductCard.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/home/presentation/component/HomeProductCard.kt new file mode 100644 index 00000000..d118827c --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/home/presentation/component/HomeProductCard.kt @@ -0,0 +1,57 @@ +package com.example.nikecompose.feature.home.presentation.component + +import androidx.compose.foundation.Image +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.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.nikecompose.domain.model.ProductItem + +@Composable +fun HomeProductCard( + product: ProductItem +) { + Column( + modifier = Modifier.width(230.dp) + ) { + Image( + painter = painterResource(id = product.imageResId), + contentDescription = product.name, + modifier = Modifier + .fillMaxWidth() + .height(230.dp) + .background(Color(0xFFF5F5F5)), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = product.name, + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = Color.Black, + lineHeight = 14.sp + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = product.price, + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/main/MainActivity.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/main/MainActivity.kt new file mode 100644 index 00000000..0742c0f0 --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/main/MainActivity.kt @@ -0,0 +1,107 @@ +package com.example.nikecompose.feature.main + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.example.nikecompose.core.component.NikeBottomBar +import com.example.nikecompose.feature.cart.presentation.CartScreen +import com.example.nikecompose.feature.home.presentation.HomeScreen +import com.example.nikecompose.feature.profile.presentation.ProfileScreen +import com.example.nikecompose.feature.shop.presentation.ShopScreen +import com.example.nikecompose.feature.wishlist.presentation.WishlistScreen +import com.example.nikecompose.navigation.BottomRoute +import com.example.nikecompose.ui.theme.NikeComposeTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + NikeComposeTheme { + NikeComposeApp() + } + } + } +} + +@Composable +fun NikeComposeApp() { + val navController = rememberNavController() + + val bottomItems = listOf( + BottomRoute.Home, + BottomRoute.Shop, + BottomRoute.Wishlist, + BottomRoute.Cart, + BottomRoute.Profile + ) + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + NikeBottomBar( + items = bottomItems, + currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route, + onItemClick = { item -> + navController.navigate(item.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + } + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = BottomRoute.Home.route, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + composable(BottomRoute.Home.route) { + HomeScreen() + } + + composable(BottomRoute.Shop.route) { + ShopScreen() + } + + composable(BottomRoute.Wishlist.route) { + WishlistScreen() + } + + composable(BottomRoute.Cart.route) { + CartScreen( + onOrderClick = { + navController.navigate(BottomRoute.Shop.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + } + + composable(BottomRoute.Profile.route) { + ProfileScreen() + } + } + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/profile/presentation/ProfileScreen.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/profile/presentation/ProfileScreen.kt new file mode 100644 index 00000000..2b85811e --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/profile/presentation/ProfileScreen.kt @@ -0,0 +1,428 @@ +package com.example.nikecompose.feature.profile.presentation + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CardTravel +import androidx.compose.material.icons.outlined.ChevronRight +import androidx.compose.material.icons.outlined.ConfirmationNumber +import androidx.compose.material.icons.outlined.Event +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import coil3.compose.AsyncImage +import com.example.nikecompose.data.dto.response.ReqresUserDto + +@Composable +fun ProfileScreen( + viewModel: ProfileViewModel = viewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ) { + when { + uiState.isLoading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + color = Color.Black + ) + } + + uiState.errorMessage != null -> { + Text( + text = uiState.errorMessage ?: "알 수 없는 오류가 발생했습니다.", + modifier = Modifier.align(Alignment.Center), + color = Color.Red, + textAlign = TextAlign.Center + ) + } + + else -> { + ProfileContent( + profileUser = uiState.profileUser, + followingUsers = uiState.followingUsers + ) + } + } + } +} + +@Composable +fun ProfileContent( + profileUser: ReqresUserDto?, + followingUsers: List +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ) { + Spacer(modifier = Modifier.height(44.dp)) + + ProfileHeader(profileUser = profileUser) + + Spacer(modifier = Modifier.height(28.dp)) + + ProfileMenuRow() + + Spacer(modifier = Modifier.height(20.dp)) + + HorizontalDivider( + thickness = 8.dp, + color = Color(0xFFF6F6F6) + ) + + MemberBenefitSection() + + HorizontalDivider( + thickness = 8.dp, + color = Color(0xFFF6F6F6) + ) + + FollowingSection( + followingUsers = followingUsers + ) + + Spacer(modifier = Modifier.weight(1f)) + + JoinDateSection() + } +} + +@Composable +fun ProfileHeader( + profileUser: ReqresUserDto? +) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AsyncImage( + model = profileUser?.avatar, + contentDescription = "프로필 이미지", + modifier = Modifier + .size(92.dp) + .clip(CircleShape) + .background(Color(0xFFD9D9D9)), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.height(18.dp)) + + Text( + text = if (profileUser != null) { + "${profileUser.firstName} ${profileUser.lastName}" + } else { + "닉네임" + }, + fontSize = 21.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(22.dp)) + + Button( + onClick = { }, + modifier = Modifier + .width(160.dp) + .height(46.dp), + shape = RoundedCornerShape(24.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + border = ButtonDefaults.outlinedButtonBorder + ) { + Text( + text = "프로필 수정", + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + } + } +} + +@Composable +fun ProfileMenuRow() { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + ProfileMenuItem( + icon = Icons.Outlined.CardTravel, + label = "주문" + ) + + VerticalMenuDivider() + + ProfileMenuItem( + icon = Icons.Outlined.ConfirmationNumber, + label = "패스" + ) + + VerticalMenuDivider() + + ProfileMenuItem( + icon = Icons.Outlined.Event, + label = "이벤트" + ) + + VerticalMenuDivider() + + ProfileMenuItem( + icon = Icons.Outlined.Settings, + label = "설정" + ) + } +} + +@Composable +fun ProfileMenuItem( + icon: ImageVector, + label: String +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = icon, + contentDescription = label, + tint = Color(0xFFB5B5B5), + modifier = Modifier.size(24.dp) + ) + + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = label, + fontSize = 11.sp, + color = Color.Black + ) + } +} + +@Composable +fun VerticalMenuDivider() { + Box( + modifier = Modifier + .height(32.dp) + .width(1.dp) + .background(Color(0xFFE0E0E0)) + ) +} + +@Composable +fun MemberBenefitSection() { + Row( + modifier = Modifier + .fillMaxWidth() + .height(84.dp) + .padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = "나이키 멤버 혜택", + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "0개 사용 가능", + fontSize = 12.sp, + color = Color.Gray + ) + } + + Icon( + imageVector = Icons.Outlined.ChevronRight, + contentDescription = "더보기", + tint = Color.Black, + modifier = Modifier.size(22.dp) + ) + } +} + +@Composable +fun FollowingSection( + followingUsers: List +) { + Column( + modifier = Modifier + .fillMaxWidth() + .height(220.dp) + .padding(top = 22.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "팔로잉 (${followingUsers.size})", + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + color = Color.Black, + modifier = Modifier.weight(1f) + ) + + Text( + text = "편집", + fontSize = 12.sp, + color = Color.Gray + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + if (followingUsers.isEmpty()) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "팔로잉 목록이 없습니다.", + fontSize = 13.sp, + color = Color.Gray + ) + } + } else { + FollowingPager(users = followingUsers) + } + } +} + +@Composable +fun FollowingPager( + users: List +) { + val pages = users.chunked(3) + + val pagerState = rememberPagerState( + pageCount = { pages.size } + ) + + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + .height(120.dp), + contentPadding = PaddingValues(horizontal = 24.dp), + pageSpacing = 12.dp + ) { page -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + pages[page].forEach { user -> + FollowingUserCard( + user = user, + modifier = Modifier.weight(1f) + ) + } + + repeat(3 - pages[page].size) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } +} + +@Composable +fun FollowingUserCard( + user: ReqresUserDto, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.Start + ) { + AsyncImage( + model = user.avatar, + contentDescription = "${user.firstName} ${user.lastName}", + modifier = Modifier + .fillMaxWidth() + .height(84.dp) + .background(Color(0xFFD9D9D9)), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = user.firstName, + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = Color.Black, + maxLines = 1 + ) + } +} + +@Composable +fun JoinDateSection() { + Box( + modifier = Modifier + .fillMaxWidth() + .height(42.dp) + .background(Color(0xFFF7F7F7)), + contentAlignment = Alignment.Center + ) { + Text( + text = "회원 가입일: 2025년 9월", + fontSize = 11.sp, + color = Color.Gray + ) + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/profile/presentation/ProfileViewModel.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/profile/presentation/ProfileViewModel.kt new file mode 100644 index 00000000..5223d929 --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/profile/presentation/ProfileViewModel.kt @@ -0,0 +1,50 @@ +package com.example.nikecompose.feature.profile.presentation + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.nikecompose.data.repository.ProfileRepository +import com.example.nikecompose.feature.profile.presentation.model.ProfileUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class ProfileViewModel : ViewModel() { + + private val repository = ProfileRepository() + + private val _uiState = MutableStateFlow(ProfileUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadProfile() + } + + private fun loadProfile() { + viewModelScope.launch { + _uiState.value = _uiState.value.copy( + isLoading = true, + errorMessage = null + ) + + val profileResult = repository.getProfileUser() + val followingResult = repository.getFollowingUsers() + + val profileUser = profileResult.getOrNull() + val followingUsers = followingResult.getOrNull().orEmpty() + + val errorMessage = when { + profileResult.isFailure -> profileResult.exceptionOrNull()?.message + followingResult.isFailure -> followingResult.exceptionOrNull()?.message + else -> null + } + + _uiState.value = ProfileUiState( + isLoading = false, + profileUser = profileUser, + followingUsers = followingUsers, + errorMessage = errorMessage + ) + } + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/profile/presentation/model/ProfileUiState.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/profile/presentation/model/ProfileUiState.kt new file mode 100644 index 00000000..7a61d383 --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/profile/presentation/model/ProfileUiState.kt @@ -0,0 +1,10 @@ +package com.example.nikecompose.feature.profile.presentation.model + +import com.example.nikecompose.data.dto.response.ReqresUserDto + +data class ProfileUiState( + val isLoading: Boolean = false, + val profileUser: ReqresUserDto? = null, + val followingUsers: List = emptyList(), + val errorMessage: String? = null +) \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/shop/presentation/ShopScreen.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/shop/presentation/ShopScreen.kt new file mode 100644 index 00000000..4f60226d --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/shop/presentation/ShopScreen.kt @@ -0,0 +1,74 @@ +package com.example.nikecompose.feature.shop.presentation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.nikecompose.data.dummy.productList +import com.example.nikecompose.feature.shop.presentation.component.ShopProductCard + +@Composable +fun ShopScreen() { + var selectedTabIndex by remember { mutableIntStateOf(0) } + val tabs = listOf("전체", "Tops & T-shirts", "sale") + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ) { + Spacer(modifier = Modifier.height(28.dp)) + + TabRow( + selectedTabIndex = selectedTabIndex, + containerColor = Color.White, + contentColor = Color.Black + ) { + tabs.forEachIndexed { index, title -> + Tab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + text = { + Text( + text = title, + fontSize = 12.sp + ) + } + ) + } + } + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 24.dp, vertical = 24.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(36.dp) + ) { + items( + items = productList, + key = { product -> product.id } + ) { product -> + ShopProductCard(product = product) + } + } + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/shop/presentation/component/ShopProductCard.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/shop/presentation/component/ShopProductCard.kt new file mode 100644 index 00000000..44026764 --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/shop/presentation/component/ShopProductCard.kt @@ -0,0 +1,102 @@ +package com.example.nikecompose.feature.shop.presentation.component + +import androidx.compose.foundation.Image +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.aspectRatio +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.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.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.nikecompose.R +import com.example.nikecompose.domain.model.ProductItem + +@Composable +fun ShopProductCard( + product: ProductItem +) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier.fillMaxWidth() + ) { + Image( + painter = painterResource(id = product.imageResId), + contentDescription = product.name, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .background(Color(0xFFF5F5F5)), + contentScale = ContentScale.Crop + ) + + Icon( + painter = painterResource( + id = if (product.isLiked) R.drawable.ic_heart_on else R.drawable.ic_heart_off + ), + contentDescription = "좋아요", + modifier = Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + .size(22.dp), + tint = Color.Unspecified + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + product.badge?.let { + Text( + text = it, + fontSize = 11.sp, + color = Color(0xFFFF6A00), + fontWeight = FontWeight.Bold + ) + } + + Text( + text = product.name, + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = Color.Black, + lineHeight = 14.sp + ) + + Text( + text = product.subTitle, + fontSize = 10.sp, + color = Color.Gray, + lineHeight = 13.sp + ) + + Text( + text = product.colors, + fontSize = 10.sp, + color = Color.Gray, + lineHeight = 13.sp + ) + + Spacer(modifier = Modifier.height(2.dp)) + + Text( + text = product.price, + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/wishlist/presentation/WishlistScreen.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/wishlist/presentation/WishlistScreen.kt new file mode 100644 index 00000000..51bfc9fa --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/wishlist/presentation/WishlistScreen.kt @@ -0,0 +1,52 @@ +package com.example.nikecompose.feature.wishlist.presentation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.nikecompose.data.dummy.productList +import com.example.nikecompose.feature.wishlist.presentation.component.WishlistProductCard + +@Composable +fun WishlistScreen() { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ) { + Text( + text = "위시리스트", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = Color.Black, + modifier = Modifier.padding(start = 24.dp, top = 40.dp) + ) + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 24.dp, vertical = 28.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(28.dp) + ) { + items( + items = productList, + key = { product -> product.id } + ) { product -> + WishlistProductCard(product = product) + } + } + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/wishlist/presentation/component/WishlistProductCard.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/wishlist/presentation/component/WishlistProductCard.kt new file mode 100644 index 00000000..25d6790e --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/feature/wishlist/presentation/component/WishlistProductCard.kt @@ -0,0 +1,78 @@ +package com.example.nikecompose.feature.wishlist.presentation.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.nikecompose.domain.model.ProductItem + +@Composable +fun WishlistProductCard( + product: ProductItem +) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Image( + painter = painterResource(id = product.imageResId), + contentDescription = product.name, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .background(Color(0xFFF5F5F5)), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = product.name, + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = Color.Black, + lineHeight = 14.sp + ) + + Text( + text = product.price, + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + if (product.id == 2) { + Text( + text = product.subTitle, + fontSize = 10.sp, + color = Color.Gray, + lineHeight = 13.sp + ) + + Text( + text = product.colors, + fontSize = 10.sp, + color = Color.Gray, + lineHeight = 13.sp + ) + + Text( + text = "US$10", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + } + } +} \ No newline at end of file diff --git a/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/navigation/BottomRoute.kt b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/navigation/BottomRoute.kt new file mode 100644 index 00000000..c152efe7 --- /dev/null +++ b/Nyong/week7/nikecompose/app/src/main/java/com/example/nikecompose/navigation/BottomRoute.kt @@ -0,0 +1,16 @@ +package com.example.nikecompose.navigation + +import androidx.annotation.DrawableRes +import com.example.nikecompose.R + +sealed class BottomRoute( + val route: String, + val label: String, + @DrawableRes val iconResId: Int +) { + data object Home : BottomRoute("home", "홈", R.drawable.ic_home) + data object Shop : BottomRoute("shop", "구매하기", R.drawable.ic_buy) + data object Wishlist : BottomRoute("wishlist", "위시리스트", R.drawable.ic_wishlist) + data object Cart : BottomRoute("cart", "장바구니", R.drawable.ic_cart) + data object Profile : BottomRoute("profile", "프로필", R.drawable.ic_profile) +} \ No newline at end of file