Skip to content

[쇼핑 주문 1, 2 단계] 채넛 미션 제출합니다. #99

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

Merged
merged 78 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
9491751
Import shopping-cart a branch as a single commit
HamBeomJoon May 27, 2025
6fd0c59
test: Json Object 학습 테스트
HamBeomJoon May 27, 2025
0242a92
setting: retrofit, serialization 라이브러리 추가
HamBeomJoon May 27, 2025
a1e32f3
docs: README 기능 목록 작성
yrsel May 27, 2025
93c5625
build: shimmer 라이브러리 추가
yrsel May 27, 2025
1d527b6
feat: 상품 목록 페이지 스켈레톤 UI 적용
yrsel May 27, 2025
949faca
feat: 장바구니 페이지 스켈레톤 UI 적용
yrsel May 27, 2025
68942a7
chore: ktlint적용
yrsel May 27, 2025
c424b03
test: mockWebserver 테스트 패키지로 이동
HamBeomJoon May 27, 2025
55bcfad
build: serialization convertor 라이브러리 추가
HamBeomJoon May 27, 2025
8360a5a
feat: CartResponse 추가
HamBeomJoon May 27, 2025
efc862f
feat: ProductsResponse 추가
HamBeomJoon May 27, 2025
964322c
feat: Retrofit prodivde 함수 추가
HamBeomJoon May 27, 2025
03de383
refactor: 상품 목록 조회 서버 연동
HamBeomJoon May 27, 2025
090d259
refactor: 상품 상세 조회 서버 연동
yrsel May 27, 2025
a5afd94
feat: 사용자 인증 정보 sharedPreference에 저장
HamBeomJoon May 27, 2025
f3ba211
feat: 사용자 인증 정보 BuildConfig에 저장, 불러오기
HamBeomJoon May 27, 2025
cc27cf6
refactor: content -> productContent, CartContent로 이름 변경
HamBeomJoon May 27, 2025
469378d
refactor: 장바구니 상품 조회 서버 연동
HamBeomJoon May 27, 2025
45c2a2e
feat: header에 사용자 인증 정보 interceptor에서 추가
HamBeomJoon May 27, 2025
f724ce7
feat: 전체 장바구니 아이템 조회 및 장바구니 데이터 캐싱
yrsel May 28, 2025
23d7203
feat: 장바구니 새로운 상품 추가 서버 연동
yrsel May 28, 2025
2798f71
feat: 캐싱 장바구니 조회, 추가기능
yrsel May 28, 2025
5b4fed6
feat: 장바구니 이미 존재하는 상품 수량 증가 서버 연동
yrsel May 28, 2025
ff43839
feat: Json PrettyPrint 추가
HamBeomJoon May 28, 2025
c25bee9
refactor: datasource 함수명 변경
HamBeomJoon May 28, 2025
8e37ee4
feat: 장바구니 상품 삭제 서버 연동
HamBeomJoon May 28, 2025
8ff681d
feat: 장바구니 상품 업데이트 및 삭제 캐싱
HamBeomJoon May 28, 2025
b4f8717
feat: 상품 목록 페이지에서 장바구니에 추가 서버 연동
HamBeomJoon May 28, 2025
0259668
refactor: 상품 목록 페이지 데이터 로딩 순서 변경
HamBeomJoon May 28, 2025
f595b15
feat: 상품 목록에서 수량 증가, 감소 서버 연동
yrsel May 28, 2025
826ead7
feat: 툴바에 장바구니 상품 개수 표시 서버 연동
yrsel May 28, 2025
3b66582
feat: 장바구니 수량 증가 감소 서버 연동
HamBeomJoon May 28, 2025
bed9198
feat: 최근 본 상품 표시 기능 추가
HamBeomJoon May 28, 2025
5372d4e
feat: 추가 상품 추천 페이지 추가
HamBeomJoon May 28, 2025
7a9be3c
feat: 장바구니 상품 체크, 주문하기 view 추가
HamBeomJoon May 28, 2025
25aa63e
feat: product, cartItem UiModel 추가
HamBeomJoon May 28, 2025
3e16618
docs: README 2단계 기능 목록 추가
yrsel May 28, 2025
027b436
feat: Domain, UiModel 매핑 함수 추가
yrsel May 29, 2025
630c09a
feat: 상품 추천 어댑터 추가
yrsel May 29, 2025
9056075
feat: 상품 추천 화면 추가
yrsel May 29, 2025
99aca11
refactor: 장바구니 상품 전체 불러오도록 변경
HamBeomJoon May 29, 2025
3d3e024
chore: 메서드 정리
HamBeomJoon May 29, 2025
58fe555
fix: 장바구니 상품 삭제 시 화면 갱신
HamBeomJoon May 29, 2025
ebf8407
refactor: 장바구니 아이템 livedata 타입 변경
HamBeomJoon May 29, 2025
4e4ce8a
refactor: 장바구니 존재하는 상품 제외 후 상품 추천
yrsel May 29, 2025
ae28f7e
fix: 장바구니 상품 총 금액 연동
yrsel May 29, 2025
ea1e189
feat: 추천상품 수량 증가, 감소 기능 구현
yrsel May 29, 2025
8fc3280
feat: 장바구니 페이지 선택한 상품 총 가격, 수량 연동
HamBeomJoon May 29, 2025
b32877d
refactor: 추천 화면 갔다 돌아올 때 기존 아이템 유지하도록 변경
HamBeomJoon May 29, 2025
5be76b0
feat: 장바구니에 담긴 상품 정보 기반으로 추천 화면 초기화
HamBeomJoon May 29, 2025
66da204
feat: 상품 추천 페이지 상품 증가, 감소 총 가격, 수량 연동
HamBeomJoon May 29, 2025
83d6b87
feat: 장바구니 아이템 전체 선택 버튼 로직 구현
yrsel May 29, 2025
5e31972
feat: 상세 페이지 화면 이동 기능 추가
yrsel May 29, 2025
4fa0980
refactor: 장바구니 상품 아이템 선택 시 전체 선택 버튼과 동기화
yrsel May 29, 2025
7a433be
refactor: 문자열 상수화 및 리사이클러뷰 사이즈 조절
yrsel May 29, 2025
e7c544a
test: 기존 테스트 수정
HamBeomJoon May 29, 2025
0e2624c
docs: 기능 요구 사항 체크
HamBeomJoon May 29, 2025
ad24a87
feat: 상품 목록, 장바구니 화면 uiState추가, 스켈레톤 추가
HamBeomJoon May 29, 2025
87d5ddf
chore: 사용하지 않는 파일 제거
HamBeomJoon May 29, 2025
081c4a1
chore: 사용 안하는 파일들 제거
yrsel May 29, 2025
d5b4e12
chore: CartItemMapper.kt 사용되지 않는 코드 제거
yrsel Jun 2, 2025
f0c976a
refactor: 장바구니 페이지 상품 동기화 생명주기 변경
yrsel Jun 2, 2025
3f5bf71
refactor: 장바구니 전체 선택 버튼 상태에 알맞게 동기화
yrsel Jun 2, 2025
c7d6ae2
refactor: CartRepository 에서 캐싱 데이터 동기화 메서드를 구현 강제할 수 있도록 수정
yrsel Jun 2, 2025
3fecd89
refactor: CartRepository 조회 메서드 접두사 fetch 로 일관되게 수정
yrsel Jun 2, 2025
8973ede
rename: ItemDiffUtil -> RecommendDiffUtil
yrsel Jun 2, 2025
b2fe948
refactor: CartAdapter ListAdapter 활용해서 구현하도록 수정
yrsel Jun 2, 2025
5beb5ce
refactor: 추천 상품 페이지 리사이클러뷰 하단 제약조건 추가
yrsel Jun 2, 2025
1a40367
refactor: 하드코딩된 리소스 값 수정
yrsel Jun 2, 2025
a6e7876
refactor: Client Interceptor 클래스 분리
yrsel Jun 2, 2025
07ce825
refactor: 모든 콜백함수 결과값 Result로 성공, 실패 판단할 수 있도록 수정
yrsel Jun 2, 2025
380a7e6
refactor: ProductAdapter ListAdapter 활용해서 구현하도록 수정
yrsel Jun 3, 2025
e37bf98
refactor: RecentProductAdapter ListAdapter 활용해서 구현하도록 수정
yrsel Jun 3, 2025
2439382
refactor: CartViewModel MediatorLiveData 활용하도록 수정
yrsel Jun 3, 2025
ef42285
refactor: 장바구니 추가, 수량 증가, 수량 감소 UseCase로 구현
yrsel Jun 3, 2025
07c800d
refactor: Adapter ClickListeners Acitivty -> ViewModel 이동
yrsel Jun 3, 2025
0c5b01e
refactor: ProductViewModel 항상 0페이지부터 데이터 받아오는 부분 수정
yrsel Jun 3, 2025
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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
# android-shopping-order
# android-shopping-order

