diff --git a/.github/workflows/develop_PR_builder.yml b/.github/workflows/develop_PR_builder.yml index 33d22b83..2f72d32c 100644 --- a/.github/workflows/develop_PR_builder.yml +++ b/.github/workflows/develop_PR_builder.yml @@ -36,6 +36,12 @@ jobs: - name: Change gradlew permissions run: chmod +x ./gradlew + - name: Add Local Properties + env: + KAKAO_API_KEY: ${{ secrets.KAKAO_API_KEY }} + run: | + echo kakaoApiKey=$KAKAO_API_KEY >> ./local.properties + - name: Access Firebase Service run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > ./app/google-services.json diff --git a/.gitignore b/.gitignore index 2a37cc12..ae099a52 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,6 @@ render.experimental.xml # Keystore files *.jks -*.keystore # Google Services (e.g. APIs or Firebase) google-services.json diff --git a/README.md b/README.md index 5c762140..0023faf4 100644 --- a/README.md +++ b/README.md @@ -1 +1,111 @@ -# HMH Android +# 하면함 Android + +## 스마트폰 중독 탈출, 너도 하면함! +
+
+

+ + + + +

+
+
+ +

Tech Stack

+ +- [Android App Architecture](https://developer.android.com/topic/architecture) +- [Dagger-Hilt](https://developer.android.com/training/dependency-injection/hilt-android) +- [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html), [Flow](https://kotlinlang.org/docs/flow.html) +- [Jetpack Compose](https://developer.android.com/jetpack/compose) +- [Material 3](https://m3.material.io/) +- [Gradle Version Catalog](https://docs.gradle.org/current/userguide/platforms.html) + and [Custom Convention Plugins](https://docs.gradle.org/current/samples/sample_convention_plugins.html) + +

Activity Graph

+ +![Alt](https://repobeats.axiom.co/api/embed/d2c401ae723c367a03ed9fb81ea6e6e7cfbee2ea.svg "Repobeats analytics image") + +## Contributors ✨ + +하면한다는 Android 개발자 + + + + + + + + + + + + +
Kwak EuiJin
KwakEuiJin

💻
Kang Yuri
KangYuri

💻
Kyoung JiHyun
JiHyun Kyoung

💻
+ + + + + + + + + + + + + +## Folder Tree 📁 + +- 📁 app + - 📁 build + - 📄 build.gradle.kts + - 📄 google-services.json + - 📁 libs + - 📄 proguard-rules.pro + - 📁 src +- 📁 build-logic + - 📁 convention + - 📁 gradle + - 📄 gradle.properties + - 📄 settings.gradle.kts +- 📄 build.gradle.kts +- 📁 buildSrc + - 📁 build + - 📄 build.gradle.kts + - 📄 gradle.properties + - 📄 settings.gradle.kts + - 📁 src +- 📁 core + - 📁 common + - 📁 database +- 📁 data + - 📁 usagestats +- 📁 domain + - 📁 usagestats +- 📁 feature + - 📁 login + - 📁 main + - 📁 onboarding + - 📁 statistics +- 📁 gradle + - 📄 libs.versions.toml + - 📁 wrapper +- 📄 gradle.properties +- 📄 gradlew +- 📄 gradlew.bat +- 📄 local.properties +- 📄 settings.gradle.kts + +## Progress Board 📋 +- [Team-HMH-Android](https://github.com/orgs/Team-HMH/projects/1) + + +## Code Convention 💻 +- [Kotlin Code Convention](https://www.notion.so/msmmx/Kotlin-Convention-5ab4410e68c949c287804d2380c51af4) +- [XML Convention](https://www.notion.so/msmmx/XML-Convention-b11209ba2b404df08f251383c7d3c316) +- [name convention](https://www.notion.so/msmmx/name-convention-ca8bab7da7314b30b2a962755854b11e) +- [GitHub Convention](https://www.notion.so/msmmx/b75d3559813e478f9f6d73e9c818834b?pvs=4#bc0bf41e7df9421391dc1000d25a17c6) + + +This project follows the all-contributors specification. Contributions of any kind welcome! diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 411b5ed6..05ed02c8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,9 +16,23 @@ android { versionName = libs.versions.appVersion.get() } + signingConfigs { + getByName("debug") { + keyAlias = "android_debug_key" + keyPassword = "android" + storeFile = File("${project.rootDir.absolutePath}/keystore/debug.keystore") + storePassword = "android" + } + } + buildTypes { + debug { + isDebuggable = true + signingConfig = signingConfigs.getByName("debug") + } release { isMinifyEnabled = false + isDebuggable = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", @@ -28,16 +42,6 @@ android { } dependencies { - // Feature - implementation(projects.feature.onboarding) - - // Core - implementation(projects.core.common) - implementation(projects.core.database) - - // Feature - implementation(projects.feature.onboarding) - implementation(projects.feature.main) // Feature implementation(projects.feature.statistics) @@ -57,4 +61,12 @@ dependencies { // Splash implementation(libs.splash.screen) + + // Features + implementation(projects.feature.login) + implementation(projects.feature.onboarding) + implementation(projects.feature.main) + + // kakao + implementation(libs.kakao.login) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7faba2b9..dff28e6b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,12 +2,16 @@ + + - + - - + + + + + + + + + + + diff --git a/app/src/main/java/com/hmh/hamyeonham/SampleActivity.kt b/app/src/main/java/com/hmh/hamyeonham/SampleActivity.kt index f3a7b90d..62aa7129 100644 --- a/app/src/main/java/com/hmh/hamyeonham/SampleActivity.kt +++ b/app/src/main/java/com/hmh/hamyeonham/SampleActivity.kt @@ -1,27 +1,24 @@ package com.hmh.hamyeonham -import android.content.Intent import android.os.Bundle import android.view.animation.Animation import android.view.animation.AnimationUtils import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import com.hmh.hamyeonham.common.view.viewBinding import com.hmh.hamyeonham.databinding.ActivitySampleBinding -import com.hmh.hamyeonham.statistics.StaticsActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class SampleActivity : AppCompatActivity() { - private lateinit var binding: ActivitySampleBinding + private val binding by viewBinding(ActivitySampleBinding::inflate) override fun onCreate(savedInstanceState: Bundle?) { - binding = ActivitySampleBinding.inflate(layoutInflater) super.onCreate(savedInstanceState) val splashScreen = installSplashScreen() initSplashAnimation(splashScreen) setContentView(binding.root) - Intent(this, StaticsActivity::class.java).let(::startActivity) } private fun initSplashAnimation(splashScreen: SplashScreen) { @@ -29,17 +26,14 @@ class SampleActivity : AppCompatActivity() { val splashScreenView = splashScreenViewProvider.view val fadeOut = AnimationUtils.loadAnimation(this, R.anim.fade_out) - fadeOut.setAnimationListener( - object : Animation.AnimationListener { - override fun onAnimationStart(animation: Animation) {} + fadeOut.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) {} + override fun onAnimationEnd(animation: Animation) { + splashScreenViewProvider.remove() + } - override fun onAnimationEnd(animation: Animation) { - splashScreenViewProvider.remove() - } - - override fun onAnimationRepeat(animation: Animation) {} - }, - ) + override fun onAnimationRepeat(animation: Animation) {} + }) splashScreenView.startAnimation(fadeOut) } } diff --git a/app/src/main/res/layout/activity_sample.xml b/app/src/main/res/layout/activity_sample.xml index d36955d3..e7c78653 100644 --- a/app/src/main/res/layout/activity_sample.xml +++ b/app/src/main/res/layout/activity_sample.xml @@ -2,6 +2,6 @@ + android:background="@color/white"> diff --git a/build-logic/convention/src/main/kotlin/com/hmh/hamyeonham/plugin/CommonConfigs.kt b/build-logic/convention/src/main/kotlin/com/hmh/hamyeonham/plugin/CommonConfigs.kt index 11c11dd6..cc1306ad 100644 --- a/build-logic/convention/src/main/kotlin/com/hmh/hamyeonham/plugin/CommonConfigs.kt +++ b/build-logic/convention/src/main/kotlin/com/hmh/hamyeonham/plugin/CommonConfigs.kt @@ -10,6 +10,10 @@ import java.util.Properties internal fun Project.configureAndroidCommonPlugin() { + val properties = Properties().apply { + load(rootProject.file("local.properties").inputStream()) + } + apply() apply() with(plugins) { @@ -18,7 +22,13 @@ internal fun Project.configureAndroidCommonPlugin() { apply() extensions.getByType().apply { - defaultConfig {} + defaultConfig { + val kakaoApiKey = properties["kakaoApiKey"] as? String ?: "" + + manifestPlaceholders["kakaoApiKey"] = properties["kakaoApiKey"] as String + + buildConfigField("String", "KAKAO_API_KEY", "\"${kakaoApiKey}\"") + } buildFeatures.apply { viewBinding = true buildConfig = true diff --git a/feature/login/.gitignore b/feature/login/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/login/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/login/build.gradle.kts b/feature/login/build.gradle.kts new file mode 100644 index 00000000..da2007b1 --- /dev/null +++ b/feature/login/build.gradle.kts @@ -0,0 +1,22 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + hmh("feature") + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.hmh.hamyeonham.feature.login" +} + +dependencies { + implementation(projects.core.common) + implementation(libs.appcompat) + implementation(libs.material) + implementation(libs.constraintlayout) + + // kakao + implementation(libs.kakao.login) + + implementation(libs.dot.indicator) + implementation(libs.coil.core) +} diff --git a/feature/login/consumer-rules.pro b/feature/login/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/login/proguard-rules.pro b/feature/login/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/login/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/login/src/main/AndroidManifest.xml b/feature/login/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ec037070 --- /dev/null +++ b/feature/login/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/LoginActivity.kt b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/LoginActivity.kt new file mode 100644 index 00000000..c5fddea4 --- /dev/null +++ b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/LoginActivity.kt @@ -0,0 +1,92 @@ +package com.hmh.hamyeonham.feature.login + +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.hmh.hamyeonham.common.context.toast +import com.hmh.hamyeonham.feature.login.data.DummyImage +import com.hmh.hamyeonham.feature.login.databinding.ActivityLoginBinding +import com.kakao.sdk.auth.model.OAuthToken +import com.kakao.sdk.common.model.ClientError +import com.kakao.sdk.common.model.ClientErrorCause +import com.kakao.sdk.user.UserApiClient + +class LoginActivity : AppCompatActivity() { + private lateinit var binding: ActivityLoginBinding + + private val loginViewModel: LoginViewModel by viewModels() + private lateinit var loginViewPagerAdapter: LoginViewPagerAdapter + + // 삭제 예정 + private val dummyImageList = listOf( + DummyImage( + Image = R.drawable.login_sample_rectagle_viewpager, + ), + DummyImage( + Image = R.drawable.login_sample_rectagle_viewpager, + ), + DummyImage( + Image = R.drawable.login_sample_rectagle_viewpager, + ), + ) + + private val callback: (OAuthToken?, Throwable?) -> Unit = { token, error -> + when { + error != null -> { + } + + token != null -> { + moveToUserInfoActivity() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + binding = ActivityLoginBinding.inflate(layoutInflater) + super.onCreate(savedInstanceState) + setContentView(binding.root) + + binding.btnLogin.setOnClickListener { + loginWithKakaoApp() + } + setLoginViewPager() + } + + private fun setLoginViewPager() { + loginViewPagerAdapter = LoginViewPagerAdapter(dummyImageList) + binding.run { + vpLogin.adapter = loginViewPagerAdapter + indicatorLoginDots.attachTo(binding.vpLogin) + } + } + + private fun loginWithKakaoApp() { + if (UserApiClient.instance.isKakaoTalkLoginAvailable(this)) { + UserApiClient.instance.loginWithKakaoTalk(this) { token, error -> + if (error != null) { + toast("카카오 로그인 실패") + if (error is ClientError && error.reason == ClientErrorCause.Cancelled) { + toast("다시 로그인 해주세요.") + return@loginWithKakaoTalk + } + loginWithKakaoAccount() + } else if (token != null) { + toast("카카오 로그인 성공") + moveToUserInfoActivity() + } + } + } else { + loginWithKakaoAccount() + } + } + + private fun loginWithKakaoAccount() { + UserApiClient.instance.loginWithKakaoAccount(this, callback = callback) + } + + private fun moveToUserInfoActivity() { + startActivity(Intent(this, UserInfoActivity::class.java)) + finish() + } +} diff --git a/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/LoginViewModel.kt b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/LoginViewModel.kt new file mode 100644 index 00000000..0d5cdf4c --- /dev/null +++ b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/LoginViewModel.kt @@ -0,0 +1,12 @@ +package com.hmh.hamyeonham.feature.login + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class LoginViewModel : ViewModel(){ + + private val _kakaoLoginResult = MutableLiveData() + val kakaoLoginResult: LiveData get() = _kakaoLoginResult + +} diff --git a/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/LoginViewPagerAdapter.kt b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/LoginViewPagerAdapter.kt new file mode 100644 index 00000000..328a8d7b --- /dev/null +++ b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/LoginViewPagerAdapter.kt @@ -0,0 +1,38 @@ +package com.hmh.hamyeonham.feature.login + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import com.hmh.hamyeonham.feature.login.data.DummyImage +import com.hmh.hamyeonham.feature.login.databinding.ItemLoginViewPagerBinding + +class LoginViewPagerAdapter(private val imageList: List) : + RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagerViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_login_view_pager, parent, false) + return PagerViewHolder(ItemLoginViewPagerBinding.bind(view)) + } + + class PagerViewHolder(private val binding: ItemLoginViewPagerBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun onBindView(imageInfo: DummyImage) { + binding.run { + ivLoginViewPagerItem.load(imageInfo.Image) { + placeholder(R.drawable.login_sample_rectagle_viewpager) + error(R.drawable.login_sample_rectagle_viewpager) + } + } + } + } + + override fun getItemCount(): Int = imageList.size + + override fun onBindViewHolder(holder: PagerViewHolder, position: Int) { + val exploreImage = imageList[position] + holder.onBindView(exploreImage) + } +} diff --git a/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/UserInfoActivity.kt b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/UserInfoActivity.kt new file mode 100644 index 00000000..fc020b48 --- /dev/null +++ b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/UserInfoActivity.kt @@ -0,0 +1,53 @@ +package com.hmh.hamyeonham.feature.login + +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.hmh.hamyeonham.common.context.toast +import com.hmh.hamyeonham.feature.login.databinding.ActivityUserInfoBinding +import com.kakao.sdk.user.UserApiClient + +class UserInfoActivity : AppCompatActivity() { + private lateinit var binding: ActivityUserInfoBinding + + private val userInfoViewModel: UserInfoViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + binding = ActivityUserInfoBinding.inflate(layoutInflater) + super.onCreate(savedInstanceState) + setContentView(binding.root) + + getKakaoUserNickname() + binding.btnLoginLogout.setOnClickListener { + logoutFromKakao() + } + } + + private fun getKakaoUserNickname() { + UserApiClient.instance.me { user, error -> + if (error != null) { + moveToLogin() + } else if (user != null) { + val kakaoNickname = user.kakaoAccount?.profile?.nickname + binding.tvLoginNickname.text = kakaoNickname + } + } + } + + private fun logoutFromKakao() { + UserApiClient.instance.logout { error -> + if (error != null) { + toast("다시 로그아웃 해주세요.") + } else { + toast("로그아웃 되었습니다.") + moveToLogin() + } + } + } + + private fun moveToLogin() { + startActivity(Intent(this, LoginActivity::class.java)) + finish() + } +} diff --git a/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/UserInfoViewModel.kt b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/UserInfoViewModel.kt new file mode 100644 index 00000000..69e9365a --- /dev/null +++ b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/UserInfoViewModel.kt @@ -0,0 +1,14 @@ +package com.hmh.hamyeonham.feature.login + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class UserInfoViewModel : ViewModel() { + + private val _kakaoLogoutResult = MutableLiveData() + val kakaoLogoutResult: LiveData get() = _kakaoLogoutResult + + private val _kakaoUserNickname = MutableLiveData() + val kakaoUserNickname: LiveData get() = _kakaoUserNickname +} diff --git a/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/data/DummyImage.kt b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/data/DummyImage.kt new file mode 100644 index 00000000..807c56a3 --- /dev/null +++ b/feature/login/src/main/java/com/hmh/hamyeonham/feature/login/data/DummyImage.kt @@ -0,0 +1,5 @@ +package com.hmh.hamyeonham.feature.login.data + +data class DummyImage( + val Image: Int, +) diff --git a/feature/login/src/main/res/drawable/login_kakao_large_wide.png b/feature/login/src/main/res/drawable/login_kakao_large_wide.png new file mode 100644 index 00000000..c0c18561 Binary files /dev/null and b/feature/login/src/main/res/drawable/login_kakao_large_wide.png differ diff --git a/feature/login/src/main/res/drawable/login_sample_rectagle_viewpager.xml b/feature/login/src/main/res/drawable/login_sample_rectagle_viewpager.xml new file mode 100644 index 00000000..5932a8db --- /dev/null +++ b/feature/login/src/main/res/drawable/login_sample_rectagle_viewpager.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/login/src/main/res/layout/activity_login.xml b/feature/login/src/main/res/layout/activity_login.xml new file mode 100644 index 00000000..b0f8532b --- /dev/null +++ b/feature/login/src/main/res/layout/activity_login.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/feature/login/src/main/res/layout/activity_user_info.xml b/feature/login/src/main/res/layout/activity_user_info.xml new file mode 100644 index 00000000..8f23b60a --- /dev/null +++ b/feature/login/src/main/res/layout/activity_user_info.xml @@ -0,0 +1,29 @@ + + + + + +