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

[Feat/#8] 4주차 필수 과제 #9

Merged
merged 6 commits into from
Dec 6, 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
27 changes: 27 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import java.util.Properties

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
Comment on lines +8 to +9

Choose a reason for hiding this comment

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

버전 카탈로그를 이용해주셔도 좋을 것 같네요 ~ 그리고 id와 alias의 차이는 뭘까요?

}


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

android {
Expand All @@ -16,6 +26,7 @@ android {
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BASE_URL", properties["base.url"].toString())
}

buildTypes {
Expand All @@ -36,6 +47,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}

Expand All @@ -50,6 +62,7 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.runtime.ktx)
implementation(libs.androidx.runtime.livedata)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand All @@ -60,5 +73,19 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.material)
// Network
implementation(platform(libs.okhttp.bom))
implementation(libs.okhttp)
implementation(libs.okhttp.logging.interceptor)
implementation(libs.retrofit)
implementation(libs.retrofit.kotlin.serialization.converter)
implementation(libs.kotlinx.serialization.json)
//hilt
implementation(libs.hilt.android.v2511)
kapt(libs.hilt.compiler.v2511)
implementation(libs.androidx.hilt.navigation.compose)
}

kapt {
correctErrorTypes = true
}
19 changes: 5 additions & 14 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".MyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand All @@ -11,23 +14,11 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ANDANDROID"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".presentation.ui.auth.MyActivity"
android:exported="false"
android:label="@string/title_activity_my" />
<activity
android:name=".presentation.ui.auth.SignInAcitivity"
android:exported="false"
android:label="@string/title_activity_sign_in_acitivity" />
<activity
android:name=".presentation.ui.auth.SignUpActivity"
android:exported="false"
android:label="@string/title_activity_sign_up" />
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name">
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down
24 changes: 12 additions & 12 deletions app/src/main/java/org/sopt/and/MainActivity.kt
Copy link

Choose a reason for hiding this comment

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

[2]
Hilt 도입 ㄷㄷ.. 대단하네요
현재 파일에 Activity, Application, Composable 여러가지가 모두 들어가 있다보니 파일을 분리해준다면 조금 더 가독성이 좋아질 것 같아요!

Original file line number Diff line number Diff line change
@@ -1,51 +1,51 @@
package org.sopt.and

import android.app.Application
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.HiltAndroidApp
import org.sopt.and.navigation.AuthNavItem
import org.sopt.and.presentation.ui.auth.SignInScreen
import org.sopt.and.presentation.ui.auth.SignUpScreen
import org.sopt.and.presentation.ui.main.MainScreen
import org.sopt.and.presentation.viewmodel.SignUpViewModel
import org.sopt.and.ui.theme.ANDANDROIDTheme

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ANDANDROIDTheme {
val signUpViewModel: SignUpViewModel = viewModel()
MyApp(signUpViewModel)
MyAppScreen()
}
}
}
}

@HiltAndroidApp
class MyApp : Application()

@Composable
fun MyApp(signUpViewModel: SignUpViewModel) {
fun MyAppScreen() {
val navController = rememberNavController()

NavHost(navController, startDestination = AuthNavItem.SignUp.route) {
composable(AuthNavItem.SignUp.route) {
SignUpScreen(signUpViewModel = signUpViewModel, navController = navController)
SignUpScreen(navController = navController)
}

composable(AuthNavItem.SignIn.route) {
SignInScreen(signUpViewModel = signUpViewModel, navController = navController)
SignInScreen(navController = navController)
}

composable(AuthNavItem.Main.route) {
MainScreen(signUpViewModel = signUpViewModel)
MainScreen()
}

}

}
60 changes: 60 additions & 0 deletions app/src/main/java/org/sopt/and/data/api/ApiFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.sopt.and.data.api

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.sopt.and.BuildConfig
import retrofit2.Retrofit
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ApiFactory {
private const val BASE_URL: String = BuildConfig.BASE_URL

@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
return OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
}

@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.build()
}

@Provides
@Singleton
fun provideUserRegistrationService(retrofit: Retrofit): UserRegistrationService {
return retrofit.create(UserRegistrationService::class.java)
}

@Provides
@Singleton
fun provideLoginService(retrofit: Retrofit): LoginService {
return retrofit.create(LoginService::class.java)
}
Comment on lines +42 to +52
Copy link

Choose a reason for hiding this comment

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

[2]
하나의 interface에 여러가지 api가 들어가도 무관합니다!
따라서 api의 특성에 따라 service interface를 어떻게 분리할 지 생각해보는 것도 좋을거 같아요.
그런데 이미 파일명을 AuthService라고 잘 해놓으셨네요! 하나로 통합시킨 후 의존성 주입 부분도 AuthService 하나로 통일하면 더 좋을 것 같습니다!


@Provides
@Singleton
fun provideHobbyService(retrofit: Retrofit): HobbyService {
return retrofit.create(HobbyService::class.java)
}

}
27 changes: 27 additions & 0 deletions app/src/main/java/org/sopt/and/data/api/AuthService.kt
Copy link

Choose a reason for hiding this comment

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

[3]
service interface를 구현할때 callback을 사용할 때와 달리 suspend fun 을 사용해야만 했던 이유는 무엇일까요? 학습해보면 좋을 것 같습니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.sopt.and.data.api

import org.sopt.and.data.dto.RequestLoginData
import org.sopt.and.data.dto.RequestUserRegistrationData
import org.sopt.and.data.dto.ResponseLogin
import org.sopt.and.data.dto.ResponseUserRegistration
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST


interface UserRegistrationService {
@POST("/user")
suspend fun postUserRegistration(
@Body userRequest: RequestUserRegistrationData
): Response<ResponseUserRegistration>
}

interface LoginService {
@POST("/login")
suspend fun postLogin(
@Body loginRequeset: RequestLoginData
): Response<ResponseLogin>
}
Comment on lines +12 to +24

Choose a reason for hiding this comment

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

AuthService 라는 이름에 맞춰서 다음처럼 하나의 인터페이스로 구현하는 건 어떤가요?

Suggested change
interface UserRegistrationService {
@POST("/user")
suspend fun postUserRegistration(
@Body userRequest: RequestUserRegistrationData
): Response<ResponseUserRegistration>
}
interface LoginService {
@POST("/login")
suspend fun postLogin(
@Body loginRequeset: RequestLoginData
): Response<ResponseLogin>
}
interface AuthService{
@POST("/user")
suspend fun postUserRegistration(
@Body userRequest: RequestUserRegistrationData
): Response<ResponseUserRegistration>
@POST("/login")
suspend fun postLogin(
@Body loginRequeset: RequestLoginData
): Response<ResponseLogin>
}




13 changes: 13 additions & 0 deletions app/src/main/java/org/sopt/and/data/api/HobbyService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sopt.and.data.api

import org.sopt.and.data.dto.ResponseMyHobbyData
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Header

interface HobbyService {
@GET("/user/my-hobby")
Comment on lines +8 to +9
Copy link

Choose a reason for hiding this comment

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

"/user/my-hobby"는 하드코딩되어 있는데, 변경 시 유지보수성이 떨어질 수 있다고 합니다! 이를 상수화하면 관리가 더 편리할 수 있습니다!

suspend fun getHobby(
@Header("token") token: String?
): Response<ResponseMyHobbyData>
}
48 changes: 48 additions & 0 deletions app/src/main/java/org/sopt/and/data/dto/AuthDto.kt
Copy link

Choose a reason for hiding this comment

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

[2]
AuthService에서 사용한다는 공통점으로 묶은 부분은 좋았지만 추후 프로젝트 규모가 조금이라도 커졌을 때를 대비해 Dto 파일을 분리하는 연습을 해보면 좋을 것 같습니다! 일반적으로는 크게 request와 response에 따라 구분합니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.sopt.and.data.dto

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/* 유저 등록 */
@Serializable
data class RequestUserRegistrationData(
@SerialName("username")
val userName: String,
@SerialName("password")
val password: String,
@SerialName("hobby")
val hobby: String
)

@Serializable
data class ResponseUserRegistration(
@SerialName("result")
val result: ResultUserNo
)

@Serializable
data class ResultUserNo(
@SerialName("no")
val no: Int
)

/* 로그인 */
@Serializable
data class RequestLoginData(
@SerialName("username")
val userName: String,
@SerialName("password")
val password: String
)

@Serializable
data class ResponseLogin(
@SerialName("result")
val result: ResultToken
)
Comment on lines +38 to +42

Choose a reason for hiding this comment

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

공통되는 부분에 대해 BaseResponse를 만든 후 사용하시면 가독성이 더 좋아질 것 같아요!


@Serializable
data class ResultToken(
@SerialName("token")
val token: String
)
17 changes: 17 additions & 0 deletions app/src/main/java/org/sopt/and/data/dto/MyviewDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.sopt.and.data.dto

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/* 내 취미 조회 */
@Serializable
data class ResponseMyHobbyData(
@SerialName("result")
val result: ResponseMyHobbyDataResult
)

@Serializable
data class ResponseMyHobbyDataResult(
@SerialName("hobby")
val hobby: String
)
Copy link

Choose a reason for hiding this comment

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

[3]
callBack이 아닌 try-catch를 사용하신 이유가 있을까요? 어떤 이점이 있을거라 생각하셨나요?

Copy link

Choose a reason for hiding this comment

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

[3]
runCatching이라는 방법을 사용할 수도 있는데 차이점을 알아보는 것도 좋을 것 같습니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.sopt.and.data.repository.Auth

import org.sopt.and.data.api.LoginService
import org.sopt.and.data.dto.RequestLoginData
import org.sopt.and.data.dto.ResponseLogin
import javax.inject.Inject

class LoginRepository @Inject constructor(
private val apiService: LoginService
) {
suspend fun postLogin(requestData: RequestLoginData): Result<ResponseLogin> {
return try {
val response = apiService.postLogin(requestData)
if (response.isSuccessful) {
Result.success(response.body()!!)

Choose a reason for hiding this comment

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

!! 사용은 지양합시다. NPE가 발생할 수 있어요.

} else {
val errorCode = response.errorBody()?.string() ?: "알수없슴"
Result.failure(Exception("Error code : ${response.code()}, 에러 코드 : $errorCode"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.sopt.and.data.repository.Auth

import org.sopt.and.data.api.UserRegistrationService
import org.sopt.and.data.dto.RequestUserRegistrationData
import org.sopt.and.data.dto.ResponseUserRegistration
import javax.inject.Inject

class UserRegistrationRepository @Inject constructor(
private val apiService: UserRegistrationService
) {
suspend fun postUserRegistration(requestData: RequestUserRegistrationData): Result<ResponseUserRegistration> {

Choose a reason for hiding this comment

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

저는 서버 통신하는 부분은 return 문이 없는게 보기 좋더라구요
이런식으로도 사용가능합니다.

suspend fun postUserRegistration(
    requestData: RequestUserRegistrationData
): Result<ResponseUserRegistration> = try { 
    val response = apiService.postUserRegistration(requestData)
    if (response.isSuccessful) {
        Result.success(response.body()!!)
    } else {
        val errorCode = response.errorBody()?.string() ?: "알수없슴"
        Result.failure(Exception("Error code : ${response.code()}, 에러 코드 : $errorCode"))
    }
} catch (e: Exception) {
    Result.failure(e)
}

return try {
val response = apiService.postUserRegistration(requestData)
if (response.isSuccessful) {
Result.success(response.body()!!)
} else {
val errorCode = response.errorBody()?.string() ?: "알수없슴"
Result.failure(Exception("Error code : ${response.code()}, 에러 코드 : $errorCode"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
Loading