diff --git a/Yoshi/NikeApp/.claude/settings.local.json b/Yoshi/NikeApp/.claude/settings.local.json index b6c5da4..199d94e 100644 --- a/Yoshi/NikeApp/.claude/settings.local.json +++ b/Yoshi/NikeApp/.claude/settings.local.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "PowerShell(& \"C:\\\\Users\\\\doyeo\\\\AndroidStudioProjects\\\\NikeApp\\\\gradlew.bat\" assembleDebug 2>&1)" + "PowerShell(& \"C:\\\\Users\\\\doyeo\\\\AndroidStudioProjects\\\\NikeApp\\\\gradlew.bat\" assembleDebug 2>&1)", + "Bash(./gradlew.bat :app:compileDebugKotlin --console=plain)" ] } } diff --git a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/data/WishlistRepository.kt b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/data/WishlistRepository.kt new file mode 100644 index 0000000..a5b61d4 --- /dev/null +++ b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/data/WishlistRepository.kt @@ -0,0 +1,46 @@ +package com.example.NikeApp.data + +import android.content.Context +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf + +/** + * 위시리스트 ID 집합 저장소 + * 앱이 종료/재실행되어도 데이터가 유지 + * Compose에서 즉시 반응할 수 있게 [State] 사용 + */ +class WishlistRepository(context: Context) { + + private val prefs = context.applicationContext + .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + private val _wishlistIds = mutableStateOf>(loadFromPrefs()) + + /** 현재 위시리스트에 담긴 상품 ID 집합 */ + val wishlistIds: State> = _wishlistIds + + /** + * 하트 on/off + * 이미 들어있으면 제거 없으면 추가 + */ + fun toggle(productId: String) { + val current = _wishlistIds.value + val updated = if (productId in current) current - productId else current + productId + _wishlistIds.value = updated + saveToPrefs(updated) + } + + fun isWished(productId: String): Boolean = productId in _wishlistIds.value + + private fun loadFromPrefs(): Set = + prefs.getStringSet(KEY_IDS, emptySet())?.toSet() ?: emptySet() + + private fun saveToPrefs(ids: Set) { + prefs.edit().putStringSet(KEY_IDS, ids).apply() + } + + companion object { + private const val PREFS_NAME = "wishlist_prefs" + private const val KEY_IDS = "wishlist_ids" + } +} diff --git a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/model/Product.kt b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/model/Product.kt new file mode 100644 index 0000000..1d8be1c --- /dev/null +++ b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/model/Product.kt @@ -0,0 +1,13 @@ +package com.example.NikeApp.model + +import androidx.annotation.DrawableRes + +/** + * 상품 데이터 모델 + */ +data class Product( + val id: String, + val name: String, + val price: Int, + @DrawableRes val imageRes: Int? = null, +) diff --git a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/model/SampleProducts.kt b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/model/SampleProducts.kt new file mode 100644 index 0000000..d72a74f --- /dev/null +++ b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/model/SampleProducts.kt @@ -0,0 +1,22 @@ +package com.example.NikeApp.model + +import com.example.NikeApp.R + +/** + * 홈/구매하기 화면에서 사용하는 더미 상품 목록 + * 홈 화면: 5개 + * 구매하기: 8개 + */ +val SampleProducts: List = listOf( + Product(id = "p1", name = "Air Force 1 '07", price = 150, imageRes = R.drawable.air_force_1_07), + Product(id = "p2", name = "Air Max 90", price = 200, imageRes = R.drawable.air_max_90), + Product(id = "p3", name = "Dunk Low Retro", price = 175, imageRes = R.drawable.dunk_low_retro), + Product(id = "p4", name = "Air Jordan 1 Mid", price = 220, imageRes = R.drawable.air_jordan_1_mid), + Product(id = "p5", name = "Cortez Basic", price = 100, imageRes = R.drawable.cortez_basic), + Product(id = "p6", name = "Pegasus 41", price = 130, imageRes = R.drawable.pegasus_41), + Product(id = "p7", name = "Blazer Mid '77", price = 90, imageRes = R.drawable.blazer_mid_77), + Product(id = "p8", name = "Sportswear Crew Socks", price = 15, imageRes = R.drawable.sportswear_crew_socks), +) + +/** 가격 형식 변환 */ +fun Product.formattedPrice(): String = "US$%,d".format(price) diff --git a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/NikeApp.kt b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/NikeApp.kt index 99384de..a6bd6de 100644 --- a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/NikeApp.kt +++ b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/NikeApp.kt @@ -4,13 +4,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import com.example.NikeApp.data.WishlistRepository import com.example.NikeApp.ui.component.NikeBottomBar import com.example.NikeApp.ui.navigation.AppDestination import com.example.NikeApp.ui.navigation.BottomTab @@ -26,11 +29,16 @@ import com.example.NikeApp.ui.screen.WishlistScreen * NavController는 화면 이동과 뒤로 가기를 담당 * Scaffold는 BottomBar와 본문을 분리하여 배치 * NavHost는 실제 화면이 교체되며 그려지는 컨테이너 + * WishlistRepository는 Activity 스코프로 1회만 생성하여 모든 화면이 동일한 위시리스트 상태를 공유 */ @Composable fun NikeApp() { val navController = rememberNavController() + // applicationContext 기반으로 1회만 생성 → 모든 화면이 동일 인스턴스 공유 + val context = LocalContext.current + val wishlistRepository = remember(context) { WishlistRepository(context) } + // 현재 어떤 화면이 보여지고 있는지 NavController에서 관찰 → BottomBar 선택 상태로 변환 val backStackEntry by navController.currentBackStackEntryAsState() val currentTab: BottomTab = backStackEntry?.destination.toBottomTab() ?: BottomTab.Home @@ -48,18 +56,36 @@ fun NikeApp() { startDestination = AppDestination.Home, modifier = Modifier.padding(innerPadding), ) { - mainGraph(onNavigateToPurchase = { navController.navigateToTab(BottomTab.Purchase) }) + mainGraph( + wishlistRepository = wishlistRepository, + onNavigateToPurchase = { navController.navigateToTab(BottomTab.Purchase) }, + ) } } } -/** NavGraph 정의를 확장 함수로 분리 */ +/** + * NavGraph 정의를 확장 함수로 분리함 + */ private fun NavGraphBuilder.mainGraph( + wishlistRepository: WishlistRepository, onNavigateToPurchase: () -> Unit, ) { composable { HomeScreen() } - composable { PurchaseScreen() } - composable { WishlistScreen() } + composable { + val wishlistIds by wishlistRepository.wishlistIds + PurchaseScreen( + wishlistIds = wishlistIds, + onToggleWishlist = wishlistRepository::toggle, + ) + } + composable { + val wishlistIds by wishlistRepository.wishlistIds + WishlistScreen( + wishlistIds = wishlistIds, + onToggleWishlist = wishlistRepository::toggle, + ) + } composable { // 장바구니 → 구매하기 CartScreen(onOrderClick = onNavigateToPurchase) diff --git a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/component/ProductCard.kt b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/component/ProductCard.kt new file mode 100644 index 0000000..ac845c3 --- /dev/null +++ b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/component/ProductCard.kt @@ -0,0 +1,130 @@ +package com.example.NikeApp.ui.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.aspectRatio +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.shape.RoundedCornerShape +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.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.NikeApp.R +import com.example.NikeApp.model.Product +import com.example.NikeApp.model.formattedPrice +import com.example.NikeApp.ui.theme.NikeBlack +import com.example.NikeApp.ui.theme.NikeGray +import com.example.NikeApp.ui.theme.NikeLightGray + +// 하트 ON 색상 +private val HeartOnColor = Color(0xFFE61E2B) + +/** + * 홈/구매하기/위시리스트 화면에서 공통으로 사용하는 상품 카드 + * [showWishButton]이 true이면 우상단에 하트 버튼 노출 + */ +@Composable +fun ProductCard( + product: Product, + modifier: Modifier = Modifier, + showWishButton: Boolean = false, + isWished: Boolean = false, + onWishClick: (() -> Unit)? = null, +) { + Column(modifier = modifier) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(8.dp)) + .background(NikeLightGray), + contentAlignment = Alignment.Center, + ) { + // 실제 사진이 등록된 상품은 실사진으로 아니면 작은 더미 아이콘으로 표시됨 + if (product.imageRes != null) { + Image( + painter = painterResource(id = product.imageRes), + contentDescription = product.name, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } else { + Image( + painter = painterResource(id = android.R.drawable.ic_menu_gallery), + contentDescription = product.name, + contentScale = ContentScale.Fit, + modifier = Modifier.size(80.dp), + ) + } + + if (showWishButton) { + WishHeartButton( + isWished = isWished, + onClick = { onWishClick?.invoke() }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(8.dp), + ) + } + } + + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = product.name, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + color = NikeBlack, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = product.formattedPrice(), + fontSize = 13.sp, + color = NikeGray, + ) + } +} + +/** + * 상품 카드 우상단의 하트 토글 버튼. + */ +@Composable +private fun WishHeartButton( + isWished: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(32.dp) + .clip(RoundedCornerShape(50)) + .background(Color.White.copy(alpha = 0.85f)) + .clickable(onClick = onClick) + .padding(PaddingValues(6.dp)), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource( + id = if (isWished) R.drawable.ic_heart_on else R.drawable.ic_heart_off, + ), + contentDescription = if (isWished) "위시리스트에서 제거" else "위시리스트에 추가", + tint = if (isWished) HeartOnColor else NikeGray, + ) + } +} diff --git a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/navigation/AppDestination.kt b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/navigation/AppDestination.kt index 7a75127..da0bf02 100644 --- a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/navigation/AppDestination.kt +++ b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/navigation/AppDestination.kt @@ -41,7 +41,7 @@ enum class BottomTab( Profile(AppDestination.Profile, R.drawable.profile_menu), } -/** 현재 보여지는 NavDestination이 어떤 탭에 해당하는지 매칭. */ +/** 현재 보여지는 NavDestination이 어떤 탭에 해당하는지 매칭 */ fun NavDestination?.toBottomTab(): BottomTab? = BottomTab.entries.firstOrNull { tab -> this?.hasRoute(tab.route::class) == true diff --git a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/HomeScreen.kt b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/HomeScreen.kt index 856873b..7d56378 100644 --- a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/HomeScreen.kt +++ b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/HomeScreen.kt @@ -1,15 +1,21 @@ package com.example.NikeApp.ui.screen import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement +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.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource @@ -18,39 +24,86 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.NikeApp.R +import com.example.NikeApp.model.SampleProducts +import com.example.NikeApp.ui.component.ProductCard import com.example.NikeApp.ui.theme.NikeAppTheme import com.example.NikeApp.ui.theme.NikeBlack import com.example.NikeApp.ui.theme.NikeGray +/** + * 홈 화면 + */ @Composable fun HomeScreen(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 20.dp, vertical = 16.dp), + // 홈 화면에 5개의 상품만 보여주기 + val homeProducts = remember { SampleProducts.take(5) } + + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { - Text( - text = "Discover", - fontSize = 32.sp, - fontWeight = FontWeight.Bold, - color = NikeBlack, - ) - Text( - text = "9월 4일 목요일", - style = MaterialTheme.typography.bodyMedium, - color = NikeGray, - modifier = Modifier.padding(top = 4.dp), - ) + item { + Text( + text = "Discover", + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + color = NikeBlack, + modifier = Modifier.padding(horizontal = 20.dp), + ) + } + item { + Text( + text = "9월 4일 목요일", + style = MaterialTheme.typography.bodyMedium, + color = NikeGray, + modifier = Modifier.padding(horizontal = 20.dp), + ) + } + item { + Image( + painter = painterResource(id = R.drawable.home_img), + contentDescription = "홈 메인 이미지", + contentScale = ContentScale.FillWidth, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) + } + item { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "What's new", + fontSize = 13.sp, + color = NikeGray, + modifier = Modifier.padding(horizontal = 20.dp), + ) + Text( + text = "나이키 최신 상품", + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + color = NikeBlack, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 2.dp), + ) + } - Image( - painter = painterResource(id = R.drawable.home_img), - contentDescription = "홈 메인 이미지", - contentScale = ContentScale.FillWidth, - modifier = Modifier - .padding(top = 20.dp) - .fillMaxWidth(), - ) + // LazyRow를 사용해 수평으로 스크롤 + item { + LazyRow( + contentPadding = PaddingValues(horizontal = 20.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + items = homeProducts, + key = { product -> product.id }, + ) { product -> + ProductCard( + product = product, + modifier = Modifier.width(280.dp), + ) + } + } + } } } diff --git a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/PurchaseScreen.kt b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/PurchaseScreen.kt index 25f0d41..ff9c5a8 100644 --- a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/PurchaseScreen.kt +++ b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/PurchaseScreen.kt @@ -2,13 +2,17 @@ package com.example.NikeApp.ui.screen import androidx.compose.foundation.background import androidx.compose.foundation.clickable +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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -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.runtime.getValue @@ -22,22 +26,29 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.example.NikeApp.model.SampleProducts +import com.example.NikeApp.ui.component.ProductCard import com.example.NikeApp.ui.theme.NikeAppTheme import com.example.NikeApp.ui.theme.NikeBlack import com.example.NikeApp.ui.theme.NikeGray import com.example.NikeApp.ui.theme.NikeLightGray /** - * 구매하기 화면. + * 구매하기 화면 + * - 전체 / Tops & T-shirts / Sale 탭 구성 */ private enum class PurchaseTab(val label: String) { All("전체"), Tops("Tops & T-shirts"), - Shoes("Shoes"), + Sale("Sale"), } @Composable -fun PurchaseScreen(modifier: Modifier = Modifier) { +fun PurchaseScreen( + wishlistIds: Set, + onToggleWishlist: (String) -> Unit, + modifier: Modifier = Modifier, +) { var selectedTab by remember { mutableStateOf(PurchaseTab.All) } Column(modifier = modifier.fillMaxSize()) { @@ -46,8 +57,60 @@ fun PurchaseScreen(modifier: Modifier = Modifier) { onTabSelected = { selectedTab = it }, ) - // 본문은 빈 화면 - Box(modifier = Modifier.fillMaxSize()) + when (selectedTab) { + PurchaseTab.All -> AllProductsGrid( + wishlistIds = wishlistIds, + onToggleWishlist = onToggleWishlist, + ) + PurchaseTab.Tops, PurchaseTab.Sale -> EmptyTabPlaceholder() + } + } +} + +/** + * '전체' 탭 — LazyVerticalGrid 로 2열 × 4행 구성 + * items( ) 로 안정적인 키 부여 + * 각 상품 카드 우상단의 하트 버튼으로 위시리스트에 추가 및 제거 + */ +@Composable +private fun AllProductsGrid( + wishlistIds: Set, + onToggleWishlist: (String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 20.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + items = SampleProducts, + key = { product -> product.id }, + ) { product -> + ProductCard( + product = product, + modifier = Modifier.fillMaxWidth(), + showWishButton = true, + isWished = product.id in wishlistIds, + onWishClick = { onToggleWishlist(product.id) }, + ) + } + } +} + +@Composable +private fun EmptyTabPlaceholder(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = " ", + fontSize = 14.sp, + color = NikeGray, + ) } } @@ -113,5 +176,10 @@ private fun PurchaseTabItem( @Preview(showBackground = true) @Composable private fun PurchaseScreenPreview() { - NikeAppTheme { PurchaseScreen() } + NikeAppTheme { + PurchaseScreen( + wishlistIds = emptySet(), + onToggleWishlist = {}, + ) + } } diff --git a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/WishlistScreen.kt b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/WishlistScreen.kt index 71f83e8..b0eb9d6 100644 --- a/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/WishlistScreen.kt +++ b/Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/screen/WishlistScreen.kt @@ -1,30 +1,93 @@ package com.example.NikeApp.ui.screen -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.example.NikeApp.model.SampleProducts +import com.example.NikeApp.ui.component.ProductCard import com.example.NikeApp.ui.theme.NikeAppTheme import com.example.NikeApp.ui.theme.NikeBlack +import com.example.NikeApp.ui.theme.NikeGray +/** + * 위시리스트 화면 + * 전체 상품 중 [wishlistIds] 에 포함된 것만 LazyVerticalGrid 로 2 x N 형태로 표시 + * 카드의 하트를 다시 누르면 위시리스트에서 제거 + */ @Composable -fun WishlistScreen(modifier: Modifier = Modifier) { - Column( +fun WishlistScreen( + wishlistIds: Set, + onToggleWishlist: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val wishedProducts = SampleProducts.filter { it.id in wishlistIds } + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 20.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + // 제목은 전체 열을 차지하도록 span 지정 + item(span = { GridItemSpan(maxLineSpan) }) { + Text( + text = "위시리스트", + fontSize = 28.sp, + fontWeight = FontWeight.Bold, + color = NikeBlack, + ) + } + + if (wishedProducts.isEmpty()) { + // 빈 상태 메시지도 전체 행 차지 + item(span = { GridItemSpan(maxLineSpan) }) { + EmptyWishlistMessage() + } + } else { + items( + items = wishedProducts, + key = { product -> product.id }, + ) { product -> + ProductCard( + product = product, + modifier = Modifier.fillMaxWidth(), + showWishButton = true, + isWished = true, + onWishClick = { onToggleWishlist(product.id) }, + ) + } + } + } +} + +@Composable +private fun EmptyWishlistMessage(modifier: Modifier = Modifier) { + Box( modifier = modifier - .fillMaxSize() - .padding(horizontal = 20.dp, vertical = 16.dp), + .fillMaxWidth() + .height(240.dp), + contentAlignment = Alignment.Center, ) { Text( - text = "위시리스트", - fontSize = 28.sp, - fontWeight = FontWeight.Bold, - color = NikeBlack, + text = "아직 위시한 상품이 없습니다", + fontSize = 14.sp, + color = NikeGray, ) } } @@ -32,5 +95,10 @@ fun WishlistScreen(modifier: Modifier = Modifier) { @Preview(showBackground = true) @Composable private fun WishlistScreenPreview() { - NikeAppTheme { WishlistScreen() } + NikeAppTheme { + WishlistScreen( + wishlistIds = setOf("p1", "p3"), + onToggleWishlist = {}, + ) + } } diff --git a/Yoshi/NikeApp/app/src/main/res/drawable/air_force_1_07.png b/Yoshi/NikeApp/app/src/main/res/drawable/air_force_1_07.png new file mode 100644 index 0000000..b51811c Binary files /dev/null and b/Yoshi/NikeApp/app/src/main/res/drawable/air_force_1_07.png differ diff --git a/Yoshi/NikeApp/app/src/main/res/drawable/air_jordan_1_mid.png b/Yoshi/NikeApp/app/src/main/res/drawable/air_jordan_1_mid.png new file mode 100644 index 0000000..5d2f191 Binary files /dev/null and b/Yoshi/NikeApp/app/src/main/res/drawable/air_jordan_1_mid.png differ diff --git a/Yoshi/NikeApp/app/src/main/res/drawable/air_max_90.png b/Yoshi/NikeApp/app/src/main/res/drawable/air_max_90.png new file mode 100644 index 0000000..2dac1e4 Binary files /dev/null and b/Yoshi/NikeApp/app/src/main/res/drawable/air_max_90.png differ diff --git a/Yoshi/NikeApp/app/src/main/res/drawable/blazer_mid_77.png b/Yoshi/NikeApp/app/src/main/res/drawable/blazer_mid_77.png new file mode 100644 index 0000000..53b1873 Binary files /dev/null and b/Yoshi/NikeApp/app/src/main/res/drawable/blazer_mid_77.png differ diff --git a/Yoshi/NikeApp/app/src/main/res/drawable/cortez_basic.png b/Yoshi/NikeApp/app/src/main/res/drawable/cortez_basic.png new file mode 100644 index 0000000..dca7664 Binary files /dev/null and b/Yoshi/NikeApp/app/src/main/res/drawable/cortez_basic.png differ diff --git a/Yoshi/NikeApp/app/src/main/res/drawable/dunk_low_retro.png b/Yoshi/NikeApp/app/src/main/res/drawable/dunk_low_retro.png new file mode 100644 index 0000000..4d36217 Binary files /dev/null and b/Yoshi/NikeApp/app/src/main/res/drawable/dunk_low_retro.png differ diff --git a/Yoshi/NikeApp/app/src/main/res/drawable/pegasus_41.png b/Yoshi/NikeApp/app/src/main/res/drawable/pegasus_41.png new file mode 100644 index 0000000..991f2db Binary files /dev/null and b/Yoshi/NikeApp/app/src/main/res/drawable/pegasus_41.png differ diff --git a/Yoshi/NikeApp/app/src/main/res/drawable/sportswear_crew_socks.png b/Yoshi/NikeApp/app/src/main/res/drawable/sportswear_crew_socks.png new file mode 100644 index 0000000..75d2511 Binary files /dev/null and b/Yoshi/NikeApp/app/src/main/res/drawable/sportswear_crew_socks.png differ