Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Yoshi/NikeApp/.claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -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)"
]
}
}
Original file line number Diff line number Diff line change
@@ -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<Set<String>>(loadFromPrefs())

/** 현재 위시리스트에 담긴 상품 ID 집합 */
val wishlistIds: State<Set<String>> = _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<String> =
prefs.getStringSet(KEY_IDS, emptySet())?.toSet() ?: emptySet()

private fun saveToPrefs(ids: Set<String>) {
prefs.edit().putStringSet(KEY_IDS, ids).apply()
}

companion object {
private const val PREFS_NAME = "wishlist_prefs"
private const val KEY_IDS = "wishlist_ids"
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.NikeApp.model

import com.example.NikeApp.R

/**
* 홈/구매하기 화면에서 사용하는 더미 상품 목록
* 홈 화면: 5개
* 구매하기: 8개
*/
val SampleProducts: List<Product> = 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)
34 changes: 30 additions & 4 deletions Yoshi/NikeApp/app/src/main/java/com/example/NikeApp/ui/NikeApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<AppDestination.Home> { HomeScreen() }
composable<AppDestination.Purchase> { PurchaseScreen() }
composable<AppDestination.Wishlist> { WishlistScreen() }
composable<AppDestination.Purchase> {
val wishlistIds by wishlistRepository.wishlistIds
PurchaseScreen(
wishlistIds = wishlistIds,
onToggleWishlist = wishlistRepository::toggle,
)
}
composable<AppDestination.Wishlist> {
val wishlistIds by wishlistRepository.wishlistIds
WishlistScreen(
wishlistIds = wishlistIds,
onToggleWishlist = wishlistRepository::toggle,
)
}
composable<AppDestination.Cart> {
// 장바구니 → 구매하기
CartScreen(onOrderClick = onNavigateToPurchase)
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading