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주차 과제_STEP 3 #108

Open
wants to merge 54 commits into
base: ksc1008
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
278e0ec
integrate files from previous project
MyStoryG Jul 7, 2024
cff708e
project: setup gradle repository
ksc1008 Jul 8, 2024
3b9a499
feat: removed unused activity, setup build.gradle for API support
ksc1008 Jul 8, 2024
b8ea5bf
docs: updated READ.md
ksc1008 Jul 8, 2024
23c9495
feat: add KeywordSearch DTO
ksc1008 Jul 8, 2024
8d90216
BREAKING CHANGE: renamed package name to ksc.campus.tech.kakao.map
ksc1008 Jul 8, 2024
a3e146a
feat: removed methods about SearchResultTable
ksc1008 Jul 8, 2024
1f5be4f
feat: now support batch searching
ksc1008 Jul 8, 2024
5c3372b
feat: now support batch searching
ksc1008 Jul 8, 2024
da98320
refactor: removed log for debugging
ksc1008 Jul 8, 2024
8305fd1
refactor($SearchResultRepository): added instance property for simple…
ksc1008 Jul 8, 2024
7d18f90
style: fixed result item size issue
ksc1008 Jul 8, 2024
59c7f4c
fix ($SearchResultRepository): restored to original code due to compi…
ksc1008 Jul 8, 2024
09dcd50
feat ($SearchKakaoHelper): implement better category parsing algorithm
ksc1008 Jul 8, 2024
bdf72ab
refactor: removed unused dummy data
ksc1008 Jul 8, 2024
16adacd
feat: now SearchResult data class contains id got from search API
ksc1008 Jul 8, 2024
03d3030
fix: fixed bug that duplicate result showed when searching same keywo…
ksc1008 Jul 8, 2024
ee98ddb
fix: fixed bug that same page appeared on search
ksc1008 Jul 8, 2024
341c5d4
refactor: grouped several procedure to one function
ksc1008 Jul 8, 2024
ba4769d
refactor($SearchKeywordAdapter): replaced two callbacks with an inter…
ksc1008 Jul 8, 2024
c6039a9
style: reformatted all codes
ksc1008 Jul 8, 2024
f2ad63d
docs: added documentary for lastSearchId property
ksc1008 Jul 8, 2024
449df9a
feat: add Kakao Map Fragment
ksc1008 Jul 10, 2024
931fdc9
docs: update README.md
ksc1008 Jul 10, 2024
0b4376c
feat: implement kakaomap function
ksc1008 Jul 10, 2024
2112810
feat: implement switch from map to search menu on search button click
ksc1008 Jul 10, 2024
8c6ac17
feat: implement switch to map from search menu on back button click
ksc1008 Jul 10, 2024
63c6f01
feat: implement kakaomap function
ksc1008 Jul 10, 2024
fa4674b
feat: implement switch from map to search menu on search button click
ksc1008 Jul 10, 2024
a48c8d2
feat: implement switch to map from search menu on back button click
ksc1008 Jul 10, 2024
ec1e3b7
Merge remote-tracking branch 'origin/step2' into step2
ksc1008 Jul 10, 2024
01b5151
style: reformatted codes
ksc1008 Jul 12, 2024
a8f03c5
refactor: removed unused parameter for callback
ksc1008 Jul 12, 2024
ae75eed
style: resolved TO-DO
ksc1008 Jul 12, 2024
75191eb
feat: implement my own network connector!
ksc1008 Jul 12, 2024
45fda8d
refactor: extracted magic numbers to static const value
ksc1008 Jul 12, 2024
a08096e
refactor: reformatted some codes & removed unused import directive
ksc1008 Jul 12, 2024
2d1a03b
docs: Update README.md
ksc1008 Jul 12, 2024
4bb38bf
refactor: delete unused methods, renamed to more proper class name
ksc1008 Jul 12, 2024
d0c4356
refactor: moved repositories to repositories package
ksc1008 Jul 12, 2024
65916a8
Merge remote-tracking branch 'origin/step3' into step3
ksc1008 Jul 12, 2024
94ceb80
refactor: added @Serializable annotation to DTO
ksc1008 Jul 15, 2024
9c0374b
fix: add dependency for serialization feature
ksc1008 Jul 15, 2024
9555032
refactor($KeywordSearchResponse): renamed to camel-case name
ksc1008 Jul 15, 2024
19d43c5
Merge branch 'step2' into step3
ksc1008 Jul 15, 2024
771bf39
fix: fixed annotation to work with kotlinx.serialize
ksc1008 Jul 15, 2024
d627d43
fix: renamed variable names
ksc1008 Jul 15, 2024
80eabb8
style: used emptyList(), instead of listOf() for empty list initializ…
ksc1008 Jul 15, 2024
1337557
refactor: removed DiffUtil, used ListAdapter instead.
ksc1008 Jul 15, 2024
74c3d4b
refactor: moved enum class out of the companion object
ksc1008 Jul 15, 2024
a8bfdfb
refactor: now SearchResultAdapter inherits listAdapter
ksc1008 Jul 15, 2024
c58deff
feat: synced lifecycle of kakaoMapView to lifecycle of fragment
ksc1008 Jul 15, 2024
aabf0dc
Merge branch 'step2' into step3
ksc1008 Jul 15, 2024
1b12651
style: reformatted codes
ksc1008 Jul 15, 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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,37 @@
# android-map-search

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

## 프로그래밍 요구 사항
- 검색 데이터는 카카오로컬 API를 사용한다.
- 카카오 API 사용을 위한 앱 키를 외부에 노출하지 않는다.
- 가능한 MVVM 아키텍처 패턴을 적용하도록 한다.
- 코드 컨벤션을 준수하며 프로그래밍한다.

## 구현할 기능 목록
- KAKAO Search Service 구현
- SearchResultRepository 리스트를 KAKAO Search Service를 통해 불러오도록 변경

# 2단계

## 구현할 기능 목록
- 카카오 맵 API와 연동하여 맵을 보여주는 기능 추가
- 검색 창 선택시 검색 화면으로 전환하는 기능 추가
- 뒤로가기 콜백 처리

# 3단계

## 구현할 기능 목록
- 웹 서버와 통신하는 HTTP Connection 래퍼 클래스 작성
- HTTP Connection을 이용해 카카오 API에서 데이터를 받아오는 클래스 작성
- 받아온 Json 데이터를 DTO로 파싱하는 기능 구현
- 기존 Retrofit 기반 웹 서비스 객체를 직접 구현한 클래스로 대체
17 changes: 15 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
kotlin("plugin.serialization") version "1.9.0"
}

fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key)

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

defaultConfig {
applicationId = "campus.tech.kakao.map"
applicationId = "ksc.campus.tech.kakao.map"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

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

}

