8주차 미션_종이#25
Conversation
- Week3 RecyclerView + Adapter 코드를 Compose Lazy 레이아웃으로 교체
- HomeScreen: LazyRow로 수평 상품 목록 구현 (기존 horizontal RecyclerView 대체)
- BuyScreen: LazyVerticalGrid(GridCells.Fixed(2))로 그리드 목록 구현
- WishlistScreen: LazyVerticalGrid로 위시리스트 그리드 구현
- HomeProductItem, GridProductItem Composable로 아이템 레이아웃 작성
- 각 아이템에 key = { it.id } 지정으로 상태 안정성 확보
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HeartButton 공통 컴포저블 추출 (HomeProductItem, GridProductItem 중복 제거) - chunkedBuyProducts, chunkedWishItems 파일 레벨 val로 이동 - BagScreen 장바구니 아이콘 Material Icon → drawable 리소스 교체 - WishlistScreen, ProfileScreen 하드코딩 문자열 strings.xml 분리 - 네비게이션 아이콘 Week3 drawable 리소스 적용 - 앱 아이콘 기본 Android 템플릿으로 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Week 8 과제용으로 Week3 RecyclerView/Adapter 기반 UI를 Jetpack Compose(LazyColumn/LazyRow) 기반으로 마이그레이션한 독립 프로젝트(Week8)를 추가한 PR입니다. 홈/구매/위시리스트/장바구니/프로필 화면을 Compose로 구성하고, Compose Navigation 기반의 하단 탭 구조를 포함합니다.
Changes:
- HomeScreen에
LazyColumn+ 내부LazyRow로 가로 상품 리스트 구성 - Buy/Wishlist 화면에
LazyColumn+Row조합으로 2열 그리드 형태 구성 및 공용 상품 아이템 컴포저블 추가 - Week8 프로젝트 전반의 Gradle/Wrapper/리소스/테마/내비게이션 구성 추가
Reviewed changes
Copilot reviewed 44 out of 60 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| jongyee/Week8/settings.gradle.kts | Week8 프로젝트 설정 및 저장소/플러그인 관리 추가 |
| jongyee/Week8/gradlew.bat | Windows Gradle wrapper 실행 스크립트 추가 |
| jongyee/Week8/gradlew | POSIX Gradle wrapper 실행 스크립트 추가 |
| jongyee/Week8/gradle/wrapper/gradle-wrapper.properties | Gradle wrapper 배포 설정 추가 |
| jongyee/Week8/gradle/libs.versions.toml | 버전 카탈로그(AGP/Kotlin/Compose/Navigation 등) 추가 |
| jongyee/Week8/gradle/gradle-daemon-jvm.properties | Gradle daemon JVM/toolchain 설정 파일 추가 |
| jongyee/Week8/gradle.properties | Gradle/AndroidX 기본 프로퍼티 추가 |
| jongyee/Week8/build.gradle.kts | 루트 빌드 플러그인 alias 구성 추가 |
| jongyee/Week8/app/build.gradle.kts | 앱 모듈 Android/Compose/의존성 설정 추가 |
| jongyee/Week8/app/proguard-rules.pro | ProGuard 규칙 파일 추가 |
| jongyee/Week8/app/.gitignore | 앱 모듈 빌드 산출물 ignore 추가 |
| jongyee/Week8/.gitignore | Week8 프로젝트 공통 ignore 규칙 추가 |
| jongyee/Week8/app/src/main/AndroidManifest.xml | 앱 매니페스트/런처 액티비티 등록 |
| jongyee/Week8/app/src/main/java/com/example/week8/MainActivity.kt | Compose 엔트리포인트(MainActivity) 추가 |
| jongyee/Week8/app/src/main/java/com/example/week8/ui/MainScreen.kt | 하단 탭 + Compose Navigation 기반 메인 화면 추가 |
| jongyee/Week8/app/src/main/java/com/example/week8/navigation/AppDestination.kt | typed destination 정의(Serializable object) 추가 |
| jongyee/Week8/app/src/main/java/com/example/week8/Product.kt | 상품 모델 데이터 클래스 추가 |
| jongyee/Week8/app/src/main/java/com/example/week8/ui/home/HomeScreen.kt | 홈 화면(LazyColumn + LazyRow) 추가 |
| jongyee/Week8/app/src/main/java/com/example/week8/ui/buy/BuyScreen.kt | 구매 화면(탭 + 2열 레이아웃) 추가 |
| jongyee/Week8/app/src/main/java/com/example/week8/ui/wishlist/WishlistScreen.kt | 위시리스트 화면(2열 레이아웃) 추가 |
| jongyee/Week8/app/src/main/java/com/example/week8/ui/bag/BagScreen.kt | 장바구니 빈 상태 UI 및 구매 화면 이동 버튼 추가 |
| jongyee/Week8/app/src/main/java/com/example/week8/ui/profile/ProfileScreen.kt | 프로필 화면 UI 추가 |
| jongyee/Week8/app/src/main/java/com/example/week8/ui/product/ProductItem.kt | 홈/그리드 상품 아이템 및 좋아요 토글 UI 추가 |
| jongyee/Week8/app/src/main/java/com/example/week8/ui/theme/Color.kt | Compose 컬러 팔레트 추가 |
| jongyee/Week8/app/src/main/java/com/example/week8/ui/theme/Theme.kt | Compose Material3 테마 래퍼 추가 |
| jongyee/Week8/app/src/main/res/values/strings.xml | 화면 문구 리소스 추가 |
| jongyee/Week8/app/src/main/res/values/colors.xml | 기본 색상 리소스 추가 |
| jongyee/Week8/app/src/main/res/values/themes.xml | 라이트 테마 리소스 추가 |
| jongyee/Week8/app/src/main/res/values-night/themes.xml | 나이트 테마 리소스 추가 |
| jongyee/Week8/app/src/main/res/xml/backup_rules.xml | 백업 규칙 샘플 파일 추가 |
| jongyee/Week8/app/src/main/res/xml/data_extraction_rules.xml | 데이터 추출 규칙 샘플 파일 추가 |
| jongyee/Week8/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 런처 아이콘 정의 추가 |
| jongyee/Week8/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml | 라운드 런처 아이콘 정의 추가 |
| jongyee/Week8/app/src/main/res/drawable/ic_launcher_background.xml | 런처 아이콘 배경 벡터 추가 |
| jongyee/Week8/app/src/main/res/drawable/ic_launcher_foreground.xml | 런처 아이콘 전경 벡터 추가 |
| jongyee/Week8/app/src/main/res/drawable/ic_heart_filled.xml | 좋아요(채움) 아이콘 추가 |
| jongyee/Week8/app/src/main/res/drawable/ic_heart_empty.xml | 좋아요(비움) 아이콘 추가 |
| jongyee/Week8/app/src/main/res/drawable/home.xml | 하단 탭 홈 아이콘 추가 |
| jongyee/Week8/app/src/main/res/drawable/buy.xml | 하단 탭 구매 아이콘 추가 |
| jongyee/Week8/app/src/main/res/drawable/wishlist.xml | 하단 탭 위시리스트 아이콘 추가 |
| jongyee/Week8/app/src/main/res/drawable/bag.xml | 하단 탭 장바구니 아이콘 추가 |
| jongyee/Week8/app/src/main/res/drawable/profile.xml | 하단 탭 프로필 아이콘 추가 |
| jongyee/Week8/app/src/test/java/com/example/week8/ExampleUnitTest.kt | 기본 유닛 테스트 템플릿 추가 |
| jongyee/Week8/app/src/androidTest/java/com/example/week8/ExampleInstrumentedTest.kt | 기본 계측 테스트 템플릿 추가 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| esac | ||
|
|
||
| CLASSPATH="\\\"\\\"" | ||
|
|
| set -- \ | ||
| "-Dorg.gradle.appname=$APP_BASE_NAME" \ | ||
| -classpath "$CLASSPATH" \ | ||
| -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ | ||
| "$@" |
| :execute | ||
| @rem Setup the command line | ||
|
|
||
| set CLASSPATH= | ||
|
|
||
|
|
||
| @rem Execute Gradle | ||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* | ||
|
|
| @@ -0,0 +1,2 @@ | |||
| # Add project specific ProGuard rules here. | |||
| -keep class com.example.week7.navigation.** { *; } | |||
| @Composable | ||
| private fun HeartButton(isFavorite: Boolean, onToggle: () -> Unit) { | ||
| Image( | ||
| painter = painterResource( | ||
| id = if (isFavorite) R.drawable.ic_heart_filled else R.drawable.ic_heart_empty | ||
| ), | ||
| contentDescription = null, | ||
| modifier = Modifier | ||
| .size(24.dp) | ||
| .clickable { onToggle() } | ||
| ) |
| private val bottomNavItems = listOf( | ||
| BottomNavItem("홈", R.drawable.home, AppDestination.Home), | ||
| BottomNavItem("구매하기", R.drawable.buy, AppDestination.Buy), | ||
| BottomNavItem("위시리스트", R.drawable.wishlist, AppDestination.Wishlist), | ||
| BottomNavItem("장바구니", R.drawable.bag, AppDestination.Bag), | ||
| BottomNavItem("프로필", R.drawable.profile, AppDestination.Profile), | ||
| ) |
| items( | ||
| items = chunkedBuyProducts, | ||
| key = { chunk -> chunk.first().id } | ||
| ) { chunk -> | ||
| Row(modifier = Modifier.fillMaxWidth()) { | ||
| chunk.forEach { product -> | ||
| GridProductItem( | ||
| product = product, | ||
| modifier = Modifier.weight(1f) | ||
| ) | ||
| } |
| items( | ||
| items = chunkedWishItems, | ||
| key = { chunk -> chunk.first().id } | ||
| ) { chunk -> | ||
| Row(modifier = Modifier.fillMaxWidth()) { | ||
| chunk.forEach { product -> | ||
| GridProductItem( | ||
| product = product, | ||
| modifier = Modifier.weight(1f) | ||
| ) | ||
| } |
| <?xml version="1.0" encoding="utf-8"?> | ||
| <resources> | ||
| <style name="Theme.Week8" parent="android:Theme.Material.Light.NoActionBar" /> | ||
| </resources> |
kimdoyeon1234
left a comment
There was a problem hiding this comment.
수고하셨습니다!
@serializable + data object 기반 Type-safe Navigation을 완벽하게 적용하신 점, composable<AppDestination.Home> 형태로 올바르게 사용하신 점 정말 좋았습니다!
BagScreen에서 onNavigateToBuy를 람다로 받아 Events Flow Up 패턴을 잘 적용하신 점도 깔끔합니다
다만 BuyScreen과 WishlistScreen에서 chunked() + LazyColumn + Row 조합으로 2열 그리드를 구현하셨는데, LazyVerticalGrid를 사용하시면 훨씬 간결하게 구현할 수 있고 key도 아이템별로 안정적으로 잡을 수 있습니다!
현재 key가 chunk.first().id로만 잡혀있어서 chunk 내부 두 번째 아이템은 key가 없는 상태입니다! 리스트가 변경될 경우 rememberSaveable로 관리 중인 하트 상태가 엉뚱한 아이템에 붙을 수 있으니 꼭 수정해주세요!
그리고 하트 상태를 GridProductItem, HomeProductItem 내부에서 rememberSaveable로 관리하고 계신데, 상태를 상위로 끌어올려서(State Hoisting) 뷰모델이나 상위 컴포저블에서 관리하는 게 Compose 권장 패턴입니다! 현재 구조에서는 화면을 이탈했다가 돌아오면 하트 상태가 초기화될 수 있습니다!
전반적으로 구조도 깔끔하고 완성도가 높습니다! 수고하셨습니다
| LazyRow( | ||
| contentPadding = PaddingValues(horizontal = 24.dp) | ||
| ) { | ||
| items( | ||
| items = homeProducts, | ||
| key = { product -> product.id } | ||
| ) { product -> | ||
| HomeProductItem(product = product) | ||
| } | ||
| } | ||
| Spacer(Modifier.height(16.dp)) | ||
| } |
There was a problem hiding this comment.
LazyColumn 안에 LazyRow를 중첩하는 건 스크롤 방향이 다르기 때문에 허용됩니다! 같은 방향(세로 안에 세로, 가로 안에 가로)일 때만 크래시가 발생하니 현재 구조는 문제없습니다
| LazyColumn( | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .background(NikeWhite) | ||
| ) { | ||
| item { | ||
| Text( | ||
| text = stringResource(R.string.wishlist_title), | ||
| color = NikeBlack, | ||
| fontSize = 24.sp, | ||
| fontWeight = FontWeight.Bold, | ||
| modifier = Modifier.padding(start = 24.dp, top = 78.dp, bottom = 16.dp) | ||
| ) | ||
| } | ||
| items( | ||
| items = chunkedWishItems, | ||
| key = { chunk -> chunk.first().id } | ||
| ) { chunk -> | ||
| Row(modifier = Modifier.fillMaxWidth()) { | ||
| chunk.forEach { product -> | ||
| GridProductItem( | ||
| product = product, | ||
| modifier = Modifier.weight(1f) | ||
| ) | ||
| } | ||
| if (chunk.size == 1) { | ||
| Spacer(modifier = Modifier.weight(1f)) |
There was a problem hiding this comment.
동작은 하지만 LazyVerticalGrid를 사용하시면 chunked() 없이도 2열 그리드를 구현할 수 있고, 홀수 아이템 처리도 자동으로 됩니다! 아래처럼 변경해보세요!
kotlinLazyVerticalGrid(
columns = GridCells.Fixed(2)
) {
items(
items = buyProducts,
key = { it.id }
) { product ->
GridProductItem(product = product)
}
}
현재 chunked 방식은 key도 chunk.first().id로만 잡혀있어서 내부 아이템들이 개별적으로 키를 가지지 못한다는 문제도 있습니다! LazyVerticalGrid로 바꾸시면 이 문제도 함께 해결됩니다!
There was a problem hiding this comment.
isFavorite은 UI 상태이기 때문에 데이터 모델인 Product에 포함시키기보다는 별도의 UI State 클래스로 분리하는 게 더 좋습니다! 예를 들어 data class ProductUiState(val product: Product, val isFavorite: Boolean)처럼 분리해보세요!
| @Composable | ||
| fun HomeProductItem(product: Product) { | ||
| var isFavorite by rememberSaveable { mutableStateOf(product.isFavorite) } | ||
|
|
||
| Column( | ||
| modifier = Modifier | ||
| .width(180.dp) | ||
| .padding(end = 12.dp) |
There was a problem hiding this comment.
현재 하트 상태를 GridProductItem, HomeProductItem 내부에서 rememberSaveable로 관리하고 계신데, 상태를 상위로 끌어올려서(State Hoisting) 뷰모델이나 상위 컴포저블에서 관리하는 게 Compose 권장 패턴입니다! 현재 구조에서는 화면을 이탈했다가 돌아오면 하트 상태가 초기화될 수 있습니다!
There was a problem hiding this comment.
GridProductItem 내부에서 rememberSaveable { mutableStateOf(product.isFavorite) }로 하트 상태를 관리하고 계신데, LazyColumn의 key가 chunk.first().id로만 잡혀있어서 chunk 내부 두 번째 아이템은 key가 없는 상태입니다! 리스트가 변경될 경우 하트 상태가 엉뚱한 아이템에 붙을 수 있습니다! LazyVerticalGrid로 변경하시면 아이템별로 key가 안정적으로 잡혀서 이 문제도 함께 해결됩니다!
📝 작업 내용
📸 스크린샷
🙏 리뷰 요구사항 (선택)