-
Notifications
You must be signed in to change notification settings - Fork 71
[쇼핑 주문 1, 2 단계] 지오 미션 제출합니다. #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 83 commits
51faddb
1045229
49d9925
c61df96
6d0f167
41b6fa2
29bf4d0
4eb567c
3d96377
4ade358
58d49c6
a6da981
b7bdb5b
77791b4
522450f
2df399d
00e4ec3
bec6e07
c913d17
840daa1
b607dc0
2674b6f
dc107b4
6903135
c5e3abc
9aca3f2
efbc84b
8d3674a
7577426
d54d751
6cf001b
6df3794
69d38fc
85f2891
1ff7a9b
9a30c9c
2bc6556
81fcca8
ead30aa
c2a9bbb
01e5311
a591bac
94589aa
617c618
ab1dfd1
b7db1ee
71d8a57
87072b5
d77c0b2
cb42585
c7fe647
d2b6a31
951e206
d90a816
725ea71
3a714b7
e06ea57
71fc2af
c044eac
d5c1e78
9367d33
5138b66
38d691b
9ad5719
3aa93ab
e3e7325
1a9247e
0869c31
be9842a
9eca1f9
1063871
5127b3e
c3daeb4
eee920f
ad0f799
0ea8c93
ad1dbe1
0fa2eb0
74f8ee1
e3ffeda
94632e0
2880fcd
4093a5c
0ac4db4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,24 @@ | ||
# android-shopping-order | ||
# android-shopping-order | ||
|
||
## 기능 요구 사항 | ||
|
||
- [ ] 데이터가 로딩되기 전 상태에서는 스켈레톤 UI를 노출한다. | ||
|
||
## 프로그래밍 요구 사항 | ||
|
||
- [x] 서버를 연동한다. | ||
- [x] 사용자의 장바구니 목록 조회 API를 연동한다. | ||
- [x] 장바구니 아이템 추가 API를 연동한다. | ||
- [x] 장바구니 아이템 삭제 API를 연동한다. | ||
- [x] 장바구니 아이템 수량 변경 API를 연동한다. | ||
- [x] 장바구니 아이템 수량 조회 API를 연동한다. | ||
- [x] 상품 목록 조회 API를 연동한다. | ||
- [x] 상품 상세 조회 API를 연동한다. | ||
- [ ] 기존에 작성한 테스트가 깨지면 안 된다. | ||
- [x] 사용자 인증 정보를 저장한다. (적절한 저장 방법을 선택한다) | ||
- [x] 장바구니 화면에서 특정 상품만 골라 주문하기 버튼을 누를 수 있다. | ||
- [ ] 별도의 화면에서 상품 추천 알고리즘으로 사용자에게 적절한 상품을 추천해준다. (쿠팡 UX 참고) | ||
- [x] 상품 추천 알고리즘은 최근 본 상품 카테고리를 기반으로 최대 10개 노출한다. | ||
- [x] 해당 카테고리 상품이 10개 미만이라면 해당하는 개수만큼만 노출 | ||
- [x] 장바구니에 이미 추가된 상품이라면 미노출 | ||
- [ ] 추천된 상품을 해당 화면에서 바로 추가하여 같이 주문할 수 있다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,21 @@ | ||
import java.util.Properties | ||
|
||
plugins { | ||
id("kotlin-kapt") | ||
alias(libs.plugins.android.application) | ||
alias(libs.plugins.android.junit5) | ||
alias(libs.plugins.kotlin.android) | ||
id("kotlin-kapt") | ||
alias(libs.plugins.serialization) | ||
} | ||
|
||
private val localProperties = | ||
Properties().apply { | ||
val localFile = rootProject.file("local.properties") | ||
if (localFile.exists()) { | ||
load(localFile.inputStream()) | ||
} | ||
} | ||
|
||
android { | ||
namespace = "woowacourse.shopping" | ||
compileSdk = 35 | ||
|
@@ -22,12 +33,18 @@ android { | |
} | ||
|
||
buildTypes { | ||
debug { | ||
buildConfigField("boolean", "DEBUG", "true") | ||
buildConfigField("String", "BASE_URL", localProperties["base.url.dev"].toString()) | ||
} | ||
release { | ||
isMinifyEnabled = false | ||
proguardFiles( | ||
getDefaultProguardFile("proguard-android-optimize.txt"), | ||
"proguard-rules.pro", | ||
) | ||
buildConfigField("boolean", "DEBUG", "false") | ||
buildConfigField("String", "BASE_URL", localProperties["base.url.release"].toString()) | ||
} | ||
} | ||
compileOptions { | ||
|
@@ -45,11 +62,13 @@ android { | |
} | ||
buildFeatures { | ||
dataBinding = true | ||
buildConfig = true | ||
} | ||
} | ||
|
||
dependencies { | ||
kapt(libs.androidx.room.compiler) | ||
implementation(libs.androidx.activity) | ||
implementation(libs.androidx.activity.ktx) | ||
implementation(libs.androidx.appcompat) | ||
implementation(libs.androidx.constraintlayout) | ||
|
@@ -58,6 +77,13 @@ dependencies { | |
implementation(libs.google.glide) | ||
implementation(libs.room.runtime) | ||
implementation(libs.okhttp) | ||
implementation(libs.kotlinx.serialization.json) | ||
implementation(libs.logging.interceptor) | ||
implementation(libs.shimmer) | ||
implementation(libs.androidx.security.crypto) | ||
implementation(libs.retrofit) | ||
Comment on lines
+83
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용자의 아이디와 비밀번호를 암호화하여 저장하기 위함입니다! 😁처음에는 간단한 구조이기 때문에 키-값 형태로 하지만 이는 기기 내부에 파일로 저장되어 위험하다는 것을 학습했습니다. 🤔 따라서 암호화하여 저장하는 방법을 찾다 보니 EncryptedSharedPreferences를 발견했고, 사용해보았습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그렇군요~ |
||
implementation(libs.converter.gson) | ||
implementation(libs.retrofit2.kotlinx.serialization.converter) | ||
Comment on lines
+80
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아마 프로젝트를 설정할 때부터 version catalogs를 사용하도록 되어 있었을 것 같은데, 이전에는 gradle을 kotlin 이전에 groovy로 적던 시절도 있었어요~ 지금 당장 하기에는 너무 과한 내용일 것 같아서 추후에 한번 시간이 남을 때 살펴보시면 좋을 것 같아요
|
||
testImplementation(libs.assertj.core) | ||
testImplementation(libs.junit.jupiter) | ||
testImplementation(libs.kotest.runner.junit5) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package woowacourse.shopping.data | ||
|
||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory | ||
import kotlinx.serialization.json.Json | ||
import okhttp3.MediaType.Companion.toMediaType | ||
import okhttp3.OkHttpClient | ||
import okhttp3.logging.HttpLoggingInterceptor | ||
import retrofit2.Retrofit | ||
import woowacourse.shopping.BuildConfig | ||
import woowacourse.shopping.data.cart.service.CartService | ||
import woowacourse.shopping.data.product.service.ProductService | ||
|
||
object API { | ||
private val client: OkHttpClient by lazy { | ||
OkHttpClient | ||
.Builder() | ||
.addInterceptor(AuthInterceptor()) | ||
.addHttpLoggingInterceptor() | ||
.build() | ||
} | ||
|
||
private val retrofit by lazy { | ||
Retrofit | ||
.Builder() | ||
.baseUrl(BuildConfig.BASE_URL) | ||
.client(client) | ||
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) | ||
.build() | ||
} | ||
|
||
val productService: ProductService = retrofit.create(ProductService::class.java) | ||
val cartService: CartService = retrofit.create(CartService::class.java) | ||
|
||
private fun OkHttpClient.Builder.addHttpLoggingInterceptor() = | ||
addInterceptor( | ||
HttpLoggingInterceptor().apply { | ||
level = | ||
if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE | ||
}, | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package woowacourse.shopping.data | ||
|
||
import okhttp3.Interceptor | ||
import okhttp3.Request | ||
import okhttp3.Response | ||
|
||
class AuthInterceptor : Interceptor { | ||
override fun intercept(chain: Interceptor.Chain): Response { | ||
val originalRequest = chain.request() | ||
val authToken: String = AuthStorage.authToken ?: return chain.proceed(originalRequest) | ||
|
||
val request: Request = | ||
originalRequest | ||
.newBuilder() | ||
.addHeader( | ||
AUTH_HEADER_KEY, | ||
authToken, | ||
).build() | ||
|
||
val response: Response = chain.proceed(request) | ||
return response | ||
} | ||
|
||
companion object { | ||
private const val AUTH_HEADER_KEY = "Authorization" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package woowacourse.shopping.data | ||
|
||
import android.content.Context | ||
import android.content.SharedPreferences | ||
import androidx.core.content.edit | ||
import androidx.security.crypto.EncryptedSharedPreferences | ||
import androidx.security.crypto.MasterKeys | ||
import java.util.Base64 | ||
|
||
object AuthStorage { | ||
private lateinit var storage: SharedPreferences | ||
|
||
fun init(applicationContext: Context) { | ||
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) | ||
|
||
storage = | ||
EncryptedSharedPreferences.create( | ||
FILE_NAME, | ||
Comment on lines
+17
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분은 EncryptedSharedPreferences 은 Deprecatd 되어진걸로 보여지는데, 사용을 하신 이유가 있을까요~? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 고객의 id, pw 또는 토큰을 로컬 저장소에 저장해야 할 것 같은데, 키-값 형태의 비교적 간단한 데이터이기 때문에 다만 Deprecated된 것을 확인했기에, 다른 방법을 찾아보도록 하겠습니다! 😁 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 방법이라면, 아래와 같은 방법도 있을 것 같다는 생각이 들었어요~
|
||
masterKeyAlias, | ||
applicationContext, | ||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, | ||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, | ||
) | ||
} | ||
Comment on lines
+13
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 암호화 초기화 실패 상황을 어떻게 처리할지 고민해보세요.
이런 예외 상황들을 어떻게 감지하고 처리할지, 그리고 사용자에게 어떤 경험을 제공할지 생각해보세요. 🤖 Prompt for AI Agents
|
||
|
||
var id: String? | ||
get() = storage.getString(KEY_ID, DEFAULT_ID) | ||
set(value) = storage.edit { putString(KEY_ID, value) } | ||
|
||
var pw: String? | ||
get() = storage.getString(KEY_PW, DEFAULT_PW) | ||
set(value) = storage.edit { putString(KEY_PW, value) } | ||
|
||
val authToken: String? | ||
get() { | ||
if (id == null || pw == null) { | ||
return null | ||
} | ||
|
||
val valueToEncode = "$id:$pw".toByteArray() | ||
return BASIC_AUTH_FORMAT.format(Base64.getEncoder().encodeToString(valueToEncode)) | ||
} | ||
|
||
private const val KEY_ID = "woowacourse.shopping.KEY_ID" | ||
private const val DEFAULT_ID = "giovannijunseokim" | ||
private const val KEY_PW = "woowacourse.shopping.KEY_PW" | ||
private const val DEFAULT_PW = "password" | ||
private const val FILE_NAME = "auth" | ||
private const val BASIC_AUTH_FORMAT = "Basic %s" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package woowacourse.shopping.data | ||
|
||
import okhttp3.RequestBody | ||
|
||
sealed interface HttpMethod { | ||
val name: String | ||
val body: RequestBody? | ||
|
||
object Get : HttpMethod { | ||
override val name: String = "GET" | ||
override val body: RequestBody? = null | ||
} | ||
|
||
class Post( | ||
override val body: RequestBody, | ||
) : HttpMethod { | ||
override val name: String = "POST" | ||
} | ||
|
||
class Delete( | ||
override val body: RequestBody? = null, | ||
) : HttpMethod { | ||
override val name: String = "DELETE" | ||
} | ||
|
||
class Patch( | ||
override val body: RequestBody, | ||
) : HttpMethod { | ||
override val name: String = "PATCH" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
README 작성 좋습니다👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
참고로 아래와 같은 형태로, PR 에서 봐야할 커밋들을 필터를 걸 수 있습니다!
(필터를 안걸면 모든 코드가 로드 되다 보니... 제 맥이 버거워 하는군요)
PR > 좌측 commit > shift + click > 봐야할 커밋 드래그
PR 필터
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그 점을 신경쓰지 못했네요 😅
PR 소개글 맨 위에 추가해두었습니다!