Skip to content
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

부산대 Android 박정훈 5주차 과제 Step2 #85

Open
wants to merge 22 commits into
base: pjhn
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
63c009a
init: 4주차 코드 가져오기
Pjhn Jul 22, 2024
d5d1653
refactor: 장소 DB 처리 코드 개선
Pjhn Jul 22, 2024
eeafb37
refactor: 네트워크와 데이터베이스 작업 분리
Pjhn Jul 22, 2024
846345d
refactor: 지도 정보 뷰모델에서 처리
Pjhn Jul 23, 2024
d3bce44
refactor: 에러 메시지 표시 기능 개선
Pjhn Jul 23, 2024
c339be1
refactor: room 라이브러리 사용
Pjhn Jul 24, 2024
2e23dd5
refactor: 지역, 원격 repository 분리
Pjhn Jul 24, 2024
af63296
refactor: MapViewModel에서 LiveData를 Flow로 변경
Pjhn Jul 24, 2024
98b6866
delete: PlaceContract 및 테스트 코드 삭제
Pjhn Jul 24, 2024
60023fe
refactor: data layer 패키지 분리
Pjhn Jul 24, 2024
e2eb681
chore: hilt 라이브러리 추가
Pjhn Jul 24, 2024
e7e1299
refactor: Hilt 사용 및 Module 파일 생성
Pjhn Jul 24, 2024
7fb759c
feat: 네트워크 연결 시 정상화면 처리 기능 추가
Pjhn Jul 24, 2024
68187f2
fix: 지도 화면 갱신 시 강제종료 되는 버그 수정
Pjhn Jul 24, 2024
6003179
refactor: 지도 화면 binding 사용
Pjhn Jul 25, 2024
05b182c
feat: 지도 화면에서 새로고침 기능 비활성화
Pjhn Jul 25, 2024
8edf866
feat: 전체화면 설정
Pjhn Jul 25, 2024
657e362
design: 검색 화면 padding 수정
Pjhn Jul 25, 2024
692a002
refactor: localRepository 가독성 개선
Pjhn Jul 25, 2024
2e55d82
refactor: 지도 화면 viewBinding 사용
Pjhn Jul 26, 2024
7dda882
refactor: viewmodel 코루틴 적용 및 debounce 코드 수정
Pjhn Jul 26, 2024
e4c2b83
refactor: 지도 화면 xml 코드 수정
Pjhn Jul 28, 2024
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
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
.DS_Store
### Android template
# Gradle files
.gradle/
build/
Expand Down Expand Up @@ -33,5 +31,6 @@ google-services.json

# Android Profiling
*.hprof
/keyStore
/app/release

# Mac OS
.DS_Store
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
# android-map-refactoring
# android-map-location

카카오 맵 클론 코딩
카카오로컬 API 사용

## 기능 요구 사항
- 저장된 검색어를 선택하면 해당 검색어의 검색 결과가 표시된다.
- 검색 결과 목록 중 하나의 항목을 선택하면 해당 항목의 위치를 지도에 표시한다.
- 앱 종료 시 마지막 위치를 저장하여 다시 앱 실행 시 해당 위치로 포커스 한다.
- 카카오지도 onMapError() 호출 시 에러 화면을 보여준다.
-
## 프로그래밍 요구 사항
- BottomSheet를 사용한다.
- 카카오 API 사용을 위한 앱 키를 외부에 노출하지 않는다.
- 가능한 MVVM 아키텍처 패턴을 적용하도록 한다.
- 코드 컨벤션을 준수하며 프로그래밍한다.


68 changes: 38 additions & 30 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jlleitschuh.gradle.ktlint")
id("kotlin-parcelize")
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}