### 1단계 기능 목록

- [x] 데이터가 로딩되기 전 상태에서는 스켈레톤 UI를 노출한다.
- [x] 서버를 연동한다.
- [x] 사용자 인증 정보를 저장한다.

### 2단계 기능 목록

- [x] 장바구니 화면에서 특정 상품만 골라 주문하기 버튼을 누를 수 있다.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상품목록에서 한가지 궁금한게 있어요.
이미지가 같은 상품에 대해서,
그 중 하나의 상품 개수를 증가시켰을 때, 이미지가 같은 상품들이 함께 깜빡이는 이슈가 있습니다.

어떤 이유이고 어떻게 개선할 수 있을까요?

screencapture-1748594987448.mp4

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미지 정보가 없거나 문제가 있는 상품일 경우 Glide 설정에서 모두 동일한 drawable을 넣었습니다.
구현 당시에는 notifyDataSetChanged() 를 호출하며 전체를 갱신하다보니 동일한 이미지 소스에 애니메이션이 들어갔지 않나 하는 추측을 해보았습니다.
이 리뷰를 보기 전에 ListAdapter 로 수정해버려 위와 같은 문제가 발생하지 않아 정확한 원인을 찾지는 못했습니다. 🥲

- [x] 별도의 화면에서 상품 추천 알고리즘으로 사용자에게 적절한 상품을 추천해준다. (쿠팡 UX 참고)
- [x] 상품 추천 알고리즘은 최근 본 상품 카테고리를 기반으로 최대 10개 노출한다.
- [x] 예를 들어 가장 최근에 본 상품이 fashion 카테고리라면, fashion 상품 10개 노출
- [x] 해당 카테고리 상품이 10개 미만이라면 해당하는 개수만큼만 노출
- [x] 장바구니에 이미 추가된 상품이라면 미노출
- [x] 추천된 상품을 해당 화면에서 바로 추가하여 같이 주문할 수 있다.
23 changes: 22 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import java.util.Properties

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.android.junit5)
alias(libs.plugins.kotlin.android)
id("kotlin-kapt")
alias(libs.plugins.serialization)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

