diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..3823552 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +## Related Issues + + +## What Did You Do? +- [x] + + +## Reference +- [x] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c141b6..b64c891 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,13 +36,26 @@ jobs: - name: Change gradlew permissions run: chmod +x ./gradlew + + - name: Check Lint + run: ./gradlew ktlintCheck - name: Build with Gradle run: ./gradlew assembleDebug - + - name: Upload APK if: ${{ success() }} uses: actions/upload-artifact@v2 with: name: apk path: app/build/outputs/apk/debug/ + + - name: Build App Bundle with Gradle + run: ./gradlew bundleRelease + + - name: Upload Bundle + if: ${{ success() }} + uses: actions/upload-artifact@v2 + with: + name: aab + path: app/build/outputs/bundle/release/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 3c7772a..7643783 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,22 +1,6 @@ - - diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..797acea --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/sonarIssues.xml b/.idea/sonarIssues.xml index 863bf9c..6885986 100644 --- a/.idea/sonarIssues.xml +++ b/.idea/sonarIssues.xml @@ -8,6 +8,11 @@ + + + + + @@ -393,6 +398,11 @@ + + + + + @@ -483,6 +493,11 @@ + + + + + @@ -518,16 +533,31 @@ + + + + + + + + + + + + + + + @@ -558,11 +588,21 @@ + + + + + + + + + + @@ -638,6 +678,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -718,6 +833,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle index 8fb83ed..ee8f2f0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId "com.teambeme.beme" minSdkVersion 26 targetSdkVersion 30 - versionCode 9 - versionName "1.1.2" + versionCode 10 + versionName "1.1.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -44,7 +44,7 @@ dependencies { implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.4.32' implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" implementation 'androidx.core:core-ktx:1.3.2' - implementation "androidx.fragment:fragment-ktx:1.3.2" + implementation "androidx.fragment:fragment-ktx:1.3.3" implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' @@ -61,7 +61,7 @@ dependencies { // Glide implementation 'com.github.bumptech.glide:glide:4.12.0' - kapt 'com.github.bumptech.glide:compiler:4.11.0' + kapt 'com.github.bumptech.glide:compiler:4.12.0' // Retrofit & Gson implementation 'com.squareup.retrofit2:retrofit:2.9.0' @@ -70,10 +70,10 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.2.2' // Room - implementation "androidx.room:room-runtime:2.2.6" - kapt "androidx.room:room-compiler:2.2.6" - implementation "androidx.room:room-ktx:2.2.6" - testImplementation "androidx.room:room-testing:2.2.6" + implementation "androidx.room:room-runtime:2.3.0" + kapt "androidx.room:room-compiler:2.3.0" + implementation "androidx.room:room-ktx:2.3.0" + testImplementation "androidx.room:room-testing:2.3.0" // Lottie implementation 'com.airbnb.android:lottie:3.6.0' @@ -84,7 +84,7 @@ dependencies { // FireBase implementation platform('com.google.firebase:firebase-bom:26.2.0') implementation 'com.google.firebase:firebase-analytics-ktx' - implementation 'com.google.firebase:firebase-messaging:21.0.1' + implementation 'com.google.firebase:firebase-messaging:21.1.0' // Image Crop implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' @@ -93,25 +93,25 @@ dependencies { implementation 'com.github.didikk:sticky-nestedscrollview:1.0.1' // AndroidX Navigation Component - implementation "androidx.navigation:navigation-fragment-ktx:2.3.4" - implementation "androidx.navigation:navigation-ui-ktx:2.3.4" + implementation "androidx.navigation:navigation-fragment-ktx:2.3.5" + implementation "androidx.navigation:navigation-ui-ktx:2.3.5" // Dot Indicator implementation 'com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2' // AndroidX Security: EncryptedSharedPreference - implementation "androidx.security:security-crypto:1.0.0-rc03" + implementation "androidx.security:security-crypto:1.0.0" // PullRefreshLayout implementation 'com.baoyz.pullrefreshlayout:library:1.2.0' // Android Dagger-Hilt - implementation "com.google.dagger:hilt-android:2.33-beta" - kapt "com.google.dagger:hilt-android-compiler:2.33-beta" + implementation "com.google.dagger:hilt-android:2.35" + kapt "com.google.dagger:hilt-android-compiler:2.35" // Google Play Store API implementation 'com.google.android.play:core:1.10.0' // Kotlin Coroutines for Android - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3' } \ No newline at end of file diff --git a/app/src/main/java/com/teambeme/beme/MyFirebaseMessagingService.kt b/app/src/main/java/com/teambeme/beme/MyFirebaseMessagingService.kt index 901ac7e..6672a03 100644 --- a/app/src/main/java/com/teambeme/beme/MyFirebaseMessagingService.kt +++ b/app/src/main/java/com/teambeme/beme/MyFirebaseMessagingService.kt @@ -17,45 +17,37 @@ import com.teambeme.beme.main.view.MainActivity import com.teambeme.beme.notification.view.NotificationActivity class MyFirebaseMessagingService : FirebaseMessagingService() { - override fun onNewToken(token: String) { Log.d(TAG, "new Token: $token") -// val pref = this.getSharedPreferences("token", Context.MODE_PRIVATE) -// val editor = pref.edit() -// editor.putString("token", token).apply() -// editor.commit() -// -// Log.i("로그: ", "성공적으로 토큰을 저장함") - sendRegistrationToServer(token) } override fun onMessageReceived(remoteMessage: RemoteMessage) { - Log.d(TAG, "From: " + remoteMessage.from) + Log.d(TAG, "From: " + remoteMessage.data) super.onMessageReceived(remoteMessage) - if (remoteMessage.data.isNotEmpty()) { - Log.i("바디: ", remoteMessage.data["body"].toString()) - Log.i("타이틀: ", remoteMessage.data["title"].toString()) + Log.i("notice 바디: ", remoteMessage.data["body"].toString()) + Log.i("notice 타이틀: ", remoteMessage.data["title"].toString()) if (remoteMessage.data["title"].toString() == "오늘의 질문") { sendMainNotification(remoteMessage) } else { sendNotiNotification(remoteMessage) } } else { - Log.i("수신에러: ", "data가 비어있습니다. 메시지를 수신하지 못했습니다.") - Log.i("data값: ", remoteMessage.data.toString()) + Log.i("notice 수신에러: ", "data가 비어있습니다. 메시지를 수신하지 못했습니다.") + Log.i("notice data값: ", remoteMessage.data.toString()) } } private fun sendMainNotification(remoteMessage: RemoteMessage) { - val uniId = 0 + val uniId = remoteMessage.sentTime.toInt() val intent = Intent(this, MainActivity::class.java) + intent.putExtra("isOpenFromPushAlarm", true) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - val pendingIntent = PendingIntent.getActivities( - this, uniId, arrayOf(intent), PendingIntent.FLAG_ONE_SHOT + val pendingIntent = PendingIntent.getActivity( + this, uniId, intent, PendingIntent.FLAG_ONE_SHOT ) val channelId = "노티피케이션 메시지" @@ -63,9 +55,9 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val notificationBuilder = - NotificationCompat.Builder(this, channelId).setSmallIcon(R.mipmap.ic_launcher) - .setContentTitle(remoteMessage.data["body"].toString()) - .setContentText(remoteMessage.data["title"].toString()) + NotificationCompat.Builder(this, channelId).setSmallIcon(R.mipmap.ic_beme) + .setContentTitle(remoteMessage.data["title"].toString()) + .setContentText(remoteMessage.data["body"].toString()) .setAutoCancel(true) .setSound(soundUri) .setContentIntent(pendingIntent) @@ -82,13 +74,13 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { } private fun sendNotiNotification(remoteMessage: RemoteMessage) { - val uniId = 0 + val uniId = remoteMessage.sentTime.toInt() val intent = Intent(this, NotificationActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - val pendingIntent = PendingIntent.getActivities( - this, uniId, arrayOf(intent), PendingIntent.FLAG_ONE_SHOT + val pendingIntent = PendingIntent.getActivity( + this, uniId, intent, PendingIntent.FLAG_ONE_SHOT ) val channelId = "노티피케이션 메시지" @@ -96,9 +88,9 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val notificationBuilder = - NotificationCompat.Builder(this, channelId).setSmallIcon(R.mipmap.ic_launcher) - .setContentTitle(remoteMessage.data["body"].toString()) - .setContentText(remoteMessage.data["title"].toString()) + NotificationCompat.Builder(this, channelId).setSmallIcon(R.mipmap.ic_beme) + .setContentTitle(remoteMessage.data["title"].toString()) + .setContentText(remoteMessage.data["body"].toString()) .setAutoCancel(true) .setSound(soundUri) .setContentIntent(pendingIntent) diff --git a/app/src/main/java/com/teambeme/beme/explore/viewmodel/ExploreViewModel.kt b/app/src/main/java/com/teambeme/beme/explore/viewmodel/ExploreViewModel.kt index 9f4abb2..649e523 100644 --- a/app/src/main/java/com/teambeme/beme/explore/viewmodel/ExploreViewModel.kt +++ b/app/src/main/java/com/teambeme/beme/explore/viewmodel/ExploreViewModel.kt @@ -18,9 +18,8 @@ import javax.inject.Inject class ExploreViewModel @Inject constructor( private val exploreRepository: ExploreRepository ) : ViewModel() { - private var _userNickname: String = "" - val userNickname: String - get() = _userNickname + var userNickname: String = "" + private set private var tempOtherQuestionsList: MutableList? = mutableListOf() @@ -49,13 +48,11 @@ class ExploreViewModel @Inject constructor( private val chipChecked: MutableList get() = _chipChecked - private var _categoryNum: Int? = null - val categoryNum: Int? - get() = _categoryNum + var categoryNum: Int? = null + private set - private var _page: Int = 2 - val page: Int - get() = _page + var page: Int = 2 + private set private var _isMorePage = MutableLiveData(false) val isMorePage: LiveData @@ -63,26 +60,25 @@ class ExploreViewModel @Inject constructor( private var otherAnswersQuestionsID: Int = 0 - private var _tempPage: Int = 1 - val tempPage: Int - get() = _tempPage + var tempPage: Int = 1 + private set fun setPageAtRefresh() { - _page = 2 + page = 2 } fun setCategoryNum(category: Int) { clearTempOtherQuestionsList() - _page = 2 + page = 2 chipChecked[category - 1] = !chipChecked[category - 1] if (chipChecked == listOf(false, false, false, false, false, false)) { - _categoryNum = null + categoryNum = null } else { _chipChecked = mutableListOf(false, false, false, false, false, false) chipChecked[category - 1] = !chipChecked[category - 1] - _categoryNum = category + categoryNum = category } - requestOtherQuestionsWithCategorySorting(_categoryNum, tempPage) + requestOtherQuestionsWithCategorySorting(categoryNum, tempPage) } fun clearTempOtherQuestionsList() { @@ -112,18 +108,15 @@ class ExploreViewModel @Inject constructor( "recursion", "pageNum : $pageNum, page : $page, tempPage : $tempPage" ) + val responseData = requireNotNull(response.body()) if (pageNum != page) { if (tempPage == 1) { clearTempOtherQuestionsList() } - response.body()!!.data.answers.toMutableList().let { - tempOtherQuestionsList?.addAll( - it - ) - } - _tempPage++ - if (response.body()!!.data.answers.isNotEmpty()) { - _isMorePage.value = response.body()!!.data.answers.size == 10 + tempOtherQuestionsList?.addAll(responseData.data.answers.toMutableList()) + tempPage++ + if (responseData.data.answers.isNotEmpty()) { + _isMorePage.value = responseData.data.answers.size == 10 } else { _isMorePage.value = false } @@ -133,7 +126,7 @@ class ExploreViewModel @Inject constructor( ) } else { _otherQuestionsList.value = tempOtherQuestionsList?.toMutableList() - _tempPage = 1 + tempPage = 1 } Log.d( "recursion", @@ -151,8 +144,8 @@ class ExploreViewModel @Inject constructor( fun requestPlusOtherQuestions() { exploreRepository.getExplorationOtherQuestions( - _page, - _categoryNum + page, + categoryNum ) .enqueue( object : Callback { @@ -161,11 +154,12 @@ class ExploreViewModel @Inject constructor( response: Response ) { if (response.isSuccessful) { - tempOtherQuestionsList?.addAll(response.body()!!.data.answers.toMutableList()) + val responseData = requireNotNull(response.body()) + tempOtherQuestionsList?.addAll(responseData.data.answers.toMutableList()) _otherQuestionsList.value = tempOtherQuestionsList?.toMutableList() - if (response.body()!!.data.answers.size == 10) { - _page++ + if (responseData.data.answers.size == 10) { + page++ _isMorePage.value = true } else { _isMorePage.value = false @@ -196,13 +190,14 @@ class ExploreViewModel @Inject constructor( "recursion_detail", "pageNum : $pageNum, page : $page, tempPage : $tempPage" ) + val responseData = requireNotNull(response.body()) if (pageNum != page) { if (tempPage == 1) { clearTempSameQuestionOtherAnswersList() } - tempSameQuestionOtherAnswersList?.addAll(response.body()!!.data.answers.toMutableList()) - _tempPage++ - _isMorePage.value = response.body()!!.data.answers.size == 10 + tempSameQuestionOtherAnswersList?.addAll(responseData.data.answers.toMutableList()) + tempPage++ + _isMorePage.value = responseData.data.answers.size == 10 requestSameQuestionsOtherAnswers( otherAnswersQuestionsID, tempPage @@ -210,7 +205,7 @@ class ExploreViewModel @Inject constructor( } else { _sameQuestionOtherAnswersList.value = tempSameQuestionOtherAnswersList?.toMutableList() - _tempPage = 1 + tempPage = 1 } Log.d( "recursion_detail", @@ -229,7 +224,7 @@ class ExploreViewModel @Inject constructor( fun requestPlusSameQuestionOtherAnswers() { exploreRepository.getExplorationSameQuestionOtherAnswers( otherAnswersQuestionsID, - _page + page ).enqueue( object : Callback { override fun onResponse( @@ -237,12 +232,13 @@ class ExploreViewModel @Inject constructor( response: Response ) { if (response.isSuccessful) { - tempSameQuestionOtherAnswersList?.addAll(response.body()!!.data.answers.toMutableList()) + val responseData = requireNotNull(response.body()) + tempSameQuestionOtherAnswersList?.addAll(responseData.data.answers.toMutableList()) _sameQuestionOtherAnswersList.value = tempSameQuestionOtherAnswersList?.toMutableList() - if (response.body()!!.data.answers.size == 10) { - _page++ + if (responseData.data.answers.size == 10) { + page++ _isMorePage.value = true } else { _isMorePage.value = false @@ -289,8 +285,9 @@ class ExploreViewModel @Inject constructor( response: Response ) { if (response.isSuccessful) { + val responseData = requireNotNull(response.body()) Log.d("scrap_viewmodel", answerId.toString()) - _scrapData = response.body()!! + _scrapData = responseData Log.d("scrap_1", scrapData.message) } } diff --git a/app/src/main/java/com/teambeme/beme/main/view/MainActivity.kt b/app/src/main/java/com/teambeme/beme/main/view/MainActivity.kt index b76b1f1..1adcd3d 100644 --- a/app/src/main/java/com/teambeme/beme/main/view/MainActivity.kt +++ b/app/src/main/java/com/teambeme/beme/main/view/MainActivity.kt @@ -45,6 +45,9 @@ class MainActivity : BindingActivity(R.layout.activity_main setViewPagerAdapter(this) setBottomNavigationSelectListener(binding.bnvMain) setBottomNavigationReSelectListener(binding.bnvMain) + if (intent.getBooleanExtra("isOpenFromPushAlarm", false)) { + binding.vpMain.setCurrentItem(2, true) + } } private fun setBottomNavigationSelectListener(bottomNavigationView: BottomNavigationView) { diff --git a/app/src/main/java/com/teambeme/beme/mypage/model/CategoryFilter.kt b/app/src/main/java/com/teambeme/beme/mypage/model/CategoryFilter.kt index 0ded555..7c545f6 100644 --- a/app/src/main/java/com/teambeme/beme/mypage/model/CategoryFilter.kt +++ b/app/src/main/java/com/teambeme/beme/mypage/model/CategoryFilter.kt @@ -6,6 +6,7 @@ enum class CategoryFilter( private val checkId: Int, private val itemId: String ) { + NONE(-1, "CLICK_ANY_CATEGORY_MPFILTER"), VALUES(R.id.chip_write_1, "CLICK_VALUES_MPFILTER"), RELATIONSHIP(R.id.chip_write_2, "CLICK_RELATIONSHIP_MPFILTER"), LOVE(R.id.chip_write_3, "CLICK_LOVE_MPFILTER"), diff --git a/app/src/main/java/com/teambeme/beme/mypage/model/PublicRange.kt b/app/src/main/java/com/teambeme/beme/mypage/model/PublicRange.kt index ceb601d..5a22976 100644 --- a/app/src/main/java/com/teambeme/beme/mypage/model/PublicRange.kt +++ b/app/src/main/java/com/teambeme/beme/mypage/model/PublicRange.kt @@ -6,6 +6,7 @@ enum class PublicRange( private val checkId: Int, private val itemId: String ) { + NONE(-1, "CLICK_ANY_PUBLIC_MPFILTER"), ALL(R.id.chip_range_1, "CLICK_ALL_MPFILTER"), OPEN(R.id.chip_range_2, "CLICK_OPEN_MPFILTER"), PRIVATE(R.id.chip_range_3, "CLICK_PRIVATE_MPFILTER"); diff --git a/app/src/main/java/com/teambeme/beme/mypage/view/BottomWriteFragment.kt b/app/src/main/java/com/teambeme/beme/mypage/view/BottomWriteFragment.kt index 8ad4835..4b46ef1 100644 --- a/app/src/main/java/com/teambeme/beme/mypage/view/BottomWriteFragment.kt +++ b/app/src/main/java/com/teambeme/beme/mypage/view/BottomWriteFragment.kt @@ -56,6 +56,7 @@ class BottomWriteFragment(private val filter: Boolean) : BottomSheetDialogFragme R.id.chip_write_4 -> category = CATEGORY_DAILY R.id.chip_write_5 -> category = CATEGORY_ME R.id.chip_write_6 -> category = CATEGORY_STORY + else -> category = CATEGORY_ALL } } @@ -64,6 +65,7 @@ class BottomWriteFragment(private val filter: Boolean) : BottomSheetDialogFragme R.id.chip_range_1 -> range = null R.id.chip_range_2 -> range = "public" R.id.chip_range_3 -> range = "unpublic" + else -> range = null } } @@ -76,6 +78,7 @@ class BottomWriteFragment(private val filter: Boolean) : BottomSheetDialogFragme } companion object { + private val CATEGORY_ALL = null private val CATEGORY_VALUE = 1 private val CATEGORY_RELATION = 2 private val CATEGORY_LOVE = 3 diff --git a/app/src/main/java/com/teambeme/beme/mypage/view/MyScrapFragment.kt b/app/src/main/java/com/teambeme/beme/mypage/view/MyScrapFragment.kt index 26f3561..e80fbe7 100644 --- a/app/src/main/java/com/teambeme/beme/mypage/view/MyScrapFragment.kt +++ b/app/src/main/java/com/teambeme/beme/mypage/view/MyScrapFragment.kt @@ -26,7 +26,6 @@ class MyScrapFragment : BindingFragment(R.layout.fragmen super.onCreateView(inflater, container, savedInstanceState) binding.lifecycleOwner = viewLifecycleOwner binding.myPageViewModel = mypageViewModel - mypageViewModel.initScrap() setMyScrapAdapter() setMyPageScrapDataObserve() setIsScrapFilterClickedObserve() @@ -42,8 +41,7 @@ class MyScrapFragment : BindingFragment(R.layout.fragmen override fun onResume() { super.onResume() RecordScreenUtil.recordScreen("MyPage_MyScrapFragment") - mypageViewModel.initScrapPage() - mypageViewModel.getMyScrap() + mypageViewModel.getMyScrap(mypageViewModel.scrapTempPage) } private fun setMyScrapAdapter() { @@ -80,7 +78,7 @@ class MyScrapFragment : BindingFragment(R.layout.fragmen mypageViewModel.scrapFilter.observe(viewLifecycleOwner) { scrapFilter -> scrapFilter.let { mypageViewModel.initScrapPage() - mypageViewModel.getMyScrap() + mypageViewModel.getMyScrap(mypageViewModel.scrapTempPage) } } } @@ -116,7 +114,7 @@ class MyScrapFragment : BindingFragment(R.layout.fragmen androidx.appcompat.widget.SearchView.OnQueryTextListener { override fun onQueryTextSubmit(newText: String?): Boolean { mypageViewModel.initScrapPage() - mypageViewModel.getMyScrap() + mypageViewModel.getMyScrap(mypageViewModel.scrapTempPage) return false } @@ -126,7 +124,7 @@ class MyScrapFragment : BindingFragment(R.layout.fragmen if (userInputText.count() == 0) { mypageViewModel.initScrapPage() mypageViewModel.deleteScrapQuery() - mypageViewModel.getMyScrap() + mypageViewModel.getMyScrap(mypageViewModel.scrapTempPage) } return false } diff --git a/app/src/main/java/com/teambeme/beme/mypage/view/MyWriteFragment.kt b/app/src/main/java/com/teambeme/beme/mypage/view/MyWriteFragment.kt index cd67c57..35c29ce 100644 --- a/app/src/main/java/com/teambeme/beme/mypage/view/MyWriteFragment.kt +++ b/app/src/main/java/com/teambeme/beme/mypage/view/MyWriteFragment.kt @@ -28,7 +28,6 @@ class MyWriteFragment : BindingFragment(R.layout.fragmen super.onCreateView(inflater, container, savedInstanceState) binding.myPageViewModel = mypageViewModel binding.lifecycleOwner = viewLifecycleOwner - mypageViewModel.initMyAnswer() setMyWriteAdapter() setMyWriteObserve() setIsWriteFilterClickedObserve() @@ -45,8 +44,7 @@ class MyWriteFragment : BindingFragment(R.layout.fragmen override fun onResume() { super.onResume() RecordScreenUtil.recordScreen("MyPage_MyWriteFragment") - mypageViewModel.initPage() - mypageViewModel.getMyAnswer() + mypageViewModel.getMyAnswer(mypageViewModel.tempPage) } private fun setMyWriteAdapter() { @@ -83,7 +81,8 @@ class MyWriteFragment : BindingFragment(R.layout.fragmen mypageViewModel.mywriteFilter.observe(viewLifecycleOwner) { myWriteFilter -> myWriteFilter.let { mypageViewModel.initPage() - mypageViewModel.getMyAnswer() + mypageViewModel.clearCopyMyAnswerList() + mypageViewModel.getMyAnswer(mypageViewModel.tempPage) } } } @@ -127,7 +126,8 @@ class MyWriteFragment : BindingFragment(R.layout.fragmen override fun onQueryTextSubmit(newText: String?): Boolean { Log.d("Search", newText ?: "hyunwoo") mypageViewModel.initPage() - mypageViewModel.getMyAnswer() + mypageViewModel.clearCopyMyAnswerList() + mypageViewModel.getMyAnswer(mypageViewModel.tempPage) return false } @@ -137,7 +137,8 @@ class MyWriteFragment : BindingFragment(R.layout.fragmen if (userInputText.count() == 0) { mypageViewModel.initPage() mypageViewModel.deleteMyQuery() - mypageViewModel.getMyAnswer() + mypageViewModel.clearCopyMyAnswerList() + mypageViewModel.getMyAnswer(mypageViewModel.tempPage) } return false } diff --git a/app/src/main/java/com/teambeme/beme/mypage/viewmodel/MyPageViewModel.kt b/app/src/main/java/com/teambeme/beme/mypage/viewmodel/MyPageViewModel.kt index 8305936..f56b639 100644 --- a/app/src/main/java/com/teambeme/beme/mypage/viewmodel/MyPageViewModel.kt +++ b/app/src/main/java/com/teambeme/beme/mypage/viewmodel/MyPageViewModel.kt @@ -1,6 +1,5 @@ package com.teambeme.beme.mypage.viewmodel -import com.teambeme.beme.util.SingleLiveEvent import android.net.Uri import android.util.Log import androidx.lifecycle.LiveData @@ -8,6 +7,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.teambeme.beme.data.repository.MyPageRepository import com.teambeme.beme.mypage.model.* +import com.teambeme.beme.util.SingleLiveEvent import dagger.hilt.android.lifecycle.HiltViewModel import okhttp3.MultipartBody import retrofit2.Call @@ -38,8 +38,23 @@ class MyPageViewModel @Inject constructor( val scrapScrollUp: LiveData get() = _scrapScrollUp - private var page = 1 - private var scrapPage = 1 + private var page: Int = 2 + + var tempPage: Int = 1 + private set + + private var scrapPage = 2 + + var scrapTempPage: Int = 1 + private set + + fun clearCopyMyAnswerList() { + copyMyAnswerList.clear() + } + + fun clearCopyMyScrapList() { + copyMyScrapList.clear() + } private val _scrapFilter = MutableLiveData() val scrapFilter: LiveData @@ -65,24 +80,12 @@ class MyPageViewModel @Inject constructor( _scrapQuery.value = null } - fun initMyAnswer() { - _mywriteFilter.value?.category = null - _mywriteFilter.value?.range = null - page = 1 - _myQuery.value = null - } - fun initPage() { - page = 1 - } - - fun initScrap() { - scrapPage = 1 - _scrapQuery.value = null + page = 2 } fun initScrapPage() { - scrapPage = 1 + scrapPage = 2 } fun setScrapFilter(range: String?, category: Int?) { @@ -163,12 +166,12 @@ class MyPageViewModel @Inject constructor( val isScrapEmpty: LiveData get() = _isScrapEmpty - fun getMyAnswer() { + fun getMyAnswer(pageNum: Int) { myPageRepository.getMyAnswer( mywriteFilter.value?.range, mywriteFilter.value?.category, myQuery.value, - page + pageNum ).enqueue(object : Callback { override fun onResponse( @@ -176,25 +179,51 @@ class MyPageViewModel @Inject constructor( response: Response ) { if (response.isSuccessful) { - if (page == 1) { - copyMyAnswerList = response.body()!!.data?.answers?.toMutableList() - _isAnswerEmpty.value = copyMyAnswerList.size == 0 - _mypageWriteData.value = copyMyAnswerList.toMutableList() - if (response.body()!!.data.answers.size == 10) { - _isAnswerMax.value = false - page++ + if (pageNum != page) { + if (tempPage == 1) { + clearCopyMyAnswerList() + } + copyMyAnswerList.addAll(response.body()!!.data.answers.toMutableList()) + tempPage++ + if (response.body()!!.data.answers.isNotEmpty()) { + _isAnswerMax.value = response.body()!!.data.answers.size != 10 } else { _isAnswerMax.value = true } + getMyAnswer(tempPage) } else { - copyMyAnswerList.addAll(response.body()!!.data?.answers?.toMutableList()) _mypageWriteData.value = copyMyAnswerList.toMutableList() - if (response.body()!!.data.answers.size == 10) { - _isAnswerMax.value = false - page++ - } else { - _isAnswerMax.value = true - } + tempPage = 1 + } + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d("Network Fail", t.message.toString()) + } + }) + } + + fun getPlusMyAnswer() { + myPageRepository.getMyAnswer( + mywriteFilter.value?.range, + mywriteFilter.value?.category, + myQuery.value, + page + ).enqueue(object : + Callback { + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful) { + copyMyAnswerList.addAll(response.body()!!.data.answers.toMutableList()) + _mypageWriteData.value = copyMyAnswerList.toMutableList() + if (response.body()!!.data.answers.isNotEmpty()) { + page++ + _isAnswerMax.value = response.body()!!.data.answers.size != 10 + } else { + _isAnswerMax.value = true } } } @@ -227,12 +256,12 @@ class MyPageViewModel @Inject constructor( }) } - fun getMyScrap() { + fun getMyScrap(pageNum: Int) { myPageRepository.getMyScrap( scrapFilter.value?.range, scrapFilter.value?.category, scrapQuery.value, - scrapPage + pageNum ).enqueue(object : Callback { override fun onResponse( @@ -240,26 +269,59 @@ class MyPageViewModel @Inject constructor( response: Response ) { if (response.isSuccessful) { - if (scrapPage == 1) { - copyMyScrapList = response.body()!!.data?.answers?.toMutableList() - _isScrapEmpty.value = copyMyScrapList.size == 0 - _mypageScrapData.value = copyMyScrapList.toMutableList() - - if (response.body()!!.data.answers.size == 10) { - _isScrapMax.value = false - scrapPage++ + Log.d( + "recursion_mypage", + "pageNum : $pageNum, page : $page, tempPage : $scrapTempPage" + ) + if (pageNum != scrapPage) { + if (scrapTempPage == 1) { + clearCopyMyScrapList() + } + copyMyScrapList.addAll(response.body()!!.data.answers.toMutableList()) + scrapTempPage++ + if (response.body()!!.data.answers.isNotEmpty()) { + _isScrapMax.value = response.body()!!.data.answers.size != 10 } else { _isScrapMax.value = true } + getMyScrap(scrapTempPage) } else { - copyMyScrapList.addAll(response.body()!!.data?.answers.toMutableList()) _mypageScrapData.value = copyMyScrapList.toMutableList() - if (response.body()!!.data.answers.size == 10) { - _isScrapMax.value = false - scrapPage++ - } else { - _isScrapMax.value = true - } + scrapTempPage = 1 + } + Log.d( + "recursion_mypage", + " tempPage : $scrapTempPage" + ) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d("Network Fail", t.message.toString()) + } + }) + } + + fun getPlusMyScrap() { + myPageRepository.getMyScrap( + scrapFilter.value?.range, + scrapFilter.value?.category, + scrapQuery.value, + page + ).enqueue(object : + Callback { + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful) { + copyMyScrapList.addAll(response.body()!!.data.answers.toMutableList()) + _mypageScrapData.value = copyMyScrapList.toMutableList() + if (response.body()!!.data.answers.isNotEmpty()) { + scrapPage++ + _isScrapMax.value = response.body()!!.data.answers.size != 10 + } else { + _isScrapMax.value = true } } } diff --git a/app/src/main/java/com/teambeme/beme/signup/domain/entity/User.kt b/app/src/main/java/com/teambeme/beme/signup/domain/entity/User.kt new file mode 100644 index 0000000..241de6a --- /dev/null +++ b/app/src/main/java/com/teambeme/beme/signup/domain/entity/User.kt @@ -0,0 +1,19 @@ +package com.teambeme.beme.signup.domain.entity + +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody + +data class User( + val email: String, + val nickName: String, + val password: String +) { + fun toRequestBody(): HashMap { + return hashMapOf( + "email" to email.toRequestBody("text/plain".toMediaTypeOrNull()), + "nickname" to nickName.toRequestBody("text/plain".toMediaTypeOrNull()), + "password" to password.toRequestBody("text/plain".toMediaTypeOrNull()) + ) + } +} diff --git a/app/src/main/java/com/teambeme/beme/signup/view/ImageChooseFragment.kt b/app/src/main/java/com/teambeme/beme/signup/view/ImageChooseFragment.kt index 97df607..76d19a2 100644 --- a/app/src/main/java/com/teambeme/beme/signup/view/ImageChooseFragment.kt +++ b/app/src/main/java/com/teambeme/beme/signup/view/ImageChooseFragment.kt @@ -12,9 +12,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.databinding.DataBindingUtil -import androidx.fragment.app.Fragment +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.ktx.analytics @@ -23,13 +23,12 @@ import com.google.firebase.ktx.Firebase import com.gun0912.tedpermission.PermissionListener import com.gun0912.tedpermission.TedPermission import com.teambeme.beme.R +import com.teambeme.beme.base.BindingFragment import com.teambeme.beme.databinding.FragmentImageChooseBinding import com.teambeme.beme.signup.viewmodel.SignUpViewModel import com.teambeme.beme.util.recordClickEvent import com.theartofdev.edmodo.cropper.CropImage import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody @@ -38,8 +37,8 @@ import java.io.ByteArrayOutputStream import java.io.File @AndroidEntryPoint -class ImageChooseFragment : Fragment() { - private lateinit var binding: FragmentImageChooseBinding +class ImageChooseFragment : + BindingFragment(R.layout.fragment_image_choose) { private val signUpViewModel: SignUpViewModel by activityViewModels() override fun onAttach(context: Context) { super.onAttach(context) @@ -54,31 +53,52 @@ class ImageChooseFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = - DataBindingUtil.inflate(inflater, R.layout.fragment_image_choose, container, false) + super.onCreateView(inflater, container, savedInstanceState) binding.viewModel = signUpViewModel binding.lifecycleOwner = viewLifecycleOwner + setUiClickListener() + subscribeData() + return binding.root + } + + private fun subscribeData() { + signUpViewModel.profileImageUri.observe(viewLifecycleOwner) { + binding.imgChooseImagepick.visibility = View.GONE + binding.imgChooseImage.apply { + isVisible = true + setImageURI(it) + } + } + signUpViewModel.signUpUserInfo.observe(viewLifecycleOwner) { userInfo -> + if (userInfo != null) { + when (userInfo.success) { + true -> { + Toast.makeText(requireContext(), "회원가입 성공", Toast.LENGTH_SHORT).show() + requireActivity().finish() + } + else -> { + Log.d("SignUp", userInfo.message) + } + } + } else { + Toast.makeText(requireContext(), "회원가입이 실패했습니다", Toast.LENGTH_SHORT).show() + } + } + } + private fun setUiClickListener() { binding.btnBack.setOnClickListener { view -> recordClickEvent("BACK_PRESS", "OUT_PROFILE_SIGN") view.findNavController().popBackStack() } binding.btnImageChooseDone.setOnClickListener { - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { signUpViewModel.signUp().join() Toast.makeText(requireContext(), "회원가입 성공", Toast.LENGTH_SHORT).show() } } - signUpViewModel.profileImageUri.observe(viewLifecycleOwner) { - binding.imgChooseImagepick.visibility = View.GONE - binding.imgChooseImage.apply { - visibility = View.VISIBLE - setImageURI(it) - } - } - binding.imgChooseImagepick.setOnClickListener { val permissionListener = object : PermissionListener { override fun onPermissionGranted() { @@ -98,20 +118,6 @@ class ImageChooseFragment : Fragment() { ) .check() } - - signUpViewModel.signUpUserInfo.observe(viewLifecycleOwner) { userInfo -> - if (userInfo != null) { - if (userInfo.success) { - Toast.makeText(requireContext(), "회원가입 성공", Toast.LENGTH_SHORT).show() - requireActivity().finish() - } else { - Log.d("SignUp", userInfo.message) - } - } else { - Toast.makeText(requireContext(), "회원가입이 실패했습니다", Toast.LENGTH_SHORT).show() - } - } - return binding.root } private fun pickImage() { diff --git a/app/src/main/java/com/teambeme/beme/signup/view/PersonalInfoFragment.kt b/app/src/main/java/com/teambeme/beme/signup/view/PersonalInfoFragment.kt index e62c65e..c9db005 100644 --- a/app/src/main/java/com/teambeme/beme/signup/view/PersonalInfoFragment.kt +++ b/app/src/main/java/com/teambeme/beme/signup/view/PersonalInfoFragment.kt @@ -6,8 +6,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.databinding.DataBindingUtil -import androidx.fragment.app.Fragment +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.navigation.findNavController import com.google.firebase.analytics.FirebaseAnalytics @@ -15,14 +14,16 @@ import com.google.firebase.analytics.ktx.analytics import com.google.firebase.analytics.ktx.logEvent import com.google.firebase.ktx.Firebase import com.teambeme.beme.R +import com.teambeme.beme.base.BindingFragment import com.teambeme.beme.databinding.FragmentPersonalInfoBinding import com.teambeme.beme.signup.viewmodel.SignUpViewModel +import com.teambeme.beme.util.color import com.teambeme.beme.util.recordClickEvent import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class PersonalInfoFragment : Fragment() { - private lateinit var binding: FragmentPersonalInfoBinding +class PersonalInfoFragment : + BindingFragment(R.layout.fragment_personal_info) { private val signUpViewModel: SignUpViewModel by activityViewModels() override fun onAttach(context: Context) { super.onAttach(context) @@ -37,234 +38,184 @@ class PersonalInfoFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = - DataBindingUtil.inflate(inflater, R.layout.fragment_personal_info, container, false) + super.onCreateView(inflater, container, savedInstanceState) binding.lifecycleOwner = viewLifecycleOwner binding.signUpViewModel = signUpViewModel + setUiClickListener() + subscribeData() + return binding.root + } + + private fun setUiClickListener() { binding.btnPersonalBack.setOnClickListener { view -> recordClickEvent("BACK_PRESS", "OUT_INF_SIGN") view.findNavController().popBackStack() } - setDoubleCheckListener() - setDoneButtonClickListener() - setObserve() - return binding.root + binding.btnPersonalDone.setOnClickListener { view -> + view.findNavController() + .navigate(R.id.action_personalInfoFragment_to_imageChooseFragment) + signUpViewModel.setUserInfo() + } } - private fun setDoubleCheckListener() { - signUpViewModel.nickDoubleCheck.observe(viewLifecycleOwner) { checkInfo -> - if (signUpViewModel.isNickNameValidated.value!!) { - if (!checkInfo.data.nicknameExist) { - fixNickName() - signUpViewModel.nickDoubleCheckValidated() - } else { - binding.txtPersonalNicknameCheck.apply { - text = "이미 존재하는 닉네임입니다" - setTextColor(resources.getColor(R.color.signup_red, null)) - } - binding.imgPersonalNicknameCheck.setImageResource(R.drawable.ic_personal_check_red) + private fun subscribeData() { + signUpViewModel.userEmail.observe(viewLifecycleOwner) { email -> + if (email.isNullOrBlank()) { + binding.txtPersonalEmailCheck.apply { + text = "bean@example.com 형식으로 입력해 주세요" + setTextColor(color(R.color.signup_personal_check)) } + binding.imgPersonalEmailCheck.setImageResource(R.drawable.ic_personal_check_gray) } - checkButtonEnable() } - } - - private fun fixNickName() { - Toast.makeText( - requireContext(), - "닉네임이 ${signUpViewModel.userNickName.value!!}로 설정되었습니다.", - Toast.LENGTH_SHORT - ).show() - binding.apply { - edittxtPersonalNickname.isEnabled = false - edittxtPersonalNickname.setTextColor(resources.getColor(R.color.signup_disabled, null)) - btnPersonalNicknameDoubleCheck.visibility = View.GONE - txtPersonalNicknameCheck.apply { - text = "사용 가능한 닉네임입니다" - setTextColor(resources.getColor(R.color.signup_term_blue, null)) + signUpViewModel.isEmailValid.observe(viewLifecycleOwner) { + if (requireNotNull(signUpViewModel.userEmail.value).isNotEmpty()) { + when (it) { + true -> { + binding.txtPersonalEmailCheck.apply { + text = "형식에 맞는 이메일입니다" + setTextColor(color(R.color.signup_term_blue)) + } + binding.imgPersonalEmailCheck.setImageResource(R.drawable.ic_personal_check_blue) + } + else -> { + binding.txtPersonalEmailCheck.apply { + text = "형식에 맞지 않는 이메일입니다" + setTextColor(color(R.color.signup_red)) + } + binding.imgPersonalEmailCheck.setImageResource(R.drawable.ic_personal_check_red) + } + } } } - } - private fun setObserve() { - signUpViewModel.userEmail.observe(viewLifecycleOwner) { email -> - if (email.isNullOrBlank()) { - binding.txtPersonalEmailCheck.text = "bean@example.com 형식으로 입력해 주세요" - binding.txtPersonalEmailCheck.setTextColor( - resources.getColor( - R.color.signup_personal_check, - null - ) - ) - binding.imgPersonalEmailCheck.setImageResource(R.drawable.ic_personal_check_gray) - signUpViewModel.emailNotValidated() - } else { - if (REGEX_EMAIL.matches(email)) { - binding.txtPersonalEmailCheck.text = "형식에 맞는 이메일입니다" - binding.txtPersonalEmailCheck.setTextColor( - resources.getColor( - R.color.signup_term_blue, - null - ) - ) - binding.imgPersonalEmailCheck.setImageResource(R.drawable.ic_personal_check_blue) - signUpViewModel.emailValidated() + signUpViewModel.isNicknameValid.observe(viewLifecycleOwner) { + if (requireNotNull(signUpViewModel.userNickName.value).isNotEmpty()) { + binding.btnPersonalNicknameDoubleCheck.isEnabled = it + if (!it) { + binding.imgPersonalNicknameCheck.setImageResource(R.drawable.ic_personal_check_red) + binding.txtPersonalNicknameCheck.apply { + text = "다른 닉네임을 입력해주세요" + setTextColor(color(R.color.signup_red)) + } + signUpViewModel.nickDoubleCheckInvalidated() + } else if (signUpViewModel.isDoubleCheckedId(requireNotNull(signUpViewModel.userNickName.value))) { + binding.imgPersonalNicknameCheck.setImageResource(R.drawable.ic_personal_check_blue) + binding.txtPersonalNicknameCheck.apply { + text = "사용 가능한 닉네임입니다" + setTextColor(color(R.color.signup_term_blue)) + } + signUpViewModel.nickDoubleCheckValidated() } else { - binding.txtPersonalEmailCheck.text = "형식에 맞지 않는 이메일입니다" - binding.txtPersonalEmailCheck.setTextColor( - resources.getColor( - R.color.signup_red, - null - ) - ) - binding.imgPersonalEmailCheck.setImageResource(R.drawable.ic_personal_check_red) - signUpViewModel.emailNotValidated() + binding.imgPersonalNicknameCheck.setImageResource(R.drawable.ic_personal_check_blue) + binding.txtPersonalNicknameCheck.apply { + text = "사용 가능한 닉네임입니다, 중복확인을 해주세요" + setTextColor(color(R.color.signup_term_blue)) + } + signUpViewModel.nickDoubleCheckInvalidated() } } - checkButtonEnable() } signUpViewModel.userNickName.observe(viewLifecycleOwner) { nickName -> if (nickName.isEmpty()) { binding.imgPersonalNicknameCheck.setImageResource(R.drawable.ic_personal_check_gray) - binding.txtPersonalNicknameCheck.text = "영문, 숫자로 5자 이상 20자 이내로 입력해 주세요." - binding.txtPersonalNicknameCheck.setTextColor( - resources.getColor( - R.color.signup_personal_check, - null - ) - ) - signUpViewModel.nickNameNotValidated() - } else if (!nickName.isLettersOrDigits() || !nickNameLengthValidation(nickName)) { - binding.imgPersonalNicknameCheck.setImageResource(R.drawable.ic_personal_check_red) binding.txtPersonalNicknameCheck.apply { - text = "다른 닉네임을 입력해주세요" - setTextColor(resources.getColor(R.color.signup_red, null)) + text = "영문, 숫자로 5자 이상 20자 이내로 입력해 주세요." + setTextColor(color(R.color.signup_personal_check)) } - signUpViewModel.nickNameNotValidated() } else { + signUpViewModel.nickDoubleCheckInvalidated() + } + + if (signUpViewModel.isDoubleCheckedId(nickName)) { binding.imgPersonalNicknameCheck.setImageResource(R.drawable.ic_personal_check_blue) binding.txtPersonalNicknameCheck.apply { - text = "사용 가능한 닉네임입니다, 중복확인을 해주세요" - setTextColor(resources.getColor(R.color.signup_term_blue, null)) + text = "사용 가능한 닉네임입니다" + setTextColor(color(R.color.signup_term_blue)) } - signUpViewModel.nickNameValidated() + signUpViewModel.nickDoubleCheckValidated() } + } - checkButtonEnable() + signUpViewModel.nickDoubleCheck.observe(viewLifecycleOwner) { checkInfo -> + if (!checkInfo.data.nicknameExist) { + Toast.makeText( + requireContext(), + "닉네임이 ${signUpViewModel.userNickName.value!!}로 설정되었습니다.", + Toast.LENGTH_SHORT + ).show() + binding.txtPersonalNicknameCheck.apply { + text = "사용 가능한 닉네임입니다" + setTextColor(color(R.color.signup_term_blue)) + } + signUpViewModel.registerId(signUpViewModel.userNickName.value!!) + signUpViewModel.nickDoubleCheckValidated() + } else { + binding.txtPersonalNicknameCheck.apply { + text = "이미 존재하는 닉네임입니다" + setTextColor(color(R.color.signup_red)) + } + binding.imgPersonalNicknameCheck.setImageResource(R.drawable.ic_personal_check_red) + } + } + signUpViewModel.isNickNameDoubleChecked.observe(viewLifecycleOwner) { + if (it) { + binding.imgPersonalNicknameCheck.setImageResource(R.drawable.ic_personal_check_blue) + binding.txtPersonalNicknameCheck.apply { + text = "사용 가능한 닉네임입니다" + setTextColor(color(R.color.signup_term_blue)) + } + } } signUpViewModel.userPassWord.observe(viewLifecycleOwner) { passWord -> if (passWord.isEmpty()) { binding.imgPersonalPassword.setImageResource(R.drawable.ic_personal_check_gray) - binding.txtPersonalPassword.text = "비밀번호는 영문 숫자로 8자 이상 입력해 주세요" - binding.txtPersonalPassword.setTextColor( - resources.getColor( - R.color.signup_personal_check, - null - ) - ) - signUpViewModel.passWordNotValidated() - } else if (!passWord.isAlphabets() || !passWord.isNumbers() || !passWordLengthValidation( - passWord - ) - ) { - binding.imgPersonalPassword.setImageResource(R.drawable.ic_personal_check_red) binding.txtPersonalPassword.apply { - text = "8자 이상 20자 이내인 영문과 숫자의 조합이어야 합니다." - setTextColor(resources.getColor(R.color.signup_red, null)) + text = "비밀번호는 영문 숫자로 8자 이상 입력해 주세요" + setTextColor(color(R.color.signup_personal_check)) } - signUpViewModel.passWordNotValidated() - } else { - binding.imgPersonalPassword.setImageResource(R.drawable.ic_personal_check_blue) - binding.txtPersonalPassword.apply { - text = "사용 가능한 비밀번호입니다" - setTextColor(resources.getColor(R.color.signup_term_blue, null)) - } - signUpViewModel.passWordValidated() } - checkButtonEnable() } - - signUpViewModel.userPassWordCheck.observe(viewLifecycleOwner) { passWordCheck -> - if (passWordCheck.isEmpty()) { - binding.linearPersonalPasswordCheck.visibility = View.INVISIBLE - signUpViewModel.passWordCheckNotValidated() - } else { - binding.linearPersonalPasswordCheck.visibility = View.VISIBLE - if (passWordCheck != signUpViewModel.userPassWord.value) { - binding.imgPersonalPasswordCheck.setImageResource(R.drawable.ic_personal_check_red) - binding.txtPersonalPasswordCheck.apply { - text = "비밀번호가 일치하지 않습니다" - setTextColor(resources.getColor(R.color.signup_red, null)) + signUpViewModel.isPasswordValid.observe(viewLifecycleOwner) { + if (requireNotNull(signUpViewModel.userPassWord.value).isNotEmpty()) { + if (it) { + binding.imgPersonalPassword.setImageResource(R.drawable.ic_personal_check_blue) + binding.txtPersonalPassword.apply { + text = "사용 가능한 비밀번호입니다" + setTextColor(color(R.color.signup_term_blue)) } - signUpViewModel.passWordCheckNotValidated() } else { - binding.imgPersonalPasswordCheck.setImageResource(R.drawable.ic_personal_check_blue) - binding.txtPersonalPasswordCheck.apply { - text = "비밀번호가 일치합니다" - setTextColor(resources.getColor(R.color.signup_term_blue, null)) + binding.imgPersonalPassword.setImageResource(R.drawable.ic_personal_check_red) + binding.txtPersonalPassword.apply { + text = "8자 이상 20자 이내인 영문과 숫자의 조합이어야 합니다." + setTextColor(color(R.color.signup_red)) } - signUpViewModel.passWordCheckValidated() } } - checkButtonEnable() } - } - - private fun nickNameLengthValidation(nickName: String): Boolean = nickName.length in 5..20 - private fun passWordLengthValidation(nickName: String): Boolean = nickName.length in 8..20 - private fun setDoneButtonClickListener() { - binding.btnPersonalDone.setOnClickListener { view -> - if (signUpViewModel.validateAllValues()) { - view.findNavController() - .navigate(R.id.action_personalInfoFragment_to_imageChooseFragment) + signUpViewModel.userPassWordCheck.observe(viewLifecycleOwner) { passWordCheck -> + binding.linearPersonalPasswordCheck.isVisible = passWordCheck.isNotEmpty() + } + signUpViewModel.isPasswordDoubleChecked.observe(viewLifecycleOwner) { + if (it) { + binding.imgPersonalPasswordCheck.setImageResource(R.drawable.ic_personal_check_blue) + binding.txtPersonalPasswordCheck.apply { + text = "비밀번호가 일치합니다" + setTextColor(color(R.color.signup_term_blue)) + } } else { - makeProblemToastMessage( - signUpViewModel.isEmailValidated.value!!, - signUpViewModel.isNickNameValidated.value!!, - signUpViewModel.isPassWordValidated.value!!, - signUpViewModel.isPassWordCheckValidated.value!! - ) + binding.imgPersonalPasswordCheck.setImageResource(R.drawable.ic_personal_check_red) + binding.txtPersonalPasswordCheck.apply { + text = "비밀번호가 일치하지 않습니다" + setTextColor(color(R.color.signup_red)) + } } } - } - - private fun checkButtonEnable() { - binding.btnPersonalDone.isEnabled = signUpViewModel.validateAllValues() - } - private fun makeProblemToastMessage( - email: Boolean, - nickName: Boolean, - passWord: Boolean, - passWordCheck: Boolean - ) { - if (!email) { - Toast.makeText(requireContext(), "이메일을 바르게 적었는지 확인해주세요", Toast.LENGTH_SHORT).show() - } else if (!nickName) { - Toast.makeText(requireContext(), "닉네임을 바르게 적었는지 확인해주세요", Toast.LENGTH_SHORT).show() - } else if (!passWord) { - Toast.makeText(requireContext(), "비밀번호를 바르게 적었는지 확인해주세요", Toast.LENGTH_SHORT).show() - } else if (!passWordCheck) { - Toast.makeText(requireContext(), "비밀번호 중복 검사를 해주세요", Toast.LENGTH_SHORT).show() + signUpViewModel.isDoneButtonEnabled.observe(viewLifecycleOwner) { + binding.btnPersonalDone.isEnabled = it } } - - private fun String.isLettersOrDigits(): Boolean { - return this.filter { it in 'A'..'Z' || it in 'a'..'z' || it in '0'..'9' } - .length == this.length - } - - private fun String.isAlphabets(): Boolean { - return this.any { it in 'A'..'Z' || it in 'a'..'z' } - } - - private fun String.isNumbers(): Boolean { - return this.any { it in '0'..'9' } - } - - companion object { - private val REGEX_EMAIL = Regex(pattern = "[a-zA-Z0-9._-]+@[a-z]+\\.+[a-z]+") - } } \ No newline at end of file diff --git a/app/src/main/java/com/teambeme/beme/signup/view/TermFragment.kt b/app/src/main/java/com/teambeme/beme/signup/view/TermFragment.kt index 871524f..4467966 100644 --- a/app/src/main/java/com/teambeme/beme/signup/view/TermFragment.kt +++ b/app/src/main/java/com/teambeme/beme/signup/view/TermFragment.kt @@ -5,8 +5,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController @@ -15,14 +13,14 @@ import com.google.firebase.analytics.ktx.analytics import com.google.firebase.analytics.ktx.logEvent import com.google.firebase.ktx.Firebase import com.teambeme.beme.R +import com.teambeme.beme.base.BindingFragment import com.teambeme.beme.databinding.FragmentTermBinding import com.teambeme.beme.signup.viewmodel.SignUpViewModel import com.teambeme.beme.util.recordClickEvent import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class TermFragment : Fragment() { - private lateinit var binding: FragmentTermBinding +class TermFragment : BindingFragment(R.layout.fragment_term) { private val signUpViewModel: SignUpViewModel by activityViewModels() override fun onAttach(context: Context) { super.onAttach(context) @@ -37,7 +35,7 @@ class TermFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = DataBindingUtil.inflate(inflater, R.layout.fragment_term, container, false) + super.onCreateView(inflater, container, savedInstanceState) binding.viewModel = signUpViewModel binding.lifecycleOwner = viewLifecycleOwner binding.btnTermBack.setOnClickListener { view -> diff --git a/app/src/main/java/com/teambeme/beme/signup/viewmodel/SignUpViewModel.kt b/app/src/main/java/com/teambeme/beme/signup/viewmodel/SignUpViewModel.kt index 0f5aa40..11ec078 100644 --- a/app/src/main/java/com/teambeme/beme/signup/viewmodel/SignUpViewModel.kt +++ b/app/src/main/java/com/teambeme/beme/signup/viewmodel/SignUpViewModel.kt @@ -1,53 +1,76 @@ package com.teambeme.beme.signup.viewmodel import android.net.Uri -import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* import com.teambeme.beme.data.repository.SignUpRepository +import com.teambeme.beme.signup.domain.entity.User import com.teambeme.beme.signup.model.ResponseNickDoubleCheck import com.teambeme.beme.signup.model.ResponseSignUp +import com.teambeme.beme.util.addSourceList import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody -import retrofit2.HttpException import javax.inject.Inject @HiltViewModel class SignUpViewModel @Inject constructor( private val signUpRepository: SignUpRepository ) : ViewModel() { - val isPersonalChecked = MutableLiveData(false) - val isServiceChecked = MutableLiveData(false) + // TermFragment + val isPersonalChecked = MutableLiveData(false) + val isServiceChecked = MutableLiveData(false) + + // PersonalInfoFragment + private lateinit var userInfo: User + private val validatedId = hashSetOf() + fun isDoubleCheckedId(nickName: String) = validatedId.contains(nickName) + fun registerId(nickName: String) { + validatedId.add(nickName) + } val userEmail = MutableLiveData("") val userNickName = MutableLiveData("") val userPassWord = MutableLiveData("") val userPassWordCheck = MutableLiveData("") - private val _isEmailValidated = MutableLiveData(false) - val isEmailValidated: LiveData - get() = _isEmailValidated - private val _isNickNameValidated = MutableLiveData(false) - val isNickNameValidated: LiveData - get() = _isNickNameValidated - private val _isNickNameDoubleChecked = MutableLiveData(false) - val isNickNameDoubleChecked: LiveData - get() = _isNickNameDoubleChecked - private val _isPassWordValidated = MutableLiveData(false) - val isPassWordValidated: LiveData - get() = _isPassWordValidated - private val _isPassWordCheckValidated = MutableLiveData(false) - val isPassWordCheckValidated: LiveData - get() = _isPassWordCheckValidated + // Email + val isEmailValid = Transformations.map(userEmail) { validEmail(it) } + + // Nickname + private val isNicknameLengthValid = + Transformations.map(userNickName) { nickNameLengthValidation(it) } + private val isNicknameRegexValid = + Transformations.map(userNickName) { regexValid(it) } + val isNicknameValid = MediatorLiveData().apply { + addSourceList(isNicknameLengthValid, isNicknameRegexValid) { nicknameValid() } + } private val _nickNameDoubleCheck = MutableLiveData() val nickDoubleCheck: LiveData get() = _nickNameDoubleCheck + private val _isNickNameDoubleChecked = MutableLiveData(false) + val isNickNameDoubleChecked: LiveData + get() = _isNickNameDoubleChecked + + // Password + private val isPasswordRegexValid = Transformations.map(userPassWord) { regexValid(it) } + private val isPasswordLengthValid = + Transformations.map(userPassWord) { passwordLengthValid(it) } + val isPasswordValid = MediatorLiveData().apply { + addSourceList(isPasswordRegexValid, isPasswordLengthValid) { passwordValid() } + } + private val _isPasswordDoubleChecked = MediatorLiveData().apply { + addSourceList(userPassWord, userPassWordCheck) { passwordDoubleCheckValid() } + } + val isPasswordDoubleChecked: LiveData + get() = _isPasswordDoubleChecked + + + val isDoneButtonEnabled = MediatorLiveData().apply { + addSourceList( + isEmailValid, isPasswordDoubleChecked, isPasswordRegexValid, isPasswordLengthValid, + isNicknameLengthValid, isNicknameRegexValid, isNickNameDoubleChecked + ) { validUserInfo() } + } private val _profileImageUri = MutableLiveData() val profileImageUri: LiveData get() = _profileImageUri @@ -55,98 +78,90 @@ class SignUpViewModel @Inject constructor( val profilePart: LiveData get() = _profilePart + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData + get() = _errorMessage + private val _signUpUserInfo = MutableLiveData() val signUpUserInfo: LiveData get() = _signUpUserInfo - fun emailValidated() { - _isEmailValidated.value = true + fun nickDoubleCheckValidated() { + _isNickNameDoubleChecked.value = true } - fun emailNotValidated() { - _isEmailValidated.value = false + fun nickDoubleCheckInvalidated() { + _isNickNameDoubleChecked.value = false } - fun nickNameValidated() { - _isNickNameValidated.value = true + fun signUp() = viewModelScope.launch { + runCatching { signUpRepository.signUp(userInfo.toRequestBody(), profilePart.value) } + .onSuccess { _signUpUserInfo.value = it } + .onFailure { it.printStackTrace() } } - fun nickNameNotValidated() { - _isNickNameValidated.value = false + + fun signUpWithoutImage() { + viewModelScope.launch { + runCatching { signUpRepository.signUp(userInfo.toRequestBody(), null) } + .onSuccess { _signUpUserInfo.value = it } + .onFailure { it.printStackTrace() } + } } - fun passWordValidated() { - _isPassWordValidated.value = true + fun nickNameDoubleCheck() { + viewModelScope.launch { + runCatching { signUpRepository.nickNameDoubleCheck(requireNotNull(userNickName.value)) } + .onSuccess { _nickNameDoubleCheck.value = it } + .onFailure { it.printStackTrace() } + } } - fun passWordNotValidated() { - _isPassWordValidated.value = false + fun setProfilePart(part: MultipartBody.Part) { + _profilePart.value = part } - fun passWordCheckValidated() { - _isPassWordCheckValidated.value = true + fun setProfileUri(uri: Uri) { + _profileImageUri.value = uri } - fun passWordCheckNotValidated() { - _isPassWordCheckValidated.value = false + private fun validEmail(email: String): Boolean { + return REGEX_EMAIL.matches(email) } - fun nickDoubleCheckValidated() { - _isNickNameDoubleChecked.value = true + private fun passwordDoubleCheckValid(): Boolean { + if (userPassWord.value.isNullOrEmpty() || userPassWordCheck.value.isNullOrEmpty()) + return false + return userPassWord.value == userPassWordCheck.value } - fun validateAllValues() = - isEmailValidated.value!! && isNickNameValidated.value!! && isPassWordValidated.value!! && isPassWordCheckValidated.value!! && isNickNameDoubleChecked.value!! + private fun nicknameValid() = isNicknameLengthValid.value ?: false && + isNicknameLengthValid.value ?: false - fun signUp() = viewModelScope.launch { - if (profilePart.value != null) { - try { - _signUpUserInfo.value = signUpRepository.signUp(getPartMap(), profilePart.value!!) - } catch (e: HttpException) { - Log.d("SignUp", e.code().toString()) - Log.d("SignUp", e.message()) - Log.d("SignUp", e.stackTraceToString()) - } - } else { - try { - _signUpUserInfo.value = signUpRepository.signUp(getPartMap(), null) - } catch (e: HttpException) { - Log.d("SignUp", e.code().toString()) - Log.d("SignUp", e.message()) - Log.d("SignUp", e.stackTraceToString()) - } - } - } + private fun passwordValid() = isPasswordRegexValid.value ?: false + && isPasswordLengthValid.value ?: false - fun signUpWithoutImage() = viewModelScope.launch { - _signUpUserInfo.value = signUpRepository.signUp(getPartMap(), null) - Log.d("SignUp", _signUpUserInfo.value.toString()) - } + private fun regexValid(letter: String) = letter + .filter { it in 'A'..'Z' || it in 'a'..'z' || it in '0'..'9' } + .length == letter.length - fun nickNameDoubleCheck() = viewModelScope.launch { - try { - _nickNameDoubleCheck.value = signUpRepository.nickNameDoubleCheck(userNickName.value!!) - } catch (e: HttpException) { - Log.d("", e.message()) - } - } + private fun passwordLengthValid(password: String) = password.length in 8..20 - fun setProfilePart(part: MultipartBody.Part) { - _profilePart.value = part - } + private fun nickNameLengthValidation(nickName: String): Boolean = nickName.length in 5..20 - fun setProfileUri(uri: Uri) { - _profileImageUri.value = uri + fun setUserInfo() { + userInfo = User( + email = requireNotNull(userEmail.value), + nickName = requireNotNull(userNickName.value), + password = requireNotNull(userPassWord.value) + ) } - private fun getPartMap(): HashMap { - val email = userEmail.value!!.toRequestBody("text/plain".toMediaTypeOrNull()) - val nickName = userNickName.value!!.toRequestBody("text/plain".toMediaTypeOrNull()) - val passWord = userPassWord.value!!.toRequestBody("text/plain".toMediaTypeOrNull()) - return hashMapOf( - "email" to email, - "nickname" to nickName, - "password" to passWord - ) + private fun validUserInfo() = + isEmailValid.value!! && isPasswordDoubleChecked.value!! && isPasswordRegexValid.value!! + && isPasswordLengthValid.value!! && isNickNameDoubleChecked.value!! + + companion object { + private val REGEX_EMAIL = Regex(pattern = "[a-zA-Z0-9._-]+@[a-z]+\\.+[a-z]+") } } \ No newline at end of file diff --git a/app/src/main/java/com/teambeme/beme/util/LiveDataUtil.kt b/app/src/main/java/com/teambeme/beme/util/LiveDataUtil.kt new file mode 100644 index 0000000..1bb1115 --- /dev/null +++ b/app/src/main/java/com/teambeme/beme/util/LiveDataUtil.kt @@ -0,0 +1,23 @@ +package com.teambeme.beme.util + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData + +fun MediatorLiveData.addSourceList( + vararg liveDataArgument: MutableLiveData<*>, + onChanged: () -> T +) { + liveDataArgument.forEach { + this.addSource(it) { value = onChanged() } + } +} + +fun MediatorLiveData.addSourceList( + vararg liveDataArgument: LiveData<*>, + onChanged: () -> T +) { + liveDataArgument.forEach { + this.addSource(it) { value = onChanged() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teambeme/beme/util/ViewUtil.kt b/app/src/main/java/com/teambeme/beme/util/ViewUtil.kt new file mode 100644 index 0000000..168c5a5 --- /dev/null +++ b/app/src/main/java/com/teambeme/beme/util/ViewUtil.kt @@ -0,0 +1,10 @@ +package com.teambeme.beme.util + +import android.view.View +import android.widget.ImageView +import androidx.annotation.ColorRes + +fun View.color(@ColorRes color: Int): Int = resources.getColor(color, null) +var ImageView.imageSrc: Int + get() = this.tag as Int + set(value) = this.setImageResource(value) \ No newline at end of file diff --git a/app/src/main/res/layout/deprecated/item_explore_other_questions_unanswered.xml b/app/src/main/res/layout/deprecated/item_explore_other_questions_unanswered.xml index fac8192..63c258f 100644 --- a/app/src/main/res/layout/deprecated/item_explore_other_questions_unanswered.xml +++ b/app/src/main/res/layout/deprecated/item_explore_other_questions_unanswered.xml @@ -1,192 +1,192 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_my_scrap.xml b/app/src/main/res/layout/fragment_my_scrap.xml index a9eebe6..c19ee8c 100644 --- a/app/src/main/res/layout/fragment_my_scrap.xml +++ b/app/src/main/res/layout/fragment_my_scrap.xml @@ -109,7 +109,7 @@ android:layout_marginEnd="28dp" android:layout_marginBottom="150dp" android:backgroundTint="@color/mypage_black" - android:onClick="@{()->myPageViewModel.getMyScrap()}" + android:onClick="@{()->myPageViewModel.getPlusMyScrap()}" android:paddingTop="12dp" android:paddingBottom="16dp" android:text="더보기" diff --git a/app/src/main/res/layout/fragment_my_write.xml b/app/src/main/res/layout/fragment_my_write.xml index 20c896d..b0dbfaa 100644 --- a/app/src/main/res/layout/fragment_my_write.xml +++ b/app/src/main/res/layout/fragment_my_write.xml @@ -106,7 +106,7 @@ android:layout_marginEnd="28dp" android:layout_marginBottom="150dp" android:backgroundTint="@color/mypage_black" - android:onClick="@{()->myPageViewModel.getMyAnswer()}" + android:onClick="@{()->myPageViewModel.getPlusMyAnswer()}" android:paddingTop="12dp" android:paddingBottom="16dp" android:text="더보기" diff --git a/app/src/main/res/layout/fragment_personal_info.xml b/app/src/main/res/layout/fragment_personal_info.xml index e2cc725..b1afbbd 100644 --- a/app/src/main/res/layout/fragment_personal_info.xml +++ b/app/src/main/res/layout/fragment_personal_info.xml @@ -112,7 +112,7 @@ style="@style/personal_info_edittext" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="닉네임 *" + android:hint="아이디 *" android:importantForAutofill="no" android:text="@={signUpViewModel.userNickName}" /> diff --git a/build.gradle b/build.gradle index fe5e691..9894a95 100644 --- a/build.gradle +++ b/build.gradle @@ -1,47 +1,30 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. -ext.ReporterType = org.jlleitschuh.gradle.ktlint.reporter.ReporterType apply plugin: "org.jlleitschuh.gradle.ktlint" -ktlint { - version = "0.41.0" - debug = true - verbose = true - android = false - outputToConsole = true - reporters = [ReporterType.PLAIN, ReporterType.CHECKSTYLE] - ignoreFailures = true - enableExperimentalRules = true - additionalEditorconfigFile = file("/some/additional/.editorconfig") - kotlinScriptAdditionalPaths { - include fileTree("scripts/") - } - filter { - exclude("**/generated/**") - include("**/kotlin/**") - } -} - buildscript { - ext.kotlin_version = "1.4.32" + ext.kotlin_version = "1.4.31" repositories { google() jcenter() maven { url "https://plugins.gradle.org/m2/" } - mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:4.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jlleitschuh.gradle:ktlint-gradle:8.2.0" + classpath "org.jlleitschuh.gradle:ktlint-gradle:10.0.0" classpath 'com.google.gms:google-services:4.3.5' - classpath 'com.google.dagger:hilt-android-gradle-plugin:2.33-beta' + classpath 'com.google.dagger:hilt-android-gradle-plugin:2.35' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } +repositories { + mavenCentral() +} + allprojects { repositories { google() @@ -50,9 +33,8 @@ allprojects { } } -subprojects { - apply plugin: "org.jlleitschuh.gradle.ktlint" -} +apply plugin: "org.jlleitschuh.gradle.ktlint" + task clean(type: Delete) { delete rootProject.buildDir diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 699b673..72234b0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip