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

네트워크 통신 플로우 세팅 & 사용 가능 기프티콘 조회 api test #44

Merged
merged 7 commits into from
Jan 24, 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
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BuddyCon"
tools:targetApi="31">
tools:targetApi="31"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
19 changes: 19 additions & 0 deletions data/src/main/java/com/yapp/buddycon/data/api/GiftiConService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.yapp.buddycon.data.api

import com.yapp.buddycon.data.model.response.AvailableGifticonResponse
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query

interface GiftiConService {
@GET("api/v1/gifticons/available")
suspend fun requestGiftiConDetail(
@Header("Authorization") token: String = "Bearer " +
"eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MiwiaWF0IjoxNzAyNzg5NDc2LCJleHAiOjE3MDg4Mzc0NzZ9." +
"8YPrIlLexzGiqHwE1T_n2E-hCYbsNqJA5kUPWwgD0H8GmrGGsMgexme4NnNzBgsiHWG2uGtDLZL9fDCdiyZNUw",
@Query("pageNumber") pageNumber: Int, // page
@Query("gifticonStoreCategory") gifticonStoreCategory: String?, // 기프티콘 가게 카테고리
@Query("rowCount") rowCount: Int = 20 // page 당 요청 데이터 개수
): Response<AvailableGifticonResponse>
}
23 changes: 23 additions & 0 deletions data/src/main/java/com/yapp/buddycon/data/di/InterceptorModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.yapp.buddycon.data.di

import com.yapp.buddycon.data.BuildConfig
import com.yapp.buddycon.data.di.qualifiers.HttpLoggingInterceptorQualifier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.Interceptor
import okhttp3.logging.HttpLoggingInterceptor
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object InterceptorModule {
@HttpLoggingInterceptorQualifier
@Provides
@Singleton
fun provideHttpLoggingInterceptor(): Interceptor =
HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
}
}
54 changes: 54 additions & 0 deletions data/src/main/java/com/yapp/buddycon/data/di/NetworkModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.yapp.buddycon.data.di

import com.yapp.buddycon.data.api.GiftiConService
import com.yapp.buddycon.data.di.qualifiers.BuddyConClient
import com.yapp.buddycon.data.di.qualifiers.BuddyConRetrofit
import com.yapp.buddycon.data.di.qualifiers.HttpLoggingInterceptorQualifier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
private const val BASE_URL = "http://43.202.14.1:8080/"

@BuddyConClient
@Provides
@Singleton
fun provideBuddyConClient(
@HttpLoggingInterceptorQualifier httpLoggingInterceptor: Interceptor,
// @BuddyConInterceptorQualifier buddyConInterceptor: Interceptor
): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
// .addInterceptor(buddyConInterceptor)
.build()

@BuddyConRetrofit
@Provides
@Singleton
fun provideBuddyConRetrofit(
@BuddyConClient okHttpClient: OkHttpClient
): Retrofit =
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()

/** api service */
@Provides
@Singleton
fun provideGiftiConService(
@BuddyConRetrofit retrofit: Retrofit
): GiftiConService =
retrofit.create()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.yapp.buddycon.data.di