선택한 JSON 직렬화 라이브러리 : kotlinx.serialization
선택한 이유 : 코틀린언어에서 제공하고 있어 신뢰할 수 있다고 생각했고 리플렉션을 사용하지 않고 직렬화, 역직렬화를 지원하기에 성능상 이점과 안전성이 조금이라도 더 보장될 것 같다는 생각으로 선택하게 되었습니다.

Gson, Moshi 와 같은 라이브러리들과 어떤 차이점이 있을까요?

Copy link
Author

@yrsel yrsel Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kotlinx.serialization은 코틀린 언어 기반이고 default value를 직렬화, 역직렬화에 활용할 수 있습니다.
그리고 코틀린 언어의 null type 안전성도 보장해주기에 코틀린 언어를 효율적으로 사용할 수 있습니다.

Gson, Moshi는 자바 기반 라이브러리이고 Gson의 경우 default value를 인식하지 못하며 Moshi의 경우 KotlinJsonAdapterFactory를 추가해야 default value를 인식할 수 있습니다.
값이 없는 경우 반환 받을 타입이 not-null type일지라도 참조형 타입의 경우 null, 기본형 타입의 경우 기본값으로 반환하게 되어 코틀린 언어의 null type 안전성을 해치게 되는 경우가 발생하게 됩니다.
또한, kotlinx.serialization과는 달리 리플렉션을 활용하여 직렬화, 역직렬화를 구현했다는 차이점이있습니다.

