diff --git a/README.md b/README.md deleted file mode 100644 index 3c1d120..0000000 --- a/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# 10th_Android - - -

팀원 구성

-
- - - - - - - - - - - - - - - - -
도리/ 김도연
(파트장)
제로/ 정규은
(챌린저)
레몬/ 김지아
(챌린저)
우가/ 강수아
(챌린저)
묵은지/ 임은지
(챌린저)
-
- @kimdoyeon1234 -
-
- @jeongkyueun -
-
- @Lemon0610 -
-
- @sua710 -
-
- @mookeunji05 -
- -
- -### Git 작업 순서 - -본인의 브랜치에서만 작업하고, `main` 브랜치는 직접 수정하지 않습니다. - -#### 1. 현재 브랜치 확인 -```sh -git branch -``` - -#### 2. 변경 사항 확인 -```sh -git status -``` - -#### 3. 변경된 파일 추가 -```sh -git add . -``` - -#### 4. 커밋 메시지 작성 -```sh -git commit -m "Commit Message" -``` - -#### 5. 원격 저장소에 본인 브랜치로 푸시 -```sh -git push origin <브랜치명> -``` - -### Commit Convention - -커밋 메시지는 `타입: n주차 미션 설명`의 형식을 갖추어 작성합니다. - -| 타입 | 설명 | -|-----------|--------------------------------| -| feat | 새로운 기능 추가 | -| fix | 버그 수정 | -| refactor | 코드 리팩토링 | -| docs | 문서 수정 (README 등) | -| style | 코드 스타일 변경 (세미콜론 추가 등)| -| chore | 빌드 및 패키지 설정 변경 | -| test | 테스트 코드 추가 | - -#### Commit Example -```sh -git commit -m "feat: 1주차 미션 화면 전환 기능" -git commit -m "fix: 5주차 미션 API 응답 오류 수정" -``` - -### PR Convention - -- Pull Request(PR)은 미션 별로 생성합니다. -- PR 제목은 `n주차 미션` 형식으로 작성합니다. -- 파트장이 승인한 후, main 브랜치로 Merge 합니다. diff --git a/Week7/.gitignore b/Week7/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/Week7/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/Week7/.idea/.gitignore b/Week7/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/Week7/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/Week7/.idea/.name b/Week7/.idea/.name new file mode 100644 index 0000000..b3405b3 --- /dev/null +++ b/Week7/.idea/.name @@ -0,0 +1 @@ +My Application \ No newline at end of file diff --git a/Week7/.idea/AndroidProjectSystem.xml b/Week7/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/Week7/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Week7/.idea/compiler.xml b/Week7/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/Week7/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Week7/.idea/deploymentTargetSelector.xml b/Week7/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/Week7/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/Week7/.idea/gradle.xml b/Week7/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/Week7/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/Week7/.idea/inspectionProfiles/Project_Default.xml b/Week7/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..f0c6ad0 --- /dev/null +++ b/Week7/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,50 @@ + + + + \ No newline at end of file diff --git a/Week7/.idea/migrations.xml b/Week7/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/Week7/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/Week7/.idea/misc.xml b/Week7/.idea/misc.xml new file mode 100644 index 0000000..c902d2f --- /dev/null +++ b/Week7/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/Week7/.idea/runConfigurations.xml b/Week7/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/Week7/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/Week7/.idea/vcs.xml b/Week7/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/Week7/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Week7/app/.gitignore b/Week7/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/Week7/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Week7/app/build.gradle.kts b/Week7/app/build.gradle.kts new file mode 100644 index 0000000..f72cea4 --- /dev/null +++ b/Week7/app/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.example.myapplication" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.myapplication" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + vectorDrawables.useSupportLibrary = true + } + + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + compose = true + } + + composeOptions { + + kotlinCompilerExtensionVersion = "1.5.8" + } +} + +dependencies { + implementation("androidx.compose.foundation:foundation") + + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("androidx.activity:activity-compose:1.9.0") + + + implementation(platform("androidx.compose:compose-bom:2024.05.00")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material3:material3") + + + implementation("androidx.compose.material:material-icons-extended") + + implementation("androidx.compose.ui:ui-tooling-preview") + debugImplementation("androidx.compose.ui:ui-tooling") + + implementation("com.google.android.material:material:1.11.0") + + implementation("io.coil-kt:coil-compose:2.6.0") + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") +} \ No newline at end of file diff --git a/Week7/app/proguard-rules.pro b/Week7/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/Week7/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/Week7/app/release/app-release.aab b/Week7/app/release/app-release.aab new file mode 100644 index 0000000..9b2d1b9 Binary files /dev/null and b/Week7/app/release/app-release.aab differ diff --git a/Week7/app/src/androidTest/java/com/example/myapplication/ExampleInstrumentedTest.kt b/Week7/app/src/androidTest/java/com/example/myapplication/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..e9283cf --- /dev/null +++ b/Week7/app/src/androidTest/java/com/example/myapplication/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.myapplication + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.myapplication", appContext.packageName) + } +} \ No newline at end of file diff --git a/Week7/app/src/main/AndroidManifest.xml b/Week7/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bfd376b --- /dev/null +++ b/Week7/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Week7/app/src/main/java/com/example/myapplication/MainActivity.kt b/Week7/app/src/main/java/com/example/myapplication/MainActivity.kt new file mode 100644 index 0000000..6ecfe69 --- /dev/null +++ b/Week7/app/src/main/java/com/example/myapplication/MainActivity.kt @@ -0,0 +1,466 @@ +package com.example.myapplication + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.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.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +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 coil.compose.AsyncImage +import com.example.myapplication.model.Product +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MainScreen() + } + } +} + +@Composable +fun MainScreen() { + var selectedTab by remember { mutableStateOf("홈") } + val wishlistedIds = remember { mutableStateListOf() } + + Scaffold( + bottomBar = { + NavigationBar { + val items = listOf("홈", "구매하기", "위시리스트", "장바구니", "프로필") + val icons = listOf( + Icons.Default.Home, + Icons.Default.Store, + Icons.Default.Favorite, + Icons.Default.ShoppingCart, + Icons.Default.Person + ) + + items.forEachIndexed { index, item -> + NavigationBarItem( + selected = selectedTab == item, + onClick = { selectedTab = item }, + icon = { Icon(imageVector = icons[index], contentDescription = item) }, + label = { Text(item) } + ) + } + } + } + ) { innerPadding -> + Surface(modifier = Modifier.padding(innerPadding)) { + when (selectedTab) { + "홈" -> HomeScreen() + + "구매하기" -> StoreScreen( + wishlistedIds = wishlistedIds, + onHeartToggle = { id -> + if (wishlistedIds.contains(id)) { + wishlistedIds.remove(id) + } else { + wishlistedIds.add(id) + } + } + ) + + "위시리스트" -> WishlistScreen( + wishlistedIds = wishlistedIds, + onHeartToggle = { id -> + if (wishlistedIds.contains(id)) { + wishlistedIds.remove(id) + } + } + ) + + "장바구니" -> CartScreen(onOrderClick = { selectedTab = "구매하기" }) + + "프로필" -> ProfileScreen() + + else -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("${selectedTab} 화면") + } + } + } + } + } +} + +@Composable +fun HomeScreen() { + val jordanList = listOf( + Product(101, "AirJordan_1", R.drawable.jordan_1, "₩219,000", "Jordan"), + Product(102, "Airjordan_2", R.drawable.jordan_2, "₩199,000", "Jordan"), + Product(103, "AirJordan_3", R.drawable.jordan_3, "₩209,000", "Jordan") + ) + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = 24.dp) + ) { + item { + Image( + painter = painterResource(id = R.drawable.home_logo), + contentDescription = "홈 브랜드 로고", + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + contentScale = ContentScale.FillWidth + ) + } + + item { + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 24.dp) + ) { + Text(text = "What's new", color = Color.Black, fontSize = 18.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(4.dp)) + Text(text = "나이키 최신 상품", color = Color.Gray, fontSize = 22.sp, fontWeight = FontWeight.ExtraBold) + } + } + + item { + LazyRow( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 20.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(items = jordanList, key = { it.id }) { jordan -> + JordanHorizontalItem(product = jordan) + } + } + } + } +} + + +@Composable +fun JordanHorizontalItem(product: Product) { + Column( + modifier = Modifier.width(180.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Card( + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFFF5F5F5)), + modifier = Modifier.fillMaxWidth().height(180.dp) + ) { + Image( + painter = painterResource(id = product.imageRes), + contentDescription = product.name, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Text(text = product.name, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, color = Color.Black) + Text(text = product.price, fontSize = 13.sp, color = Color.DarkGray, fontWeight = FontWeight.Medium) + } +} + +@Composable +fun WishlistScreen( + wishlistedIds: List, + onHeartToggle: (Int) -> Unit +) { + val wishlistedProducts = sampleStoreProducts.filter { wishlistedIds.contains(it.id) } + + Column(modifier = Modifier.fillMaxSize()) { + Box(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), contentAlignment = Alignment.Center) { + Text(text = "My Wishlist", fontWeight = FontWeight.Bold, fontSize = 18.sp, color = Color.Black) + } + HorizontalDivider(thickness = 1.dp, color = Color.LightGray) + + if (wishlistedProducts.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.Default.FavoriteBorder, + contentDescription = "Empty Wishlist", + tint = Color.Gray, + modifier = Modifier.size(64.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text(text = "위시리스트가 비어있습니다.", fontWeight = FontWeight.Bold, color = Color.Black) + Text(text = "구매하기에서 하트를 눌러보세요!", color = Color.Gray, fontSize = 14.sp) + } + } + } else { + val chunkedWishlist = wishlistedProducts.chunked(2) + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + items( + items = chunkedWishlist, + key = { row -> row.map { it.id }.joinToString("-") } + ) { rowItems -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + for (product in rowItems) { + Box(modifier = Modifier.weight(1f)) { + StoreProductItem( + product = product, + isLiked = true, + onHeartClick = { onHeartToggle(product.id) } + ) + } + } + if (rowItems.size < 2) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } + } + } +} + + +@Composable +fun CartScreen(onOrderClick: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(40.dp)) + Icon( + painter = painterResource(id = R.drawable.ic_cartcircle), + contentDescription = "장바구니 아이콘", + modifier = Modifier.size(100.dp), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(24.dp)) + Text(text = "장바구니가 비어있습니다.", fontWeight = FontWeight.Bold) + Text(text = "제품을 추가하면 여기에 표시됩니다.", color = Color.Gray) + Spacer(modifier = Modifier.weight(1f)) + Surface( + modifier = Modifier.fillMaxWidth().height(56.dp).clickable { onOrderClick() }, + shape = RoundedCornerShape(28.dp), + color = Color.Black + ) { + Box(contentAlignment = Alignment.Center) { + Text(text = "주문하기", color = Color.White, fontWeight = FontWeight.Bold) + } + } + Spacer(modifier = Modifier.height(8.dp)) + } +} + + +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@Composable +fun ProfileScreen() { + var myInfo by remember { mutableStateOf(null) } + var followingList by remember { mutableStateOf>(emptyList()) } + var isLoading by remember { mutableStateOf(true) } + var errorMessage by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + try { + val userResult = RetrofitClient.service.getUser(id = 1) + myInfo = userResult.data + + val listResult = RetrofitClient.service.getUsers(page = 2) + followingList = listResult.data + } catch (e: Exception) { + errorMessage = "데이터를 불러오지 못했습니다: ${e.localizedMessage}" + } finally { + isLoading = false + } + } + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { Text("마이페이지", fontWeight = FontWeight.Bold, fontSize = 18.sp) } + ) + } + ) { innerPadding -> + Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) { + if (isLoading) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } else if (errorMessage != null) { + Text(text = errorMessage!!, color = Color.Red, modifier = Modifier.align(Alignment.Center)) + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + contentPadding = PaddingValues(bottom = 24.dp) + ) { + + item { + Spacer(modifier = Modifier.height(16.dp)) + AsyncImage( + model = myInfo?.avatar, + contentDescription = "프로필 이미지", + modifier = Modifier.size(100.dp).clip(CircleShape), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.height(16.dp)) + val fullName = "${myInfo?.first_name} ${myInfo?.last_name}" + Text(text = fullName, fontSize = 22.sp, fontWeight = FontWeight.Bold, color = Color.Black) + Spacer(modifier = Modifier.height(16.dp)) + } + + + item { + Surface( + modifier = Modifier + .width(160.dp) + .height(40.dp) + .clickable { }, + shape = RoundedCornerShape(20.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, Color.LightGray), + color = Color.White + ) { + Box(contentAlignment = Alignment.Center) { + Text(text = "프로필 수정", fontSize = 14.sp, color = Color.Black, fontWeight = FontWeight.Medium) + } + } + Spacer(modifier = Modifier.height(32.dp)) + HorizontalDivider(color = Color(0xFFEEEEEE), thickness = 1.dp) + } + + + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MenuComponent(icon = Icons.Default.Inventory, label = "주문", modifier = Modifier.weight(1f)) + VerticalDivider(modifier = Modifier.height(32.dp), color = Color(0xFFE0E0E0), thickness = 1.dp) + MenuComponent(icon = Icons.Default.Badge, label = "패스", modifier = Modifier.weight(1f)) + VerticalDivider(modifier = Modifier.height(32.dp), color = Color(0xFFE0E0E0), thickness = 1.dp) + MenuComponent(icon = Icons.Default.CalendarMonth, label = "이벤트", modifier = Modifier.weight(1f)) + VerticalDivider(modifier = Modifier.height(32.dp), color = Color(0xFFE0E0E0), thickness = 1.dp) + MenuComponent(icon = Icons.Default.Settings, label = "설정", modifier = Modifier.weight(1f)) + } + HorizontalDivider(color = Color(0xFFF5F5F5), thickness = 8.dp) + } + + + item { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { } + .padding(horizontal = 20.dp, vertical = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + Text(text = "나이키 멤버 혜택", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.Black) + Spacer(modifier = Modifier.height(4.dp)) + Text(text = "0개 사용 가능", fontSize = 13.sp, color = Color.Gray) + } + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = "더보기", + tint = Color.Gray, + modifier = Modifier.size(24.dp) + ) + } + HorizontalDivider(color = Color(0xFFF5F5F5), thickness = 8.dp) + } + + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "팔로잉 (${followingList.size})", fontSize = 16.sp, fontWeight = FontWeight.Bold) + Text(text = "편집", fontSize = 14.sp, color = Color.Gray, modifier = Modifier.clickable { }) + } + Spacer(modifier = Modifier.height(16.dp)) + + if (followingList.isNotEmpty()) { + + val pagerState = rememberPagerState(pageCount = { followingList.size }) + + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth().height(110.dp), + contentPadding = PaddingValues(horizontal = 140.dp), + pageSpacing = 16.dp + ) { page -> + val user = followingList[page] + + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + AsyncImage( + model = user.avatar, + contentDescription = "팔로잉 프로필", + modifier = Modifier + .size(70.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = user.first_name, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = Color.Black, + maxLines = 1 + ) + } + } + } else { + Text("팔로잉하는 유저가 없습니다.", color = Color.Gray, fontSize = 14.sp) + } + + Spacer(modifier = Modifier.height(32.dp)) + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text(text = "회원 가입일: 2025년 9월", color = Color.LightGray, fontSize = 12.sp) + } + } + } + } + } + } + } +} + + +@Composable +fun MenuComponent(icon: androidx.compose.ui.graphics.vector.ImageVector, label: String, modifier: Modifier = Modifier) { + Column( + modifier = modifier.clickable { }, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon(imageVector = icon, contentDescription = label, tint = Color.Gray, modifier = Modifier.size(28.dp)) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = label, fontSize = 13.sp, color = Color.Black, fontWeight = FontWeight.Medium) + } +} \ No newline at end of file diff --git a/Week7/app/src/main/java/com/example/myapplication/NetWork.kt b/Week7/app/src/main/java/com/example/myapplication/NetWork.kt new file mode 100644 index 0000000..27db50a --- /dev/null +++ b/Week7/app/src/main/java/com/example/myapplication/NetWork.kt @@ -0,0 +1,51 @@ +package com.example.myapplication + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Path +import retrofit2.http.Query + +data class UserResponse(val data: UserData) + +data class UserListResponse(val data: List) + +data class UserData( + val id: Int, + val email: String, + val first_name: String, + val last_name: String, + val avatar: String +) + +interface ReqResService { + + @GET("api/users/{id}") + suspend fun getUser( + @Path("id") id: Int, + @Header("x-api-key") + apiKey: String = "reqres_3fd0bc2b94a34359a2e2b56a6f1f7d85" + ): UserResponse + + @GET("api/users") + suspend fun getUsers( + @Query("page") page: Int = 2, + @Header("x-api-key") + apiKey: String = "reqres_3fd0bc2b94a34359a2e2b56a6f1f7d85" + ): UserListResponse +} + +object RetrofitClient { + + private const val BASE_URL = "https://reqres.in/" + + val service: ReqResService by lazy { + + Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(ReqResService::class.java) + } +} \ No newline at end of file diff --git a/Week7/app/src/main/java/com/example/myapplication/RetrofitClient.kt b/Week7/app/src/main/java/com/example/myapplication/RetrofitClient.kt new file mode 100644 index 0000000..3851917 --- /dev/null +++ b/Week7/app/src/main/java/com/example/myapplication/RetrofitClient.kt @@ -0,0 +1,18 @@ +package com.example.myapplication + +import com.example.myapplication.model.Product +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Path +import retrofit2.http.Query + + +val sampleStoreProducts = listOf( + Product(1, "Adidas", R.drawable.adidas, "$89.00", "Tops & Shirts"), + Product(2, "Nike", R.drawable.nike, "$99.00", "Tops & Shirts"), + Product(3, "Lecoqsportif", R.drawable.lecoqsportif, "$120.00", "Sale"), + Product(4, "Fila", R.drawable.fila, "$59.00", "Sale") +) + diff --git a/Week7/app/src/main/java/com/example/myapplication/StoreScreen.kt b/Week7/app/src/main/java/com/example/myapplication/StoreScreen.kt new file mode 100644 index 0000000..954bd64 --- /dev/null +++ b/Week7/app/src/main/java/com/example/myapplication/StoreScreen.kt @@ -0,0 +1,193 @@ +package com.example.myapplication + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.FavoriteBorder +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.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.myapplication.model.Product + +@Composable +fun StoreScreen( + wishlistedIds: List, + onHeartToggle: (Int) -> Unit +) { + var selectedCategory by remember { mutableStateOf("전체") } + + val categories = listOf( + "전체", + "Tops & Shirts", + "Sale" + ) + + val filteredProducts = + if (selectedCategory == "전체") { + sampleStoreProducts + } else { + sampleStoreProducts.filter { + it.category == selectedCategory + } + } + + Column( + modifier = Modifier.fillMaxSize() + ) { + + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + categories.forEach { category -> + + Text( + text = category, + modifier = Modifier + .clickable { + selectedCategory = category + } + .padding(8.dp), + fontWeight = + if (selectedCategory == category) + FontWeight.Bold + else + FontWeight.Normal, + color = + if (selectedCategory == category) + Color.Black + else + Color.Gray + ) + } + } + + HorizontalDivider( + thickness = 1.dp, + color = Color.LightGray + ) + + Spacer(modifier = Modifier.height(12.dp)) + + + val rows = filteredProducts.chunked(2) + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + + items(rows) { rowItems -> + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + + rowItems.forEach { product -> + + StoreProductItem( + product = product, + isLiked = wishlistedIds.contains(product.id), + onHeartClick = { + onHeartToggle(product.id) + }, + modifier = Modifier.weight(1f) + ) + } + + // 한 줄에 하나만 있을 때 빈 공간 맞추기 + if (rowItems.size == 1) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } + } +} + +@Composable +fun StoreProductItem( + product: Product, + isLiked: Boolean, + onHeartClick: () -> Unit, + modifier: Modifier = Modifier +) { + + Column( + modifier = modifier + ) { + + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) { + + Card( + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxSize() + ) { + + Image( + painter = painterResource(id = product.imageRes), + contentDescription = product.name, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + + IconButton( + onClick = onHeartClick, + modifier = Modifier + .align(Alignment.TopStart) + .padding(8.dp) + ) { + + Icon( + imageVector = + if (isLiked) + Icons.Default.Favorite + else + Icons.Default.FavoriteBorder, + contentDescription = "Heart", + tint = + if (isLiked) + Color.Red + else + Color.White + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = product.name, + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + + Text( + text = product.price, + color = Color.Gray, + fontSize = 13.sp + ) + } +} \ No newline at end of file diff --git a/Week7/app/src/main/java/com/example/myapplication/model/Product.kt b/Week7/app/src/main/java/com/example/myapplication/model/Product.kt new file mode 100644 index 0000000..faf5a6e --- /dev/null +++ b/Week7/app/src/main/java/com/example/myapplication/model/Product.kt @@ -0,0 +1,10 @@ +package com.example.myapplication.model + +data class Product( + val id: Int, + val name: String, + val imageRes: Int, + val price: String, + val category: String = "All", + val description: String = "프리미엄 라인업" +) \ No newline at end of file diff --git a/Week7/app/src/main/res/drawable/adidas.png b/Week7/app/src/main/res/drawable/adidas.png new file mode 100644 index 0000000..a0ee279 Binary files /dev/null and b/Week7/app/src/main/res/drawable/adidas.png differ diff --git a/Week7/app/src/main/res/drawable/fila.png b/Week7/app/src/main/res/drawable/fila.png new file mode 100644 index 0000000..24e6663 Binary files /dev/null and b/Week7/app/src/main/res/drawable/fila.png differ diff --git a/Week7/app/src/main/res/drawable/home_logo.png b/Week7/app/src/main/res/drawable/home_logo.png new file mode 100644 index 0000000..f42aa9d Binary files /dev/null and b/Week7/app/src/main/res/drawable/home_logo.png differ diff --git a/Week7/app/src/main/res/drawable/ic_cart.xml b/Week7/app/src/main/res/drawable/ic_cart.xml new file mode 100644 index 0000000..3b6e816 --- /dev/null +++ b/Week7/app/src/main/res/drawable/ic_cart.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/Week7/app/src/main/res/drawable/ic_cartcircle.xml b/Week7/app/src/main/res/drawable/ic_cartcircle.xml new file mode 100644 index 0000000..1af2ce6 --- /dev/null +++ b/Week7/app/src/main/res/drawable/ic_cartcircle.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/Week7/app/src/main/res/drawable/ic_home.xml b/Week7/app/src/main/res/drawable/ic_home.xml new file mode 100644 index 0000000..a0d86b9 --- /dev/null +++ b/Week7/app/src/main/res/drawable/ic_home.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/Week7/app/src/main/res/drawable/ic_launcher_background.xml b/Week7/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/Week7/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Week7/app/src/main/res/drawable/ic_launcher_foreground.xml b/Week7/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/Week7/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Week7/app/src/main/res/drawable/ic_profile.xml b/Week7/app/src/main/res/drawable/ic_profile.xml new file mode 100644 index 0000000..1fc97b8 --- /dev/null +++ b/Week7/app/src/main/res/drawable/ic_profile.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/Week7/app/src/main/res/drawable/ic_shop.xml b/Week7/app/src/main/res/drawable/ic_shop.xml new file mode 100644 index 0000000..483d117 --- /dev/null +++ b/Week7/app/src/main/res/drawable/ic_shop.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/Week7/app/src/main/res/drawable/ic_wishlist.xml b/Week7/app/src/main/res/drawable/ic_wishlist.xml new file mode 100644 index 0000000..6597a4a --- /dev/null +++ b/Week7/app/src/main/res/drawable/ic_wishlist.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/Week7/app/src/main/res/drawable/jordan_1.png b/Week7/app/src/main/res/drawable/jordan_1.png new file mode 100644 index 0000000..ff82221 Binary files /dev/null and b/Week7/app/src/main/res/drawable/jordan_1.png differ diff --git a/Week7/app/src/main/res/drawable/jordan_2.png b/Week7/app/src/main/res/drawable/jordan_2.png new file mode 100644 index 0000000..f6f0bc6 Binary files /dev/null and b/Week7/app/src/main/res/drawable/jordan_2.png differ diff --git a/Week7/app/src/main/res/drawable/jordan_3.png b/Week7/app/src/main/res/drawable/jordan_3.png new file mode 100644 index 0000000..70577b4 Binary files /dev/null and b/Week7/app/src/main/res/drawable/jordan_3.png differ diff --git a/Week7/app/src/main/res/drawable/lecoqsportif.png b/Week7/app/src/main/res/drawable/lecoqsportif.png new file mode 100644 index 0000000..ed9a514 Binary files /dev/null and b/Week7/app/src/main/res/drawable/lecoqsportif.png differ diff --git a/Week7/app/src/main/res/drawable/nike.png b/Week7/app/src/main/res/drawable/nike.png new file mode 100644 index 0000000..b8faabc Binary files /dev/null and b/Week7/app/src/main/res/drawable/nike.png differ diff --git a/Week7/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Week7/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/Week7/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Week7/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Week7/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/Week7/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Week7/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Week7/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/Week7/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/Week7/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/Week7/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/Week7/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/Week7/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/Week7/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/Week7/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/Week7/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/Week7/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/Week7/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/Week7/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/Week7/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/Week7/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/Week7/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/Week7/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/Week7/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/Week7/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Week7/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/Week7/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/Week7/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Week7/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/Week7/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/Week7/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Week7/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/Week7/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/Week7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Week7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/Week7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/Week7/app/src/main/res/values-night/themes.xml b/Week7/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..d25b0f2 --- /dev/null +++ b/Week7/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/Week7/app/src/main/res/values/colors.xml b/Week7/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c8524cd --- /dev/null +++ b/Week7/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/Week7/app/src/main/res/values/strings.xml b/Week7/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3de1a39 --- /dev/null +++ b/Week7/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + My Application + \ No newline at end of file diff --git a/Week7/app/src/main/res/values/themes.xml b/Week7/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..d66ac09 --- /dev/null +++ b/Week7/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + +