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

[#3] 지역코드 조회 API Service 추가 #4

Merged
merged 6 commits into from
Oct 1, 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
17 changes: 16 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import java.io.FileInputStream
import java.util.Properties

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.dagger.hilt.android)
alias(libs.plugins.kotlin.ksp)
}

val properties = Properties().apply {
load(FileInputStream(rootProject.file("local.properties")))
}

android {
namespace = "kr.ksw.visitkorea"
compileSdk = 34

Choose a reason for hiding this comment

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

여담인데, 35가 곧 배포될 예정이라서 compile / target sdk version up 을 34로 유지하는 건 앞으로 1년 정도만 허용될 거예요. (지금 당장은 하지 않아도 됨)

Expand All @@ -20,6 +27,8 @@ android {
vectorDrawables {
useSupportLibrary = true
}

buildConfigField("String", "API_KEY", properties.getProperty("API_KEY"))
}

buildTypes {
Expand All @@ -36,9 +45,10 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
jvmTarget = JavaVersion.VERSION_17.toString()
}
buildFeatures {
buildConfig = true
compose = true
}
composeOptions {
Expand Down Expand Up @@ -85,4 +95,9 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)

testImplementation(libs.kotlinx.coroutines.test)

androidTestImplementation(libs.hilt.android.testing)
kspAndroidTest(libs.hilt.compiler)
}

This file was deleted.

1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

<application
android:name=".app.VisitKoreaApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/kr/ksw/visitkorea/app/VisitKoreaApp.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.ksw.visitkorea.app

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class VisitKoreaApp : Application()
49 changes: 49 additions & 0 deletions app/src/main/java/kr/ksw/visitkorea/data/di/DataModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package kr.ksw.visitkorea.data.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kr.ksw.visitkorea.data.remote.api.AreaCodeApi
import kr.ksw.visitkorea.data.remote.api.RetrofitInterceptor
import kr.ksw.visitkorea.data.repository.AreaCodeRepository
import kr.ksw.visitkorea.data.repository.AreaCodeRepositoryImpl
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object DataModule {

private const val BASE_URL = "https://apis.data.go.kr/B551011/KorService1/"

Choose a reason for hiding this comment

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

BASE_URL 과 같은 상수는 DataModule 에 둬도 좋지만, 상수가 많아진다면 상수를 관리하는 object 를 하나 둬도 괜찮을 거 같아요.


@Provides
@Singleton
fun provideOkhttpClient(interceptor: RetrofitInterceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
}

@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.client(client)
.build()
}

@Provides
@Singleton
fun provideAreaCodeApi(retrofit: Retrofit): AreaCodeApi = retrofit.create(AreaCodeApi::class.java)

