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_장수민_3주차 과제(1단계) #48

Merged
merged 6 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
# android-map-search

### 기능 요구 사항
이전 단계와 기능은 비슷하나 **카카오로컬 API**를 사용한다.

- 검색어를 입력하면 검색 결과가 15개 이상 표시된다.
- 검색 결과 목록은 세로 스크롤이 된다.
- 입력한 검색어는 X를 눌러서 삭제할 수 있다.
- 검색 결과 목록에서 하나의 항목을 선택할 수 있다.
- 선택된 항목은 검색어 저장 목록에 추가된다.
- 저장된 검색어 목록은 가로 스크롤이 된다.
- 저장된 검색어는 X를 눌러서 삭제할 수 있다.
- 저장된 검색어는 앱을 재실행하여도 유지된다.

### 프로그래밍 요구 사항
- 검색 데이터는 카카오로컬 API를 사용한다.
- 카카오 API 사용을 위한 앱 키를 외부에 노출하지 않는다.
- 가능한 MVVM 아키텍처 패턴을 적용하도록 한다.
- 코드 컨벤션을 준수하며 프로그래밍한다.
7 changes: 7 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
Expand All @@ -15,6 +17,9 @@ android {
versionCode = 1
versionName = "1.0"

resValue("string", "kakao_api_key", getApiKey("KAKAO_API_KEY"))
buildConfigField("String", "KAKAO_REST_API_KEY", getApiKey("KAKAO_REST_API_KEY"))

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Expand All @@ -37,6 +42,7 @@ android {

buildFeatures {
viewBinding = true
buildConfig = true
}
}

Expand All @@ -61,3 +67,4 @@ dependencies {
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key)
23 changes: 14 additions & 9 deletions app/src/main/java/campus/tech/kakao/map/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package campus.tech.kakao.map

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import androidx.activity.viewModels
Expand All @@ -18,15 +19,14 @@ class MainActivity : AppCompatActivity() {
}

private val placeAdapter: PlaceAdapter by lazy {
PlaceAdapter(placeList,
PlaceAdapter(locationList,
LayoutInflater.from(this@MainActivity),
object :
PlaceAdapter.OnItemClickListener {
override fun onItemClick(position: Int) {
val item = placeAdapter.getItem(position)
val searchHistory = SearchHistory(item.name)
val searchHistory = SearchHistory(item.placeName)
viewModel.saveSearchHistory(searchHistory)
Log.d("실행", "저장")
}
}
)
Expand All @@ -41,12 +41,10 @@ class MainActivity : AppCompatActivity() {
val item = viewModel.searchHistoryList.value?.get(position)
if (item != null) {
mainBinding.search.setText(item.searchHistory)
Log.d("실행", "검색창")
}
}
override fun onXMarkClick(position: Int) {
viewModel.deleteSearchHistory(position)
Log.d("실행", "삭제")
}
}
)
Expand All @@ -57,6 +55,7 @@ class MainActivity : AppCompatActivity() {

private var placeList: List<Place> = emptyList()

private var locationList: List<Document> = emptyList()

Choose a reason for hiding this comment

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

locationList, placeList이 MainActivity에서 선언되어야 할 필요가 있을까요? 불필요한 필드 선언은 코드를 복잡하게 만듭니다. :)
만약 불필요하다면 코드를 지워주세요~!

Copy link
Author

Choose a reason for hiding this comment

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

넵!! 처음에 추가해 둔 placeList 보고 같이 선언해 둔 건데 그럴 필요가 없었던 것 같습니다! 2단계 진행하면서 지우도록 하겠습니다!

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainBinding = ActivityMainBinding.inflate(layoutInflater)
Expand Down Expand Up @@ -91,15 +90,21 @@ class MainActivity : AppCompatActivity() {

private fun setupSearchEditText(mainBinding: ActivityMainBinding) {
val searchEditText = mainBinding.search
val handler = Handler(Looper.getMainLooper())
val delayMillis = 800L

searchEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}

override fun afterTextChanged(s: Editable?) {
val searchText = searchEditText.text.toString()
viewModel.getSearchResult(searchText)
val searchText = s.toString().trim()

handler.removeCallbacksAndMessages(null)
handler.postDelayed({

Choose a reason for hiding this comment

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

만약 불필요한 네트워크 콜을 줄이고, 버퍼를 주고싶은 것이라면 postDelayed대신 텍스트 변경 이벤트가 throttleing되도록 만들어 보세요. 코루틴을 활용하여 적용해보시면 좋을 것 같네요 :)

Copy link
Author

Choose a reason for hiding this comment

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

넵...!! 알려주셔서 감사합니다!

viewModel.getPlace(searchText)
}, delayMillis)
}
})
}
Expand All @@ -110,7 +115,7 @@ class MainActivity : AppCompatActivity() {
})
viewModel.getSearchHistoryList()

