diff --git a/README.md b/README.md index 91dbd079..2bd5276d 100644 --- a/README.md +++ b/README.md @@ -1 +1,21 @@ # android-map-location + +## 1단계 - 카카오맵 API 심화 +### 기능 요구 사항 +- 저장된 검색어를 선택하면 해당 검색어의 검색 결과가 표시된다. +- 검색 결과 목록 중 하나의 항목을 선택하면 해당 항목의 위치를 지도에 표시한다. +- 앱 종료 시 마지막 위치를 저장하여 다시 앱 실행 시 해당 위치로 포커스 한다. +- 카카오지도 onMapError() 호출 시 에러 화면을 보여준다. +### 프로그래밍 요구 사항 +- BottomSheet를 사용한다. +- 카카오 API 사용을 위한 앱 키를 외부에 노출하지 않는다. +- 가능한 MVVM 아키텍처 패턴을 적용하도록 한다. +- 코드 컨벤션을 준수하며 프로그래밍한다. + +## 2단계 - 테스트 +### 기능 요구 사항 +- 테스트 코드를 작성한다. +### 프로그래밍 요구 사항 +- 카카오 API 사용을 위한 앱 키를 외부에 노출하지 않는다. +- 가능한 MVVM 아키텍처 패턴을 적용하도록 한다. +- 코드 컨벤션을 준수하며 프로그래밍한다. \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9b4327c9..760dea52 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,6 +48,9 @@ android { viewBinding = true buildConfig = true } + testOptions { + animationsDisabled = true + } } dependencies { @@ -75,5 +78,6 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test:rules:1.5.0") androidTestImplementation("androidx.test.espresso:espresso-intents:3.5.1") + androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1") } fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key) \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/MapActivityUITest.kt b/app/src/androidTest/java/campus/tech/kakao/map/MapActivityUITest.kt new file mode 100644 index 00000000..ca9159a8 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/MapActivityUITest.kt @@ -0,0 +1,22 @@ +package campus.tech.kakao.map + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.ActivityScenarioRule +import org.junit.Rule +import org.junit.Test + + +class MapActivityUITest { + @get:Rule + val mapActivityRule = ActivityScenarioRule(MapActivity::class.java) + + @Test + fun 검색창_보이는지_확인(){ + val mapSearch = onView(withId(R.id.map_search)) + mapSearch.check(matches(isDisplayed())) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityUITest.kt b/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityUITest.kt new file mode 100644 index 00000000..5fd92d8c --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityUITest.kt @@ -0,0 +1,53 @@ +package campus.tech.kakao.map + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withParent +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.core.AllOf.allOf +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SearchActivityUITest { + @get:Rule + val searchActivityRule = ActivityScenarioRule(SearchActivity::class.java) + + @Test + fun 검색창_표시_확인(){ + val search = onView(withId(R.id.search)) + search.check(matches(isDisplayed())) + } + + @Test + fun X_버튼_확인(){ + val clear = onView(withId(R.id.search_clear)) + clear.check(matches(isDisplayed())) + } + @Test + fun 입력한_검색어_X_눌러_삭제() { + val query = "박물관" + val search = onView(allOf(withId(R.id.search), withParent(withId(R.id.search_main)))) + val clear = onView(withId(R.id.search_clear)) + search.perform(replaceText(query)) + clear.perform(click()) + search.check(matches(withText(""))) + } + + @Test + fun 검색_결과_클릭시_검색_결과_나오는지_확인(){ + val query = "박물관" + val search = onView(allOf(withId(R.id.search), withParent(withId(R.id.search_main)))) + search.perform(replaceText(query)) + val searchResult = onView(withId(R.id.search_result_recycler_view)) + searchResult.check(matches(isDisplayed())) + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt deleted file mode 100644 index 95b43803..00000000 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package campus.tech.kakao.map - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/java/campus/tech/kakao/map/MainViewModel.kt b/app/src/main/java/campus/tech/kakao/map/MainViewModel.kt index d5344630..4a699f38 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainViewModel.kt @@ -3,9 +3,11 @@ package campus.tech.kakao.map import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData -import campus.tech.kakao.map.DBHelper.SearchWordDbHelper -import campus.tech.kakao.map.DTO.Document -import campus.tech.kakao.map.DTO.SearchWord +import campus.tech.kakao.map.dbHelper.SearchWordDbHelper +import campus.tech.kakao.map.dto.Document +import campus.tech.kakao.map.dto.MapPositionContract +import campus.tech.kakao.map.dto.SearchWord +import campus.tech.kakao.map.MyApplication.Companion.mapPosition import campus.tech.kakao.map.RetrofitData.Companion.getInstance class MainViewModel(application: Application): AndroidViewModel(application) { @@ -21,7 +23,7 @@ class MainViewModel(application: Application): AndroidViewModel(application) { } private fun wordfromDocument(document: Document): SearchWord { - return SearchWord(document.placeName, document.categoryGroupName, document.addressName) + return SearchWord(document.placeName, document.addressName, document.categoryGroupName) } fun deleteWord(word: SearchWord){ wordDbHelper.deleteWord(word) @@ -39,4 +41,11 @@ class MainViewModel(application: Application): AndroidViewModel(application) { super.onCleared() wordDbHelper.close() } + + fun getMapInfo(document: Document){ + mapPosition.setPreferences(MapPositionContract.PREFERENCE_KEY_LATITUDE, document.latitude) + mapPosition.setPreferences(MapPositionContract.PREFERENCE_KEY_LONGITUDE, document.longitude) + mapPosition.setPreferences(MapPositionContract.PREFERENCE_KEY_PLACENAME, document.placeName) + mapPosition.setPreferences(MapPositionContract.PREFERENCE_KEY_ADDRESSNAME, document.addressName) + } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MapActivity.kt b/app/src/main/java/campus/tech/kakao/map/MapActivity.kt index f71c0cb5..a1690b73 100644 --- a/app/src/main/java/campus/tech/kakao/map/MapActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MapActivity.kt @@ -1,31 +1,67 @@ package campus.tech.kakao.map import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Color import android.os.Bundle import android.util.Log +import android.view.View import android.widget.LinearLayout +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import campus.tech.kakao.map.MyApplication.Companion.mapPosition +import com.google.android.material.bottomsheet.BottomSheetBehavior import com.kakao.vectormap.KakaoMap import com.kakao.vectormap.KakaoMapReadyCallback +import com.kakao.vectormap.LatLng import com.kakao.vectormap.MapLifeCycleCallback import com.kakao.vectormap.MapView +import com.kakao.vectormap.camera.CameraUpdate +import com.kakao.vectormap.camera.CameraUpdateFactory +import com.kakao.vectormap.label.Label +import com.kakao.vectormap.label.LabelLayer +import com.kakao.vectormap.label.LabelOptions +import com.kakao.vectormap.label.LabelStyle +import com.kakao.vectormap.label.LabelStyles import java.lang.Exception class MapActivity : AppCompatActivity() { private lateinit var mapView: MapView private var map: KakaoMap? = null private lateinit var searchBar: LinearLayout + private var latitude = 37.402005 + private var longitude = 127.108621 + private lateinit var placeName:String + private lateinit var addressName:String + private var styles: LabelStyles? = null + private lateinit var options:LabelOptions + private var layer: LabelLayer? = null + private var label: Label? = null + private lateinit var bitmapImage: Bitmap + private lateinit var markerImage: Bitmap + private lateinit var bottomSheetBehavior: BottomSheetBehavior + private val bottomSheet by lazy { findViewById(R.id.bottom_sheet) } + private val bottomSheetName by lazy { findViewById(R.id.name) } + private val bottomSheetAddress by lazy { findViewById(R.id.address) } + companion object{ + var documentClicked = false + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_map) mapView = findViewById(R.id.map_view) + setMapInfo() mapView.start(object : MapLifeCycleCallback() { override fun onMapDestroy() { } override fun onMapError(p0: Exception?) { - Log.e("MapActivity", "onMapError: ${p0?.message}", p0) + setContentView(R.layout.map_error) + val errorText = findViewById(R.id.map_error_text) + errorText.text = p0?.message } }, object: KakaoMapReadyCallback() { @@ -33,17 +69,38 @@ class MapActivity : AppCompatActivity() { map = kakaoMap } + override fun getPosition(): LatLng { + return LatLng.from(latitude, longitude) + } + + override fun getZoomLevel(): Int { + return 17 + } }) searchBar = findViewById(R.id.search_bar) searchBar.setOnClickListener { val intent = Intent(this@MapActivity, SearchActivity::class.java) startActivity(intent) } + initBottomSheet() } override fun onResume() { super.onResume() + setMapInfo() mapView.resume() + if(documentClicked){ + makeMarker() + setBottomSheet() + documentClicked = false + } + else{ + layer?.remove(label) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + val cameraUpdate: CameraUpdate = CameraUpdateFactory.newCenterPosition(LatLng.from(latitude, longitude)) + map?.moveCamera(cameraUpdate) + } override fun onPause() { @@ -51,4 +108,46 @@ class MapActivity : AppCompatActivity() { mapView.pause() } + private fun setMapInfo(){ + latitude = mapPosition.getPreferences("latitude","37.406960").toDouble() + longitude = mapPosition.getPreferences("longitude","127.110030").toDouble() + placeName = mapPosition.getPreferences("placeName","") + addressName = mapPosition.getPreferences("addressName","") + } + + private fun makeMarker(){ + bitmapImage = BitmapFactory.decodeResource(resources, R.drawable.marker) + markerImage = Bitmap.createScaledBitmap(bitmapImage, 100, 100, true) + styles = map?.labelManager?.addLabelStyles(LabelStyles.from(LabelStyle.from(markerImage).setTextStyles(40, Color.BLACK))) + if(styles != null){ + options = LabelOptions.from(LatLng.from(latitude, longitude)).setStyles(styles).setTexts(placeName) + layer = map?.labelManager?.layer + if(label != null){ + layer?.remove(label) + } + label = layer?.addLabel(options) + } + else{ + Log.e("MapActivity", "makeMarker: styles is null") + } + } + + private fun initBottomSheet(){ + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) + bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback(){ + override fun onStateChanged(bottomSheet: View, newState: Int) { + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { + } + + }) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + private fun setBottomSheet(){ + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + bottomSheetName.text = placeName + bottomSheetAddress.text = addressName + } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MyApplication.kt b/app/src/main/java/campus/tech/kakao/map/MyApplication.kt index 2d2aa825..b3091393 100644 --- a/app/src/main/java/campus/tech/kakao/map/MyApplication.kt +++ b/app/src/main/java/campus/tech/kakao/map/MyApplication.kt @@ -1,11 +1,18 @@ package campus.tech.kakao.map import android.app.Application +import campus.tech.kakao.map.dto.MapPositionPreferences + import com.kakao.vectormap.KakaoMapSdk class MyApplication : Application() { override fun onCreate() { super.onCreate() + mapPosition = MapPositionPreferences(this) KakaoMapSdk.init(this, BuildConfig.KAKAO_API_KEY) } + + companion object{ + lateinit var mapPosition : MapPositionPreferences + } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/RetrofitData.kt b/app/src/main/java/campus/tech/kakao/map/RetrofitData.kt index de3f13a0..c9a232ee 100644 --- a/app/src/main/java/campus/tech/kakao/map/RetrofitData.kt +++ b/app/src/main/java/campus/tech/kakao/map/RetrofitData.kt @@ -1,10 +1,10 @@ package campus.tech.kakao.map import androidx.lifecycle.MutableLiveData -import campus.tech.kakao.map.DTO.Document -import campus.tech.kakao.map.DTO.PlaceResponse -import campus.tech.kakao.map.DTO.UrlContract -import campus.tech.kakao.map.DTO.UrlContract.AUTHORIZATION +import campus.tech.kakao.map.dto.Document +import campus.tech.kakao.map.dto.PlaceResponse +import campus.tech.kakao.map.dto.UrlContract +import campus.tech.kakao.map.dto.UrlContract.AUTHORIZATION import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -28,8 +28,13 @@ class RetrofitData private constructor() { if (response.isSuccessful) { val documentList = mutableListOf() val body = response.body() - body?.documents?.forEach { - documentList.add(Document(it.placeName, it.categoryGroupName, it.addressName)) + body?.documents?.forEach {document -> + documentList.add(Document( + document.placeName, + document.categoryGroupName, + document.addressName, + document.longitude, + document.latitude)) } _documents.value = documentList } diff --git a/app/src/main/java/campus/tech/kakao/map/RetrofitService.kt b/app/src/main/java/campus/tech/kakao/map/RetrofitService.kt index 7f4e7be9..b1935976 100644 --- a/app/src/main/java/campus/tech/kakao/map/RetrofitService.kt +++ b/app/src/main/java/campus/tech/kakao/map/RetrofitService.kt @@ -1,6 +1,7 @@ package campus.tech.kakao.map -import campus.tech.kakao.map.DTO.PlaceResponse +import campus.tech.kakao.map.dto.PlaceResponse + import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Header diff --git a/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt b/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt index 3a10cb0b..98bbf48d 100644 --- a/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt @@ -10,8 +10,10 @@ import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import campus.tech.kakao.map.Adapter.DocumentAdapter -import campus.tech.kakao.map.Adapter.WordAdapter + +import campus.tech.kakao.map.adapter.DocumentAdapter +import campus.tech.kakao.map.adapter.WordAdapter + class SearchActivity : AppCompatActivity() { @@ -29,13 +31,22 @@ class SearchActivity : AppCompatActivity() { setupUI() searchResult.layoutManager = LinearLayoutManager(this) searchWordResult.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) - documentAdapter = DocumentAdapter(){ Document -> - model.addWord(Document) - } - wordAdapter = WordAdapter() { SearchWord -> - model.deleteWord(SearchWord) - } - search.doOnTextChanged { text, start, before, count -> + + documentAdapter = DocumentAdapter({ document -> + model.addWord(document) + },{document -> + model.getMapInfo(document) + finish() + }) + wordAdapter = WordAdapter( + { searchWord -> + model.deleteWord(searchWord) + },{ searchWord -> + model.searchLocalAPI(searchWord.name) + } + ) + search.doOnTextChanged { text, _, _, _ -> + val query = text.toString() if (query.isEmpty()){ noResult.visibility = View.VISIBLE @@ -71,7 +82,9 @@ class SearchActivity : AppCompatActivity() { }) } - fun setupUI(){ + + private fun setupUI(){ + search = findViewById(R.id.search) clear = findViewById(R.id.search_clear) noResult = findViewById(R.id.no_search_result) diff --git a/app/src/main/java/campus/tech/kakao/map/adapter/DocumentAdapter.kt b/app/src/main/java/campus/tech/kakao/map/adapter/DocumentAdapter.kt new file mode 100644 index 00000000..b141f6b9 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/adapter/DocumentAdapter.kt @@ -0,0 +1,60 @@ +package campus.tech.kakao.map.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.dto.Document +import campus.tech.kakao.map.MapActivity.Companion.documentClicked +import campus.tech.kakao.map.R + +class DocumentAdapter( + val addWord: (Document) -> Unit, + val sendDocumentInfo: (Document) -> Unit +): ListAdapter( + object : DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: Document, newItem: Document): Boolean { + return oldItem === newItem + } + + override fun areContentsTheSame(oldItem: Document, newItem: Document): Boolean { + return oldItem == newItem + } + + } +) { + private var placeClicked = { position:Int -> + val document: Document = getItem(position) + addWord(document) + sendDocumentInfo(document) + documentClicked = true + } + inner class ViewHolder( + itemView: View + ): RecyclerView.ViewHolder(itemView) { + val name:TextView = itemView.findViewById(R.id.name) + val address:TextView = itemView.findViewById(R.id.address) + val type:TextView = itemView.findViewById(R.id.type) + init { + itemView.setOnClickListener { + placeClicked(bindingAdapterPosition) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.place_item, parent, false)) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val document: Document = getItem(position) + holder.name.text = document.placeName + holder.address.text = document.addressName + holder.type.text = document.categoryGroupName + } +} + + diff --git a/app/src/main/java/campus/tech/kakao/map/adapter/WordAdapter.kt b/app/src/main/java/campus/tech/kakao/map/adapter/WordAdapter.kt new file mode 100644 index 00000000..7f9a30da --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/adapter/WordAdapter.kt @@ -0,0 +1,63 @@ +package campus.tech.kakao.map.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.dto.SearchWord + +class WordAdapter( + val deleteWord: (SearchWord) -> Unit, + val clickWord: (SearchWord) -> Unit +): ListAdapter( + object : DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: SearchWord, newItem: SearchWord): Boolean { + return oldItem === newItem + } + + override fun areContentsTheSame(oldItem: SearchWord, newItem: SearchWord): Boolean { + return oldItem == newItem + } + + } +) { + inner class ViewHolder( + itemView: View + ): RecyclerView.ViewHolder(itemView) { + val searchWord: TextView = itemView.findViewById(R.id.search_word) + val delete: ImageView = itemView.findViewById(R.id.x) + init { + delete.setOnClickListener { + deletedWords(bindingAdapterPosition) + } + searchWord.setOnClickListener { + clickedWord(bindingAdapterPosition) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.word_item, parent, false)) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val word = getItem(position) + holder.searchWord.text = word.name + } + + private val deletedWords = { position:Int -> + val word = getItem(position) + deleteWord(word) + } + + private val clickedWord = { position:Int -> + val word = getItem(position) + clickWord(word) + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/dbHelper/SearchWordDbHelper.kt b/app/src/main/java/campus/tech/kakao/map/dbHelper/SearchWordDbHelper.kt new file mode 100644 index 00000000..bf22f9bf --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/dbHelper/SearchWordDbHelper.kt @@ -0,0 +1,109 @@ +package campus.tech.kakao.map.dbHelper + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import campus.tech.kakao.map.dto.SearchWord +import campus.tech.kakao.map.dto.SearchWordContract +import campus.tech.kakao.map.dto.SearchWordContract.DB_VERSION + +class SearchWordDbHelper(context: Context): SQLiteOpenHelper( + context, SearchWordContract.DB_NAME, null, DB_VERSION) { + private val _searchWords = MutableLiveData>() + val searchSameSelection = "${SearchWordContract.COLUMN_NAME_NAME} = ? AND " + + "${SearchWordContract.COLUMN_NAME_ADDRESS} = ? AND " + + "${SearchWordContract.COLUMN_NAME_TYPE} = ?" + override fun onCreate(db: SQLiteDatabase?) { + createTable(db) + } + + private fun createTable(db: SQLiteDatabase?) { + db?.execSQL( + "CREATE TABLE ${SearchWordContract.TABLE_NAME} " + + "(${SearchWordContract.COLUMN_NAME_NAME} TEXT, " + + "${SearchWordContract.COLUMN_NAME_ADDRESS} TEXT, " + + "${SearchWordContract.COLUMN_NAME_TYPE} TEXT)" + ) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + db?.execSQL("DROP TABLE IF EXISTS ${SearchWordContract.TABLE_NAME}") + createTable(db) + } + + fun getSearchWords(): LiveData> { + return _searchWords + } + + fun addWord(word: SearchWord) { + val db = writableDatabase + if (existWord(word, db)){ + deleteWord(word) + } + val values = ContentValues() + values.put(SearchWordContract.COLUMN_NAME_NAME, word.name) + values.put(SearchWordContract.COLUMN_NAME_ADDRESS, word.address) + values.put(SearchWordContract.COLUMN_NAME_TYPE, word.type) + db.insert(SearchWordContract.TABLE_NAME, null, values) + db.close() + updateSearchWords() + } + + fun existWord(word: SearchWord, db: SQLiteDatabase): Boolean{ + val selection = searchSameSelection + val cursor = db.query( + SearchWordContract.TABLE_NAME, + arrayOf(SearchWordContract.COLUMN_NAME_NAME), + selection, + arrayOf(word.name, word.address, word.type), + null, + null, + "${SearchWordContract.COLUMN_NAME_NAME} DESC" + ).use { + it.moveToFirst() + } + return cursor + } + + fun deleteWord(word: SearchWord){ + val db = writableDatabase + val selection = searchSameSelection + val selectionArgs = arrayOf(word.name, word.address, word.type) + db.delete(SearchWordContract.TABLE_NAME, selection, selectionArgs) + updateSearchWords() + } + + fun updateSearchWords(){ + val db = readableDatabase + val resultList = mutableListOf() + val cursor = db.query( + SearchWordContract.TABLE_NAME, + arrayOf( + SearchWordContract.COLUMN_NAME_NAME, + SearchWordContract.COLUMN_NAME_ADDRESS, + SearchWordContract.COLUMN_NAME_TYPE + ), + null, + null, + null, + null, + null + ).use {cursor -> + while (cursor.moveToNext()) { + val name = cursor.getString( + cursor.getColumnIndexOrThrow(SearchWordContract.COLUMN_NAME_NAME) + ) + val address = cursor.getString( + cursor.getColumnIndexOrThrow(SearchWordContract.COLUMN_NAME_ADDRESS) + ) + val type = cursor.getString( + cursor.getColumnIndexOrThrow(SearchWordContract.COLUMN_NAME_TYPE)) + resultList.add(SearchWord(name, address, type)) + } + } + _searchWords.value = resultList + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/dto/Document.kt b/app/src/main/java/campus/tech/kakao/map/dto/Document.kt new file mode 100644 index 00000000..637f7059 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/dto/Document.kt @@ -0,0 +1,24 @@ +package campus.tech.kakao.map.dto + +import com.google.gson.annotations.SerializedName + +//필요 없는 데이터 주석화 +data class Document( + //val id: String, + @SerializedName("place_name") + val placeName: String, + //val category_name: String, + //val category_group_code: String, + @SerializedName("category_group_name") + val categoryGroupName: String, + //val phone: String, + @SerializedName("address_name") + val addressName: String, + //val road_address_name: String, + @SerializedName("x") + val longitude : String, + @SerializedName("y") + val latitude: String, + //val place_url: String, + //val distance: String, +) diff --git a/app/src/main/java/campus/tech/kakao/map/dto/MapPositionContract.kt b/app/src/main/java/campus/tech/kakao/map/dto/MapPositionContract.kt new file mode 100644 index 00000000..b0af0619 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/dto/MapPositionContract.kt @@ -0,0 +1,9 @@ +package campus.tech.kakao.map.dto + +object MapPositionContract { + const val PREFERENCE_NAME = "MapPosition" + const val PREFERENCE_KEY_LATITUDE = "latitude" + const val PREFERENCE_KEY_LONGITUDE = "longitude" + const val PREFERENCE_KEY_PLACENAME = "placeName" + const val PREFERENCE_KEY_ADDRESSNAME = "addressName" +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/dto/MapPositionPreferences.kt b/app/src/main/java/campus/tech/kakao/map/dto/MapPositionPreferences.kt new file mode 100644 index 00000000..bb52e031 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/dto/MapPositionPreferences.kt @@ -0,0 +1,17 @@ +package campus.tech.kakao.map.dto + +import android.content.Context + +class MapPositionPreferences(context: Context) { + + private val mapPosition = context.getSharedPreferences(MapPositionContract.PREFERENCE_NAME, Context.MODE_PRIVATE) + + fun setPreferences(key: String, value: String) { + mapPosition.edit().putString(key, value).apply() + } + + fun getPreferences(key: String, defaultValue: String): String { + return mapPosition.getString(key, defaultValue).toString() + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/dto/Meta.kt b/app/src/main/java/campus/tech/kakao/map/dto/Meta.kt new file mode 100644 index 00000000..60b41d75 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/dto/Meta.kt @@ -0,0 +1,14 @@ +package campus.tech.kakao.map.dto + +import com.google.gson.annotations.SerializedName + +data class Meta( + @SerializedName("total_count") + val totalCount: Int, + @SerializedName("pageable_count") + val pageableCount: Int, + @SerializedName("is_end") + val isEnd: Boolean, + @SerializedName("same_name") + val sameName: SameName +) diff --git a/app/src/main/java/campus/tech/kakao/map/dto/PlaceResponse.kt b/app/src/main/java/campus/tech/kakao/map/dto/PlaceResponse.kt new file mode 100644 index 00000000..3abf2475 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/dto/PlaceResponse.kt @@ -0,0 +1,6 @@ +package campus.tech.kakao.map.dto + +data class PlaceResponse( + val meta: Meta, + val documents: List +) diff --git a/app/src/main/java/campus/tech/kakao/map/dto/SameName.kt b/app/src/main/java/campus/tech/kakao/map/dto/SameName.kt new file mode 100644 index 00000000..833cddfe --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/dto/SameName.kt @@ -0,0 +1,7 @@ +package campus.tech.kakao.map.dto + +data class SameName( + val region: List, + val keyword: String, + val selected_region: String +) diff --git a/app/src/main/java/campus/tech/kakao/map/dto/SearchWord.kt b/app/src/main/java/campus/tech/kakao/map/dto/SearchWord.kt new file mode 100644 index 00000000..8ee333ed --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/dto/SearchWord.kt @@ -0,0 +1,3 @@ +package campus.tech.kakao.map.dto + +data class SearchWord(val name: String, val address: String, val type: String) diff --git a/app/src/main/java/campus/tech/kakao/map/dto/SearchWordContract.kt b/app/src/main/java/campus/tech/kakao/map/dto/SearchWordContract.kt new file mode 100644 index 00000000..2acdb952 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/dto/SearchWordContract.kt @@ -0,0 +1,11 @@ +package campus.tech.kakao.map.dto + +object SearchWordContract { + const val DB_NAME = "search_word.db" + const val DB_VERSION = 1 + const val TABLE_NAME = "SearchWord" + const val COLUMN_NAME_NAME = "name" + const val COLUMN_NAME_ADDRESS = "address" + const val COLUMN_NAME_TYPE = "type" + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/dto/UrlContract.kt b/app/src/main/java/campus/tech/kakao/map/dto/UrlContract.kt new file mode 100644 index 00000000..2d46eb85 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/dto/UrlContract.kt @@ -0,0 +1,8 @@ +package campus.tech.kakao.map.dto + +import campus.tech.kakao.map.BuildConfig + +object UrlContract { + const val BASE_URL = "https://dapi.kakao.com/v2/local/search/" + const val AUTHORIZATION = "KakaoAK ${BuildConfig.KAKAO_REST_API_KEY}" +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 24d17df2..00000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/activity_map.xml b/app/src/main/res/layout/activity_map.xml index 4c302a6f..05991d19 100644 --- a/app/src/main/res/layout/activity_map.xml +++ b/app/src/main/res/layout/activity_map.xml @@ -1,36 +1,41 @@ - - - + - - - + android:layout_height="match_parent" + tools:context=".MapActivity"> + + + + + + + + - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml index 97d7e02e..42d5ed8f 100644 --- a/app/src/main/res/layout/activity_search.xml +++ b/app/src/main/res/layout/activity_search.xml @@ -2,7 +2,9 @@ diff --git a/app/src/main/res/layout/bottom_sheet.xml b/app/src/main/res/layout/bottom_sheet.xml new file mode 100644 index 00000000..a020f694 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet.xml @@ -0,0 +1,30 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/map_error.xml b/app/src/main/res/layout/map_error.xml new file mode 100644 index 00000000..4d0a94b9 --- /dev/null +++ b/app/src/main/res/layout/map_error.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bfa0bb3e..d876f202 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,4 +3,8 @@ 검색어를 입력해 주세요. 검색 결과가 없습니다. word + 카카오 + 경기 성남시 분당구 백현동 532 + 오류가 발생했습니다.\n다시 시도해 주세요. + \ No newline at end of file diff --git a/app/src/test/java/campus/tech/kakao/map/FunTest.kt b/app/src/test/java/campus/tech/kakao/map/FunTest.kt new file mode 100644 index 00000000..95127e48 --- /dev/null +++ b/app/src/test/java/campus/tech/kakao/map/FunTest.kt @@ -0,0 +1,65 @@ +package campus.tech.kakao.map + +import android.app.Application +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModelStore +import campus.tech.kakao.map.dbHelper.SearchWordDbHelper +import campus.tech.kakao.map.dto.Document +import campus.tech.kakao.map.dto.SearchWord +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertFalse +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class FunTest { + private lateinit var model: MainViewModel + private lateinit var viewModelStore: ViewModelStore + private lateinit var context: Context + @Before + fun setUp() { + context = RuntimeEnvironment.getApplication() + viewModelStore = ViewModelStore() + model = MainViewModel(context as Application) + } + @Test + fun 검색어를_입력하면_검색_결과_표시(){ + val query = "이안아파트" + val expectedQueryResultName = arrayOf("이안금곡아파트", "대우이안아파트", "이안금곡아파트 관리사무소", + "대우이안아파트 정문", "이안동래센트럴시티아파트", "이안금곡아파트 입주자대표회의 전기차충전소", + "대우이안아파트 상가동", "CU 화명대우이안점", "대우이안아파트 지하주차장", + "이안공인중개사사무소", "부산시 북구 대우이안아파트 전기차충전소", "대우이안공인중개사사무소", + "삼계이안아파트", "이안센트럴포레장유1단지아파트", "성일이안시티아파트") + model.searchLocalAPI(query) + val actualQueryResult = model.documentList.value + actualQueryResult?.forEach { document -> + assert(expectedQueryResultName.contains(document.placeName)) + } + } + + @Test + fun 검색어_저장_되고_삭제도_되는지_확인(){ + val query = Document( + "이안아파트", "아파트", + "남양주", "10", + "10") + val expectedResult = SearchWord( + "이안아파트", "남양주", "아파트") + model.addWord(query) + val result = model.wordList.value?.contains(expectedResult) + if (result != null) assert(result) else assert(false) + + val query2 = SearchWord( + "이안아파트", "남양주", "아파트") + model.deleteWord(query2) + val result2 = model.wordList.value?.contains(query2) + if (result2 != null) assertFalse(result2) else assert(false) + } +} \ No newline at end of file