@Provides
@Singleton
fun provideAreaCodeRepository(areaCodeApi: AreaCodeApi): AreaCodeRepository {
return AreaCodeRepositoryImpl(areaCodeApi)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package kr.ksw.visitkorea.data.mapper

import kr.ksw.visitkorea.data.remote.model.ApiResponse

fun <T> ApiResponse<T>.toItems(): List<T> = response.body.items.item
19 changes: 19 additions & 0 deletions app/src/main/java/kr/ksw/visitkorea/data/remote/api/AreaCodeApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kr.ksw.visitkorea.data.remote.api

import kr.ksw.visitkorea.data.remote.dto.AreaCodeDTO
import kr.ksw.visitkorea.data.remote.model.ApiResponse
import retrofit2.http.GET
import retrofit2.http.Query

interface AreaCodeApi {
@GET("areaCode1")
suspend fun getAreaCode(
@Query("numOfRows") numOfRows: Int = 20
): ApiResponse<AreaCodeDTO>

@GET("areaCode1")
suspend fun getSigunguCode(
@Query("numOfRows") numOfRows: Int = 40,
@Query("areaCode") areaCode: String
): ApiResponse<AreaCodeDTO>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package kr.ksw.visitkorea.data.remote.api

import kr.ksw.visitkorea.BuildConfig
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject



class RetrofitInterceptor @Inject constructor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val requestUrl = request.url
.newBuilder()
.apply {
baseQueryMap.forEach { (key, value) ->
addQueryParameter(key, value)
}
}
.build()

return chain.proceed(request
.newBuilder()
.url(requestUrl)
.build()
)
}

companion object {
private val baseQueryMap = mapOf(
"MobileOS" to "AND",
"MobileApp" to "visitkorea",
"_type" to "json",
"serviceKey" to BuildConfig.API_KEY
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.ksw.visitkorea.data.remote.dto

data class AreaCodeDTO(
val code: String,
val name: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package kr.ksw.visitkorea.data.remote.model

data class ApiResponse<T>(

Choose a reason for hiding this comment

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

data class 의 마지막 item 들에는 trailing comma 를 넣어 주는 것이 좋습니다. 아래의 다른 data class 들도 마찬가지로요 :)

val response: CommonResponse<T>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kr.ksw.visitkorea.data.remote.model

data class CommonBody<T>(
val numOfRows: Int,
val pageNo: Int,
val totalCount: Int,
val items: CommonItem<T>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.ksw.visitkorea.data.remote.model

data class CommonHeader(
val resultCode: String,
val resultMsg: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package kr.ksw.visitkorea.data.remote.model

data class CommonItem<T>(
val item: List<T>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.ksw.visitkorea.data.remote.model

data class CommonResponse<T>(

Choose a reason for hiding this comment

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

제네릭을 이용한 Response class 를 만드는 건 좋습니다. 그리고 이런 형태가 반복적으로 사용된다면 data/remote 가 아닌 common 과 같은 패키지를 하나 만들어서 관리해도 좋을 거 같아요.

val header: CommonHeader,
val body: CommonBody<T>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package kr.ksw.visitkorea.data.repository

import kr.ksw.visitkorea.data.remote.dto.AreaCodeDTO

interface AreaCodeRepository {
suspend fun getAreaCode(): Result<List<AreaCodeDTO>>

suspend fun getSigunguCode(areaCode: String): Result<List<AreaCodeDTO>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kr.ksw.visitkorea.data.repository

import kr.ksw.visitkorea.data.mapper.toItems
import kr.ksw.visitkorea.data.remote.api.AreaCodeApi
import kr.ksw.visitkorea.data.remote.dto.AreaCodeDTO
import javax.inject.Inject

class AreaCodeRepositoryImpl @Inject constructor(
private val areaCodeApi: AreaCodeApi
): AreaCodeRepository {
override suspend fun getAreaCode(): Result<List<AreaCodeDTO>> = runCatching {
areaCodeApi.getAreaCode().toItems()
}

override suspend fun getSigunguCode(areaCode: String): Result<List<AreaCodeDTO>> = runCatching {
areaCodeApi.getSigunguCode(areaCode = areaCode).toItems()
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package kr.ksw.visitkorea.presentation.splash

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
Expand All @@ -16,26 +14,18 @@ import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kr.ksw.visitkorea.R
import kr.ksw.visitkorea.presentation.main.MainActivity
import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme

@AndroidEntryPoint
class SplashActivity : ComponentActivity() {

private val locationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissionMap ->
if(permissionMap[Manifest.permission.ACCESS_COARSE_LOCATION] != true &&
permissionMap[Manifest.permission.ACCESS_FINE_LOCATION] != true) {
Toast.makeText(
this@SplashActivity,
getString(R.string.location_permission_denied_toast),
Toast.LENGTH_SHORT
).show()
}
startMainActivity()
}
private val viewModel: SplashViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -55,20 +45,26 @@ class SplashActivity : ComponentActivity() {
}
}
}
checkPermission()
observeSideEffect()
viewModel.checkPermission(this)
}

private fun checkPermission() {
if(ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED) {
startMainActivity()
} else {
locationPermissionLauncher.launch(arrayOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
))
private fun observeSideEffect() {

Choose a reason for hiding this comment

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

observeSideEffect() 라는 메소드명은 side effect 의 관찰에만 너무 중점을 두고 있는 느낌이라, observePermissionGrantResult 이런 식으로 바꿔도 좋을 것 같습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

네 알겠습니다!

lifecycleScope.launch {
viewModel.sideEffect.collectLatest { effect ->
when(effect) {
SplashSideEffect.PermissionDenied -> {
Toast.makeText(
this@SplashActivity,
getString(R.string.location_permission_denied_toast),
Toast.LENGTH_SHORT
).show()
}
SplashSideEffect.StartMainActivity -> {
startMainActivity()
}
}
}
}
}

Expand Down
Loading
Loading