buildTypes {
Expand All @@ -36,11 +45,14 @@ android {

buildFeatures {
viewBinding = true
buildConfig = true
}
}

dependencies {

implementation ("androidx.activity:activity-ktx:1.2.2")
implementation ("androidx.fragment:fragment-ktx:1.3.3")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
Expand All @@ -51,6 +63,7 @@ dependencies {
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("com.kakao.maps.open:android:2.9.5")
implementation("androidx.activity:activity:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="ksc.campus.tech.kakao.map"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
Expand All @@ -15,7 +16,7 @@
android:theme="@style/Theme.Map"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".views.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
11 changes: 0 additions & 11 deletions app/src/main/java/campus/tech/kakao/map/MainActivity.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package ksc.campus.tech.kakao.map.models

import android.util.Log
import ksc.campus.tech.kakao.map.models.dto.KeywordSearchResponse
import ksc.campus.tech.kakao.map.models.mynetwork.MyNetworkCallbacks
import ksc.campus.tech.kakao.map.models.mynetwork.MyHttpHelper

object KakaoSearchService {
private val categoryGroupCodeToDescription: HashMap<String, String> = hashMapOf(
Pair("MT1", "대형마트"),
Pair("CS2", "편의점"),
Pair("PS3", "어린이집, 유치원"),
Pair("SC4", "학교"),
Pair("AC5", "학원"),
Pair("PK6", "주차장"),
Pair("OL7", "주유소, 충전소"),
Pair("SW8", "지하철역"),
Pair("BK9", "은행"),
Pair("CT1", "문화시설"),
Pair("AG2", "중개업소"),
Pair("PO3", "공공기관"),
Pair("AT4", "관광명소"),
Pair("AD5", "숙박"),
Pair("FD6", "음식점"),
Pair("CE7", "카페"),
Pair("HP8", "병원"),
Pair("PM9", "약국")
)

/**
* 요청이 유효한지 검증하기 위해 사용.
*
* 현재 진행중인 쿼리가 유저가 마지막으로 보낸 쿼리인지 검증한다.
*
* 유저가 새로운 쿼리를 보낼 때마다 [lastSearchId] 값이 1씩 증가한다.
*/
private var lastSearchId: Int = 0

private fun isQueryValid(query: String): Boolean = query.isNotBlank()


private fun parseCategory(category: String) =
category.split('>').last().trim().replace(",", ", ")

private fun responseToResultArray(response: KeywordSearchResponse?): List<SearchResult>{
if(response == null)
return mutableListOf()
val result = mutableListOf<SearchResult>()

for (doc in response.documents) {
result.add(
SearchResult(
doc.id,
doc.placeName,
doc.addressName,
categoryGroupCodeToDescription.getOrDefault(
doc.categoryGroupCode,
parseCategory(doc.categoryName)
)
)
)
}
return result
}


fun batchSearchByKeyword(
query: String,
apiKey: String,
batchCount: Int,
onResponse: ((results: List<SearchResult>) -> Unit)?
) {
lastSearchId++

batchSearchByKeyword(lastSearchId, query, apiKey, 1, batchCount, onResponse)
}


private fun batchSearchByKeyword(
searchId: Int,
query: String,
apiKey: String,
page: Int,
batchCount: Int,
onResponse: ((results: List<SearchResult>) -> Unit)?
) {
if (page > batchCount)
return

if (!isQueryValid(query))
return

val myService = MyHttpHelper()
myService.run("KakaoAK $apiKey", query, page, object:MyNetworkCallbacks<KeywordSearchResponse>{
override fun onResponse(response: KeywordSearchResponse) {
val result = responseToResultArray(response)
if (lastSearchId != searchId) {
return
}
onResponse?.invoke(result)
if (!response.meta.isEnd) {
batchSearchByKeyword(
searchId,
query,
apiKey,
page + 1,
batchCount,
onResponse
)
}
}

override fun onError(errorMessage: String) {
Log.e("KSC", "request failed")
Log.e("KSC", "Message: $errorMessage")
}

override fun onTimeout() {
Log.e("KSC", "connection timeout")
}

})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package ksc.campus.tech.kakao.map.models

import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.provider.BaseColumns
import ksc.campus.tech.kakao.map.models.contracts.SearchKeywordContract
import ksc.campus.tech.kakao.map.models.contracts.SearchResultContract

data class SearchResult(val id: String, val name: String, val address: String, val type: String)

class SearchDbHelper(context: Context) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase?) {
db?.execSQL(SearchKeywordContract.CREATE_QUERY)
}

override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
if (oldVersion <= 1) {
db?.execSQL(SearchKeywordContract.CREATE_QUERY)
}
if (oldVersion in 2..2) {
db?.execSQL(SearchKeywordContract.DROP_QUERY)
db?.execSQL(SearchKeywordContract.CREATE_QUERY)
}
if(oldVersion in 1 .. 3){
db?.execSQL(SearchResultContract.DROP_QUERY)
}
}

fun insertOrReplaceKeyword(keyword: String) {
val db = writableDatabase
val contentValues = ContentValues().apply {
put(SearchKeywordContract.COLUMN_KEYWORD, keyword)
}

db.replace(SearchKeywordContract.TABLE_NAME, null, contentValues)
}

fun deleteKeyword(keyword: String) {
val db = writableDatabase
db.delete(
SearchKeywordContract.TABLE_NAME,
"${SearchKeywordContract.COLUMN_KEYWORD} = ?",
arrayOf(keyword)
)
}

private fun getAllSearchKeywordFromCursor(cursor: Cursor?): List<String> {
val result = mutableListOf<String>()

try {
while (cursor?.moveToNext() == true) {
result.add(cursor.getString(1))
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
if (cursor != null && !cursor.isClosed) {
cursor.close()
}
}

return result
}

fun queryAllSearchKeywords(): List<String> {
val db = readableDatabase
val cursor = db.rawQuery("SELECT * FROM ${SearchKeywordContract.TABLE_NAME}", null)

return getAllSearchKeywordFromCursor(cursor)
}

companion object {
private var instance: SearchDbHelper? = null
const val DATABASE_VERSION = 3
const val DATABASE_NAME = "MapSearch2"

fun getInstance(context: Context): SearchDbHelper {
if (instance == null) {
instance = SearchDbHelper(context)
}
return instance as SearchDbHelper
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ksc.campus.tech.kakao.map.models.contracts

import android.provider.BaseColumns

object SearchKeywordContract : BaseColumns {
const val TABLE_NAME = "SEARCH_KEYWORD"
const val COLUMN_KEYWORD = "keyword"

const val CREATE_QUERY = "CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
"${BaseColumns._ID} INTEGER PRIMARY KEY AUTOINCREMENT, " +
"$COLUMN_KEYWORD TEXT," +
"UNIQUE($COLUMN_KEYWORD))"

const val DROP_QUERY = "DROP TABLE IF EXISTS $TABLE_NAME"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package ksc.campus.tech.kakao.map.models.contracts

import android.provider.BaseColumns._ID

object SearchResultContract {
const val TABLE_NAME = "SEARCH_RESULT"
const val COLUMN_NAME = "name"
const val COLUMN_ADDRESS = "address"
const val COLUMN_TYPE = "type"

const val CREATE_QUERY = "CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
"$_ID INTEGER PRIMARY KEY AUTOINCREMENT, " +
"$COLUMN_NAME TEXT, " +
"$COLUMN_ADDRESS TEXT, " +
"$COLUMN_TYPE TEXT)"

const val DROP_QUERY = "DROP TABLE IF EXISTS $TABLE_NAME"
}
Loading