GsonMoshi 둘을 비교한다면 MoshiGson에 비해 코틀린 친화적이고 성능과 버전 업데이트 현황에서 이점이 있다고 할 수 있을 것 같습니다.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다 👍
default value가 중요한 키워드가 될 수 있으니
리플렉션, non-null 과 같은 키워드들을 중점적으로 기억해두시면 도움이 될거에요

}

android {
Expand Down Expand Up @@ -43,8 +46,20 @@ android {
excludes += "win32-x86*/**"
}
}
defaultConfig {
val localProperties =
Properties().apply {
load(File(rootDir, "local.properties").inputStream())
}
val id = localProperties["id"] as String
val pw = localProperties["password"] as String

buildConfigField("String", "ID", "\"$id\"")
buildConfigField("String", "PASSWORD", "\"$pw\"")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BuildConfig 활용 좋습니다 👍

}
buildFeatures {
dataBinding = true
buildConfig = true
}
}

Expand All @@ -67,15 +82,21 @@ dependencies {
kapt(libs.androidx.room.compiler)

// remote
implementation(libs.converter.gson)
implementation(libs.mockwebserver)
implementation(libs.logging.interceptor)
implementation(libs.kotlinx.serialization.json)
implementation(libs.retrofit)
implementation(libs.retrofit2.kotlinx.serialization.converter)

// shimmer
implementation(libs.shimmer)

testImplementation(libs.assertj.core)
testImplementation(libs.junit.jupiter)
testImplementation(libs.kotest.runner.junit5)
testImplementation(libs.mockk)
testImplementation(libs.mockwebserver)
testImplementation(libs.json)

androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.test.ext.junit)
Expand Down
40 changes: 31 additions & 9 deletions app/src/androidTest/java/woowacourse/shopping/Fixture.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,51 @@ import woowacourse.shopping.domain.model.Product
object Fixture {
val dummyCartEntity =
CartEntity(
0,
0,
10,
)

val dummyCartEntity2 =
CartEntity(
1,
1,
)

val dummyRecentlyViewedProduct =
RecentlyViewedProduct(
0,
1111,
productId = 0,
name = "맥심 모카골드 마일드",
price = 100,
imageUrl = "aa",
category = "aa",
viewedAt = 1234,
)

val dummyRecentlyViewedProductList =
listOf(
RecentlyViewedProduct(
0,
1111,
productId = 0,
name = "맥심 모카골드 마일드",
price = 100,
imageUrl = "aa",
category = "aa",
viewedAt = 1234,
),
RecentlyViewedProduct(
1,
5555,
productId = 1,
name = "맥심 모카골드 마일드2",
price = 1000,
imageUrl = "bb",
category = "bb",
viewedAt = 5555,
),
RecentlyViewedProduct(
2,
9999,
productId = 2,
name = "맥심 모카골드 마일드3",
price = 10000,
imageUrl = "cc",
category = "cc",
viewedAt = 9999,
),
)

Expand All @@ -41,5 +62,6 @@ object Fixture {
"맥심 모카골드 마일드",
Price(12000),
"https://sitem.ssgcdn.com/64/93/82/item/0000006829364_i1_464.jpg",
"커피",
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class CartDaoTest {
@Test
fun `상품을_추가할_수_있다`() {
// when
dao.insertProduct(Fixture.dummyCartEntity)
dao.insertProduct(Fixture.dummyCartEntity2)
val result = dao.getAllProducts()

// then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,6 @@ class CartActivityTest {
onView(withId(R.id.rv_cart_product)).checkIsDisplayed()
}

@Test
fun 이전_페이지로_이동하는_버튼이_표시된다() {
onView(withId(R.id.btn_cart_previous)).checkIsDisplayed()
}

@Test
fun 다음_페이지로_이동하는_버튼이_표시된다() {
onView(withId(R.id.btn_cart_next)).checkIsDisplayed()
}

@Test
fun 현재_페이지가_표시된다() {
onView(withId(R.id.tv_cart_page)).checkIsDisplayed()
}

@After
fun finish() {
Intents.release()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class ProductDetailActivityTest {
fakeContext,
ProductDetailActivity::class.java,
).apply {
putExtra(Extra.KEY_PRODUCT_DETAIL, Fixture.dummyProduct)
putExtra(Extra.KEY_PRODUCT_ID, Fixture.dummyProduct.productId)
}
scenario = ActivityScenario.launch(intent)
}
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".presentation.recommend.RecommendActivity"
android:exported="false" />
</application>

</manifest>
20 changes: 0 additions & 20 deletions app/src/main/java/woowacourse/shopping/MainActivity.kt

This file was deleted.

32 changes: 4 additions & 28 deletions app/src/main/java/woowacourse/shopping/ShoppingApplication.kt
Original file line number Diff line number Diff line change
@@ -1,41 +1,17 @@
package woowacourse.shopping

import android.app.Application
import android.util.Log
import okhttp3.mockwebserver.MockWebServer
import woowacourse.shopping.data.remote.ProductMockWebServerDispatcher
import woowacourse.shopping.data.datasource.local.UserPreference
import woowacourse.shopping.di.DataSourceModule
import woowacourse.shopping.di.DatabaseModule
import java.io.IOException
import woowacourse.shopping.di.RepositoryModule

class ShoppingApplication : Application() {
private val mockWebServer = MockWebServer()

override fun onCreate() {
super.onCreate()
startMockServer()
DatabaseModule.init(this)
DataSourceModule.init(this)
}

override fun onTerminate() {
super.onTerminate()
mockWebServer.shutdown()
}

private fun startMockServer() {
Thread {
try {
mockWebServer.dispatcher = ProductMockWebServerDispatcher
mockWebServer.start(PORT_NUMBER)
Log.d("ShoppingApplication", "MockWebServer started on port $PORT_NUMBER")
} catch (e: IOException) {
Log.e("ShoppingApplication", "Failed to start MockWebServer", e)
}
}.start()
}

companion object {
private const val PORT_NUMBER = 1999
RepositoryModule.init(this)
UserPreference.init(this)
}
}
11 changes: 7 additions & 4 deletions app/src/main/java/woowacourse/shopping/data/CartItemMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import woowacourse.shopping.domain.model.CartItem
class CartItemMapper(
private val productDataSource: ProductDataSource,
) {
fun toDomain(data: CartEntity): CartItem {
val product = productDataSource.fetchProductById(data.productId)
return CartItem(product, data.quantity)
}
// fun toDomain(data: CartEntity): CartItem {
// productDataSource.fetchProductById(data.productId){ result ->
// result.
// }
// return CartItem(product, data.quantity)
// }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 사용하지 않는 코드인걸까요?

Copy link
Author

@yrsel yrsel Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구현에 사용될까해서 놔뒀다가 마지막에 정리하지 못했던 것 같습니다.

해당 파일이 불필요해서 제거하였습니다!

관련 커밋 : d5b4e12



fun toData(domain: CartItem): CartEntity =
CartEntity(
Expand Down
Loading