import com.yapp.buddycon.data.repository.AuthRepositoryImpl
import com.yapp.buddycon.data.repository.TokenRepositoryImpl
import com.yapp.buddycon.data.repository.remote.AvailableGifticonRepositoryImpl
import com.yapp.buddycon.domain.repository.AuthRepository
import com.yapp.buddycon.domain.repository.AvailableGifticonRepository
import com.yapp.buddycon.domain.repository.TokenRepository
import dagger.Binds
import dagger.Module
Expand All @@ -24,4 +26,10 @@ interface RepositoryModule {
fun bindAuthRepository(
authRepositoryImpl: AuthRepositoryImpl
): AuthRepository

@Binds
@Singleton
fun bindAvailableGifticonRepository(
availableGifticonRepositoryImpl: AvailableGifticonRepositoryImpl
): AvailableGifticonRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.yapp.buddycon.data.di.qualifiers

import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class HttpLoggingInterceptorQualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BuddyConInterceptorQualifier
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.yapp.buddycon.data.di.qualifiers

import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LoginClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BuddyConClient
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.yapp.buddycon.data.di.qualifiers

import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LoginRetrofit

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BuddyConRetrofit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package com.yapp.buddycon.data.model.request
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.yapp.buddycon.data.model.response

import com.google.gson.annotations.SerializedName
import com.yapp.buddycon.domain.model.gifticon.AvailableGifticon

data class AvailableGifticonResponse(
@SerializedName("body")
val body: Body,
@SerializedName("message")
val message: String,
@SerializedName("status")
val status: Int
)

data class Body(
val content: List<Content>,
val empty: Boolean,
val first: Boolean,
val last: Boolean,
val number: Int,
val numberOfElements: Int,
val pageable: Pageable,
val size: Int
) {
data class Content(
val expireDate: String,
val gifticonId: Int,
val gifticonStore: String,
val gifticonStoreCategory: String,
val imageUrl: String,
val memo: String,
val name: String
) {
fun mapToAvailableGifticonInfo() = AvailableGifticon.AvailableGifticonInfo(
imageUrl = this.imageUrl,
name = this.name,
expireDate = this.expireDate
// 가게, 메뉴 카테고리 정보 mapping 추가 예정
)
}

data class Pageable(
Copy link
Contributor

Choose a reason for hiding this comment

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

@hanchang97
Pageable , SortX 클래스는 필요하지 않을 것 같은데 왜 모델링 하셨을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

응답 정상적으로 오는지 확인하는 과정에서 일단 만들어뒀는데 사양적으로 필요하지 않을 것 같아서 제거하도록 하겠습니다!

val pageNumber: Int,
val pageSize: Int,
val paged: Boolean,
val unpaged: Boolean
)

fun mapToAvailableGifticon() = AvailableGifticon(
availableGifticons = this.content.map { it.mapToAvailableGifticonInfo() },
isFirstPage = this.first,
isLastPage = this.last,
pageNumber = this.pageable.pageNumber
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package com.yapp.buddycon.data.repository.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.yapp.buddycon.data.repository.remote

import com.yapp.buddycon.data.api.GiftiConService
import com.yapp.buddycon.domain.repository.AvailableGifticonRepository
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class AvailableGifticonRepositoryImpl @Inject constructor(
private val gifticonService: GiftiConService
) : AvailableGifticonRepository {
override fun getAvailableGifiticon() = flow {
emit(gifticonService.requestGiftiConDetail(pageNumber = 0, gifticonStoreCategory = null))
}.catch { error ->
throw Throwable("catch error!", error)
}.map { response ->
if (response.isSuccessful) {
(response.body() ?: throw NullPointerException("null response")).body.mapToAvailableGifticon()
} else {
throw Throwable("error.. msg : ${response.message()}")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.yapp.buddycon.domain.model.gifticon

import com.yapp.buddycon.domain.model.type.GifticonCategory
import com.yapp.buddycon.domain.model.type.GifticonStoreCategory

data class AvailableGifticon(
val availableGifticons: List<AvailableGifticonInfo>,
val isFirstPage: Boolean = false,
val isLastPage: Boolean = false,
val pageNumber: Int = 0,
) {
data class AvailableGifticonInfo(
Copy link
Contributor

Choose a reason for hiding this comment

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

@hanchang97 클래스 안에 클래스가 있는 구조의 장점이 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

중첩된 data class 사용을 통해 관련된 데이터 모델을 논리적으로 그룹화하려는 의도였습니다. 중첩된 data class는 외부 클래스와 강한 연관성을 가지기 때문에, 해당 클래스가 어떤 목적을 가지는지 명확하게 표현할 수 있을거라 생각했습니다..!

val imageUrl: String = "",
val category: GifticonCategory = GifticonCategory.ETC,
val storeCategory: GifticonStoreCategory = GifticonStoreCategory.OTHERS,
val name: String = "",
val expireDate: String = "",
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.yapp.buddycon.domain.model.type

enum class GifticonStoreCategory(val value: String) {
CAFE("CAFE"),
CONVENIENCE_STORE("CONVENIENCE_STORE"),
OTHERS("OTHERS")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.yapp.buddycon.domain.repository

import com.yapp.buddycon.domain.model.gifticon.AvailableGifticon
import kotlinx.coroutines.flow.Flow

interface AvailableGifticonRepository {
fun getAvailableGifiticon(): Flow<AvailableGifticon> // todo - parameter 추가 예정
}
5 changes: 5 additions & 0 deletions feature/gifticon/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
plugins {
alias libs.plugins.android.library
alias libs.plugins.kotlin.android
alias libs.plugins.hilt.android
alias libs.plugins.kotlin.kapt
}

android {
Expand Down Expand Up @@ -39,6 +41,9 @@ dependencies {
implementation project(":core:designsystem")
implementation project(":domain")

implementation libs.hilt.android
kapt libs.hilt.compiler

implementation libs.coil.compose
implementation libs.androidx.core.ktx
implementation libs.androidx.activity.compose
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.yapp.buddycon.designsystem.R
import com.yapp.buddycon.designsystem.component.appbar.TopAppBarWithNotification
import com.yapp.buddycon.designsystem.component.button.FloatingActionButton
import com.yapp.buddycon.designsystem.theme.BuddyConTheme
import com.yapp.buddycon.gifticon.available.AvailabeGifticonScreen

@Composable
fun GifticonScreeen(
Expand All @@ -35,9 +36,10 @@ fun GifticonScreeen(
}

@Composable
private fun GifticonContent(
fun GifticonContent(
modifier: Modifier = Modifier
) {
Column(modifier) {
AvailabeGifticonScreen()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.yapp.buddycon.gifticon.available

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.hilt.navigation.compose.hiltViewModel

@Composable
fun AvailabeGifticonScreen(
availableGifticonViewModel: AvailableGifticonViewModel = hiltViewModel()
) {
Box(
modifier = Modifier.fillMaxSize().background(Color.LightGray)
) {
Button(
modifier = Modifier.align(Alignment.Center),
onClick = {
availableGifticonViewModel.getAvailableGifiticon()
}
) {
Text("test")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.yapp.buddycon.gifticon.available

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yapp.buddycon.domain.repository.AvailableGifticonRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class AvailableGifticonViewModel @Inject constructor(
private val availableGifticonRepository: AvailableGifticonRepository
) : ViewModel() {

fun getAvailableGifiticon() {
viewModelScope.launch {
availableGifticonRepository.getAvailableGifiticon()
.onStart {
// set loading state
Log.e("BuddyConTest", "loading...")
}.catch {
// error handling
Log.e("BuddyConTest", "catch error!")
}.collectLatest { availableGifticon ->
Log.e("BuddyConTest", "collect data : $availableGifticon")
}
}
}
}
Loading