android {
namespace = "campus.tech.kakao.map"
compileSdk = 34


defaultConfig {
resValue("string", "kakao_api_key", getApiKey("KAKAO_API_KEY"))
buildConfigField("String", "KAKAO_REST_API_KEY", getApiKey("KAKAO_REST_API_KEY"))
applicationId = "campus.tech.kakao.map"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
ndk {
abiFilters.add("arm64-v8a")
abiFilters.add("armeabi-v7a")}


testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand All @@ -39,42 +46,43 @@ android {
}

buildFeatures {
viewBinding = true
dataBinding = true
buildConfig = true
}
}

dependencies {

implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("com.kakao.maps.open:android:2.9.5")
implementation("androidx.activity:activity-ktx:1.9.0")
implementation("androidx.test:core-ktx:1.6.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
implementation("androidx.room:room-runtime:2.6.1")
kapt("com.google.dagger:hilt-compiler:2.48.1")
kapt("androidx.room:room-compiler:2.6.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation ("androidx.activity:activity-ktx:1.1.0")
implementation ("androidx.fragment:fragment-ktx:1.2.5")
implementation("com.google.dagger:hilt-android:2.48.1")
kapt("com.google.dagger:hilt-compiler:2.48.1")
implementation("androidx.activity:activity-ktx:1.9.0")
implementation("androidx.room:room-ktx:2.6.1")
testImplementation("androidx.room:room-testing:2.6.1")
implementation("androidx.room:room-runtime:2.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
implementation("com.kakao.sdk:v2-all:2.20.3")
implementation("com.kakao.maps.open:android:2.9.5")
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.activity:activity:1.8.0")
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk-android:1.13.11")
testImplementation("io.mockk:mockk-agent:1.13.11")
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation("org.robolectric:robolectric:4.11.1")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("io.mockk:mockk:1.13.12")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.test:rules:1.6.1")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.6.1")
androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1")
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.48.1")
implementation("androidx.test:core-ktx:1.6.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.3.0")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.3.0")
}

fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key)
21 changes: 21 additions & 0 deletions app/build.gradle.kts.rej
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
diff a/app/build.gradle.kts b/app/build.gradle.kts (rejected hunks)
@@ -69,10 +69,17 @@
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.datastore:datastore-preferences:1.0.0")
- implementation("androidx.activity:activity:1.8.0")
+ implementation("androidx.activity:activity-ktx:1.8.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+ androidTestImplementation("androidx.test:rules:1.4.0")
+ androidTestImplementation("androidx.test:runner:1.4.0")
+ androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1")
+ androidTestImplementation("androidx.test.espresso:espresso-intents:3.5.1")
+ androidTestImplementation("io.mockk:mockk-android:1.13.3")
+ androidTestImplementation("androidx.arch.core:core-testing:2.1.0")
}

+
fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key)
\ No newline at end of file
15 changes: 13 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".PlaceApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand All @@ -13,14 +16,22 @@
android:theme="@style/Theme.Map"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".presentation.map.MapActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".presentation.search.SearchViewModel"
android:exported="false" />
<activity
android:name=".presentation.search.SearchActivity"
android:exported="false"/>


</application>

</manifest>
</manifest>
11 changes: 0 additions & 11 deletions app/src/main/java/campus/tech/kakao/map/MainActivity.kt

This file was deleted.

40 changes: 40 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/PlaceApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package campus.tech.kakao.map

import android.app.Application
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.view.View
import com.kakao.vectormap.KakaoMapSdk
import dagger.hilt.android.HiltAndroidApp


@HiltAndroidApp
class PlaceApplication: Application() {

override fun onCreate() {
super.onCreate()
appInstance = this

initKakaoMapSdk()
}

private fun initKakaoMapSdk(){
val key = getString(R.string.kakao_api_key)
KakaoMapSdk.init(this, key)
}
companion object {
@Volatile
private lateinit var appInstance: PlaceApplication
Comment on lines +27 to +28

Choose a reason for hiding this comment

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

appInstance 를 활용하고자 하신곳이 있을까요? 이 과제에서는 없는듯 하네요.
만약에 application context 를 사용하고 싶으신것이라면, hilt module에 @ApplicationContext 어노테이션을 사용해서 가져올 수 있는 방법이 있습니다. (참고 블로그)

fun isNetworkActive(): Boolean {
val connectivityManager: ConnectivityManager =
appInstance.getSystemService(ConnectivityManager::class.java)
val network: Network = connectivityManager.activeNetwork ?: return false
val actNetwork: NetworkCapabilities =
connectivityManager.getNetworkCapabilities(network) ?: return false

return actNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
actNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package campus.tech.kakao.map.data

import android.content.Context
import campus.tech.kakao.map.domain.model.Place
import javax.inject.Inject

class LastVisitedPlaceManager @Inject constructor(context: Context) {

private val sharedPreferences = context.getSharedPreferences("LastVisitedPlace", Context.MODE_PRIVATE)

Choose a reason for hiding this comment

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

생성자로 context를 주입받아서 클래스 내부에서 선언해도 되겠지만, shared preference 자체를 hilt module에 선언하고, 생성자로 전달받도록 할 수 있을것 같습니다


fun saveLastVisitedPlace(place: Place) {
val editor = sharedPreferences.edit()
editor.putString("placeName", place.place)
editor.putString("roadAddressName", place.address)
editor.putString("categoryName", place.category)
editor.putString("yPos", place.yPos)
editor.putString("xPos", place.xPos)
editor.apply()
}

fun getLastVisitedPlace(): Place? {
val placeName = sharedPreferences.getString("placeName", null)
val roadAddressName = sharedPreferences.getString("roadAddressName", null)
val categoryName = sharedPreferences.getString("categoryName", null)
val yPos = sharedPreferences.getString("yPos", null)
val xPos = sharedPreferences.getString("xPos", null)

return if (placeName != null && roadAddressName != null && categoryName != null && yPos != null && xPos != null) {
Place("", placeName, roadAddressName, categoryName, xPos, yPos)
} else {
null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package campus.tech.kakao.map.data

import campus.tech.kakao.map.data.dao.PlaceDao
import campus.tech.kakao.map.data.entity.PlaceEntity
import campus.tech.kakao.map.data.entity.PlaceLogEntity
import campus.tech.kakao.map.domain.model.Place
import campus.tech.kakao.map.domain.repository.PlaceRepository
import javax.inject.Inject

open class PlaceLocalDataRepository @Inject constructor(
private val placeDao: PlaceDao,
) : PlaceRepository {

override suspend fun getPlaces(placeName: String): List<Place> {
return placeDao.getPlaces(placeName).map { it.toPlace() }
}

override suspend fun updatePlaces(places: List<Place>) {
placeDao.deleteAllPlaces()
placeDao.insertPlaces(places.map {
PlaceEntity(it.id, it.place, it.address, it.category, it.xPos, it.yPos)
})
}

override suspend fun getPlaceById(id: String): Place? {
return placeDao.getPlaceById(id)?.toPlace()
}

override suspend fun updateLogs(logs: List<Place>) {
placeDao.deleteAllLogs()
placeDao.insertLogs(logs.map { PlaceLogEntity(it.id, it.place) })
}

override suspend fun removeLog(id: String) {
placeDao.removeLog(id)

}

override suspend fun getLogs(): List<Place> {
return placeDao.getLogs().map { it.toPlace() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package campus.tech.kakao.map.data

import android.content.Context
import campus.tech.kakao.map.BuildConfig
import campus.tech.kakao.map.data.dao.PlaceDao
import campus.tech.kakao.map.data.net.KakaoApi
import campus.tech.kakao.map.domain.model.Place
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject

class PlaceRemoteDataRepository @Inject constructor(
private val placeDao: PlaceDao,
private val kakaoApi: KakaoApi
) : PlaceLocalDataRepository(placeDao){
override suspend fun getPlaces(placeName: String): List<Place> {
return withContext(Dispatchers.IO) {
val resultPlaces = mutableListOf<Place>()
for (page in 1..3) {
val response = kakaoApi.getSearchKeyword(
key = BuildConfig.KAKAO_REST_API_KEY,
query = placeName,
size = 15,
page = page
)
if (response.isSuccessful) {
Comment on lines +20 to +27

Choose a reason for hiding this comment

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

아마도 페이지네이션 이나 한번에 3개의 페이지를 불러오고 싶은 상황인것 같습니다. 이럴때는 내부에서 loop를 도는것 보다는 getPlaces() 메소드 자체가 3번 호출되는 방향이 좀더 적절하지 않을까 싶습니다

response.body()?.documents?.let { resultPlaces.addAll(it) }
} else throw RuntimeException("통신 에러 발생")
}
updatePlaces(resultPlaces)
resultPlaces
}
}
}
Loading