viewModel.placeList.observe(this@MainActivity, Observer {
viewModel.locationList.observe(this@MainActivity, Observer {
placeAdapter.setData(it)
mainBinding.emptyMainText.visibility = if (it.isNullOrEmpty()) View.VISIBLE else View.GONE
})
Expand Down
38 changes: 38 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/MainViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package campus.tech.kakao.map

import android.content.Context
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import campus.tech.kakao.map.RetrofitInstance.retrofitService
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class MainViewModel(context: Context) : ViewModel() {
private val dbHelper: DBHelper = DBHelper(context)
Expand All @@ -12,6 +17,7 @@ class MainViewModel(context: Context) : ViewModel() {

private var _placeList = MutableLiveData<List<Place>>()
private val _searchHistoryList = MutableLiveData<List<SearchHistory>>()
private var _locationList = MutableLiveData<List<Document>>()

init {
_searchHistoryList.value = getSearchHistory()
Expand All @@ -23,6 +29,9 @@ class MainViewModel(context: Context) : ViewModel() {
val placeList: LiveData<List<Place>>
get() = _placeList

val locationList: LiveData<List<Document>>
get() = _locationList

fun insertPlace(place: Place) {
dbHelper.insert(db, place)
}
Expand Down Expand Up @@ -75,4 +84,33 @@ class MainViewModel(context: Context) : ViewModel() {
preferenceManager.deleteArrayListItem(Constants.SEARCH_HISTORY_KEY, position)
getSearchHistoryList()
}

fun getPlace(query: String) {
if (query.isEmpty()) {
_locationList.value = emptyList()
} else {
retrofitService.getPlaces("KakaoAK "+BuildConfig.KAKAO_REST_API_KEY, query).enqueue(object : Callback<Location> {

Choose a reason for hiding this comment

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

뷰모델이 직접 retrofitService에 접근하는 대신, 이러한 데이터 조작 역할을 repository 컴포넌트를 만들어 위임해보세요. 이전에 전달드렸던 안드로이드 공식 문서의 아키텍쳐 부분을 참고하여 수정해주세요.

Copy link
Author

Choose a reason for hiding this comment

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

넵! 수정하겠습니다!

override fun onResponse(
call: Call<Location>,
response: Response<Location>
) {
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
_locationList.postValue(body.documents)
Log.d("성공", ""+ body.documents)
} else {
_locationList.postValue(emptyList())
}
} else {
Log.d("태그",response.code().toString())
}
}

override fun onFailure(call: Call<Location>, t: Throwable) {
Log.d("error", ""+ t)
}
})
}
}
}
35 changes: 35 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/Place.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,42 @@
package campus.tech.kakao.map

import com.google.gson.annotations.SerializedName
data class Place (
val name: String,
val address: String,
val category: String,
)

// API
data class Location (
@SerializedName("documents") val documents: List<Document>,
@SerializedName("meta") val meta: Meta
)

data class Document (
@SerializedName("address_name") val addressName: String,
@SerializedName("category_group_code") val categoryGroupCode: String,
@SerializedName("category_group_name") val categoryGroupName: String,
@SerializedName("category_name") val categoryName: String,
@SerializedName("distance") val distance: String,
@SerializedName("id") val id: String,
@SerializedName("phone") val phone: String,
@SerializedName("place_name") val placeName: String,
@SerializedName("place_url") val placeUrl: String,
@SerializedName("road_address_name") val roadAddressName: String,
@SerializedName("x") val x: String,
@SerializedName("y") val y: String
)

data class Meta(
@SerializedName("is_end") val isEnd: Boolean,
@SerializedName("pageable_count") val pageableCount: Int,
@SerializedName("same_name") val sameName: SameName,
@SerializedName("total_count") val totalCount: Int
)

data class SameName(
@SerializedName("keyword") val keyword: String,
@SerializedName("region") val region: List<String>,
@SerializedName("selected_region") val selectedRegion: String
)
18 changes: 12 additions & 6 deletions app/src/main/java/campus/tech/kakao/map/PlaceAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.RecyclerView

class PlaceAdapter(var items: List<Place>, val inflater: LayoutInflater, var itemClickListener: OnItemClickListener): RecyclerView.Adapter<PlaceAdapter.PlaceViewHolder>() {
class PlaceAdapter(var items: List<Document>, val inflater: LayoutInflater, var itemClickListener: OnItemClickListener): RecyclerView.Adapter<PlaceAdapter.PlaceViewHolder>() {

interface OnItemClickListener {
fun onItemClick(position: Int) {}
Expand All @@ -25,21 +25,27 @@ class PlaceAdapter(var items: List<Place>, val inflater: LayoutInflater, var ite
}

override fun onBindViewHolder(holder: PlaceAdapter.PlaceViewHolder, position: Int) {
holder.name.text = items[position].name
holder.address.text = items[position].address
holder.category.text = items[position].category
holder.name.text = items[position].placeName
holder.address.text = items[position].addressName
holder.category.text = getLastCategory(items[position].categoryName)
}

override fun getItemCount(): Int {
return items.size
}

fun setData(searchResults: List<Place>) {
private fun getLastCategory(input: String): String {
val categories = input.split(">")
val lastCategory = categories.lastOrNull()?.trim()

return lastCategory ?: ""
}
fun setData(searchResults: List<Document>) {
items = searchResults
notifyDataSetChanged()
}

fun getItem(position: Int): Place {
fun getItem(position: Int): Document {
return items[position]
}

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

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitInstance {

Choose a reason for hiding this comment

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

추후 DI 개념을 학습하게 되신다면, 어디에 선언해야 하는지에 대한 고민에 답을 얻으실 수 있으실거에요. 현재로서는 뷰모델에 선언보다는 object로 선언하는 것이 더 좋아보입니다 :)

Copy link
Author

Choose a reason for hiding this comment

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

네! 코드리뷰 해주셔서 감사합니다!

Copy link
Author

Choose a reason for hiding this comment

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

혹시 제가 컨플릭을 고쳤는데... 괜찮은 걸까요!?!! 제가 하면 안되는 것은 아니였겠죠...??

Choose a reason for hiding this comment

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

아니요! 컨플릭트 해결해주셔야 제가 머지가 가능해서요. ㅎㅎ PR은 머지하도록 하겠습니다.

val retrofitService = Retrofit.Builder()

Choose a reason for hiding this comment

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

만약 여러 쓰레드에서 동시에 retrofitService에 접근하게 된다면 어떻게 될까요? 이에 대해서 생각해보시고 적절한 해결방법을 코드에 적용해보세요. :)

Copy link
Author

Choose a reason for hiding this comment

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

아... 생각을 못 하고 있었습니다!! 적절한 방법을 찾아보겠습니다!

.baseUrl("https://dapi.kakao.com/v2/local/search/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(RetrofitService::class.java)
}
15 changes: 15 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/RetrofitService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package campus.tech.kakao.map

import com.google.gson.internal.GsonBuildConfig
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query

interface RetrofitService {
@GET("keyword")
fun getPlaces(
@Header("Authorization") authorization: String = "KakaoAK "+BuildConfig.KAKAO_REST_API_KEY ,
@Query("query") query: String
): Call<Location>
}
2 changes: 1 addition & 1 deletion app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="20dp"
android:paddingHorizontal="15dp"
tools:context=".MainActivity">

<EditText
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/layout/place_item.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="10dp">
android:padding="5dp">

<ImageView
android:id="@+id/marker"
Expand All @@ -23,6 +23,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/address"
android:layout_marginStart="10dp"
android:maxLines="1"
android:text="place"
android:textSize="17sp"
/>
Expand Down