diff --git a/.github/workflows/Android-release CD.yml b/.github/workflows/Android-release CD.yml index 087926cc..c08b01ee 100644 --- a/.github/workflows/Android-release CD.yml +++ b/.github/workflows/Android-release CD.yml @@ -55,17 +55,31 @@ jobs: echo '${{ secrets.SENTRY_PROPERTIES }}' > ./app/src/main/resources/sentry.properties # Build AAB Release - - name: Build release Bundle + - name: Build release Bundle (1.x.x) run: ./gradlew clean bundleRelease + if: startsWith(github.ref, 'refs/heads/release/1.') + + - name: Build prodRelease Bundle (2.x.x+) + run: ./gradlew clean bundleProdRelease + if: startsWith(github.ref, 'refs/heads/release/') && !startsWith(github.ref, 'refs/heads/release/1.') # Upload Google Play - - name: Deploy to Play Store ๐Ÿš€ + - name: Deploy to Play Store ๐Ÿš€ (1.x.x) + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }} + packageName: ${{ secrets.AOS_PACKAGE_NAME }} + releaseFiles: app/build/outputs/bundle/release/app-release.aab + track: production + status: draft + if: startsWith(github.ref, 'refs/heads/release/1.') + + - name: Deploy to Play Store ๐Ÿš€ (2.0.0+) uses: r0adkll/upload-google-play@v1 with: serviceAccountJsonPlainText: ${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }} - packageName: ${{secrets.AOS_PACKAGE_NAME}} - releaseFiles: ./app/build/outputs/bundle/release/app-release.aab + packageName: ${{ secrets.AOS_PACKAGE_NAME }} + releaseFiles: app/build/outputs/bundle/prodRelease/app-prod-release.aab track: production status: draft - if: github.ref != 'refs/heads/main' - + if: startsWith(github.ref, 'refs/heads/release/') && !startsWith(github.ref, 'refs/heads/release/1.') diff --git a/.gitignore b/.gitignore index 2c3dfb68..80750c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ build/ # Local configuration file (sdk path, etc) local.properties +*.properties # Proguard folder generated by Eclipse proguard/ @@ -54,7 +55,7 @@ captures/ *.keystore # Google Services (e.g. APIs or Firebase) -# google-services.json +google-services.json # Android Patch gen-external-apklibs @@ -71,30 +72,7 @@ obj/ /out/ # User-specific configurations -.idea/caches/ -.idea/libraries/ -.idea/shelf/ -.idea/workspace.xml -.idea/tasks.xml -.idea/.name -.idea/compiler.xml -.idea/copyright/profiles_settings.xml -.idea/encodings.xml -.idea/misc.xml -.idea/modules.xml -.idea/scopes/scope_settings.xml -.idea/dictionaries -.idea/vcs.xml -.idea/jsLibraryMappings.xml -.idea/datasources.xml -.idea/dataSources.ids -.idea/sqlDataSources.xml -.idea/dynamic.xml -.idea/uiDesigner.xml -.idea/assetWizardSettings.xml -.idea/gradle.xml -.idea/jarRepositories.xml -.idea/navEditor.xml +.idea/ # OS-specific files .DS_Store diff --git a/README.md b/README.md index 08bfd853..a8ff7f83 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ git clone git@github.com:Daily-DAYO/DAYO_Android.git ## Application Version - minSdkVersion : 26
-- targetSdkVersion : 33 +- targetSdkVersion : 35 ## Git Convention - Create issue
diff --git a/app/build.gradle b/app/build.gradle index 5bc4e3ae..84f401f2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,6 +11,7 @@ plugins { Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) def NATIVE_APP_KEY = properties.getProperty('NATIVE_APP_KEY') +def ADS_APPLICATION_ID = properties.getProperty('ADS_APPLICATION_ID') def keystorePropertiesFile = rootProject.file("app/keystore-release.properties") sentry { @@ -26,18 +27,22 @@ kotlin { } android { + compileSdk rootProject.ext.compileSdkVersion + defaultConfig { applicationId "com.daily.dayo" - compileSdk 34 - minSdkVersion 26 - targetSdkVersion 34 - versionCode 11500 - versionName "1.1.5" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 20000 + versionName "2.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField ("String", "NATIVE_APP_KEY", properties['NATIVE_APP_KEY_STR']) - manifestPlaceholders = [NATIVE_APP_KEY: NATIVE_APP_KEY] + manifestPlaceholders = [ + NATIVE_APP_KEY : NATIVE_APP_KEY, + ADS_APPLICATION_ID: ADS_APPLICATION_ID + ] } signingConfigs { @@ -59,6 +64,7 @@ android { buildTypes { debug { + minifyEnabled false applicationIdSuffix ".debug" versionNameSuffix "-debug" resValue "string", "app_name", "DAYO (Debug)" @@ -70,6 +76,17 @@ android { signingConfig signingConfigs.release } } + flavorDimensions = ["environment"] + productFlavors { + dev { + dimension "environment" + buildConfigField("String", "BASE_URL", properties['BASE_URL_DEV']) + } + prod { + dimension "environment" + buildConfigField("String", "BASE_URL", properties['BASE_URL_PROD']) + } + } kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } @@ -102,4 +119,7 @@ dependencies { // Firebase implementation 'com.google.firebase:firebase-crashlytics-ktx' implementation 'com.google.firebase:firebase-analytics-ktx' + + // Google Ads + implementation 'com.google.android.gms:play-services-ads:23.6.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2ca7f617..53ea5df4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,11 @@ + + + @@ -64,7 +70,7 @@ + android:windowSoftInputMode="adjustResize" /> diff --git a/app/src/main/java/com/daily/dayo/DayoApplication.kt b/app/src/main/java/com/daily/dayo/DayoApplication.kt index 4f481181..af69ceb1 100644 --- a/app/src/main/java/com/daily/dayo/DayoApplication.kt +++ b/app/src/main/java/com/daily/dayo/DayoApplication.kt @@ -2,6 +2,7 @@ package com.daily.dayo import android.app.Application import com.bumptech.glide.Glide +import com.google.android.gms.ads.MobileAds import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.ktx.analytics import com.google.firebase.ktx.Firebase @@ -9,11 +10,12 @@ import com.kakao.sdk.common.KakaoSdk import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class DayoApplication : Application(){ +class DayoApplication : Application() { private lateinit var firebaseAnalytics: FirebaseAnalytics override fun onCreate() { super.onCreate() KakaoSdk.init(this, BuildConfig.NATIVE_APP_KEY) + MobileAds.initialize(this) firebaseAnalytics = Firebase.analytics } diff --git a/build.gradle b/build.gradle index 975b4f0e..a0e6994a 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,9 @@ buildscript { compose_version = '1.4.6' nav_version = '2.6.0' paging_version = "3.2.0" + targetSdkVersion = 35 + compileSdkVersion = 35 + minSdkVersion = 26 } repositories { google() diff --git a/data/build.gradle b/data/build.gradle index a9eb90f0..e481800b 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -14,10 +14,10 @@ properties.load(project.rootProject.file('local.properties').newDataInputStream( android { namespace 'daily.dayo.data' - compileSdk 33 + compileSdk rootProject.ext.compileSdkVersion defaultConfig { - minSdk 26 + minSdk rootProject.ext.minSdkVersion testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -32,11 +32,17 @@ android { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - buildConfigField("String", "BASE_URL", properties['BASE_URL_RELEASE']) } - - debug { - buildConfigField("String", "BASE_URL", properties['BASE_URL_DEBUG']) + } + flavorDimensions = ["environment"] + productFlavors { + dev { + dimension "environment" + buildConfigField("String", "BASE_URL", properties['BASE_URL_DEV']) + } + prod { + dimension "environment" + buildConfigField("String", "BASE_URL", properties['BASE_URL_PROD']) } } compileOptions { diff --git a/data/src/main/java/daily/dayo/data/datasource/local/SharedManager.kt b/data/src/main/java/daily/dayo/data/datasource/local/SharedManager.kt index 5dde1817..cdb8959f 100644 --- a/data/src/main/java/daily/dayo/data/datasource/local/SharedManager.kt +++ b/data/src/main/java/daily/dayo/data/datasource/local/SharedManager.kt @@ -2,11 +2,14 @@ package daily.dayo.data.datasource.local import android.content.Context import android.content.SharedPreferences +import com.google.gson.Gson import daily.dayo.data.util.PreferenceHelper import daily.dayo.data.util.PreferenceHelper.get import daily.dayo.data.util.PreferenceHelper.set import com.google.gson.JsonArray import dagger.hilt.android.qualifiers.ApplicationContext +import daily.dayo.domain.model.SearchHistory +import daily.dayo.domain.model.SearchHistoryDetail import daily.dayo.domain.model.User import daily.dayo.domain.model.UserTokens import org.json.JSONArray @@ -16,6 +19,7 @@ import javax.inject.Singleton @Singleton class SharedManager @Inject constructor(@ApplicationContext context: Context) { private val prefs: SharedPreferences = PreferenceHelper.defaultPrefs(context) + private val KEY_SEARCH_HISTORY = "search_history" fun saveCurrentUser(userInfo: Any?) = when (userInfo) { is UserTokens -> { @@ -58,31 +62,47 @@ class SharedManager @Inject constructor(@ApplicationContext context: Context) { prefs["notiNoticePermit"] = value } - fun setSearchKeywordRecent(searchKeywordRecent: ArrayList) { - val jsonArr = JsonArray() - for (i in searchKeywordRecent) { - jsonArr.add(i) - } + fun saveSearchHistory(searchHistory: SearchHistory) { + val gson = Gson() + val json = gson.toJson(searchHistory) + val editor = prefs.edit() + editor.putString(KEY_SEARCH_HISTORY, json) + editor.apply() + } - var result = jsonArr.toString() - prefs["recentSearchKeyword"] = result + fun getSearchKeywordRecent(): SearchHistory { + val json = prefs.getString(KEY_SEARCH_HISTORY, null) + val gson = Gson() + return gson.fromJson(json, SearchHistory::class.java) ?: SearchHistory(0, mutableListOf()) } - fun getSearchKeywordRecent(): ArrayList { - val result = prefs["recentSearchKeyword", ""] - val resultArr = ArrayList() - val jsonArr: JSONArray = if (result.isEmpty()) { - JSONArray() - } else { - JSONArray(result) + fun updateSearchHistory(newItem: SearchHistoryDetail) { + var searchHistory = getSearchKeywordRecent() + // History์— Type ๊ตฌ๋ถ„์„ ํ•˜์ง€ ์•Š์Œ์— ๋”ฐ๋ผ OR๋ฌธ์œผ๋กœ ์ฒ˜๋ฆฌ + val existingItem = searchHistory.data.find { + it.history == newItem.history || it.searchHistoryType == newItem.searchHistoryType } - if (jsonArr.length() != 0) { - for (i in 0 until jsonArr.length()) { - resultArr.add(jsonArr.optString(i)) - } + if (existingItem != null) { + searchHistory = searchHistory.copy( + count = searchHistory.count - 1, + data = searchHistory.data.filter { + it.history != existingItem.history || it.searchHistoryType != existingItem.searchHistoryType + }.toMutableList() + ) } - return resultArr + + searchHistory = searchHistory.copy( + count = searchHistory.count + 1, + data = listOf(newItem) + searchHistory.data + ) + saveSearchHistory(searchHistory) + } + + fun clearSearchHistory() { + val editor = prefs.edit() + editor.remove(KEY_SEARCH_HISTORY) + editor.apply() } fun clearPreferences() { diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/alarm/AlarmApiService.kt b/data/src/main/java/daily/dayo/data/datasource/remote/alarm/AlarmApiService.kt index 228fb10e..5cfab0ba 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/alarm/AlarmApiService.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/alarm/AlarmApiService.kt @@ -12,5 +12,5 @@ interface AlarmApiService { suspend fun requestAllAlarmList(@Query("end") end: Int): NetworkResponse @POST("/api/v1/alarms/{alarmId}") - suspend fun requestIsCheckAlarm(@Path("alarmId") alarmId: Int): NetworkResponse + suspend fun markAlarmAsChecked(@Path("alarmId") alarmId: Int): NetworkResponse } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/alarm/AlarmResponse.kt b/data/src/main/java/daily/dayo/data/datasource/remote/alarm/AlarmResponse.kt index dbf68607..d66f2a56 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/alarm/AlarmResponse.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/alarm/AlarmResponse.kt @@ -30,5 +30,7 @@ data class AlarmDto( @SerializedName("memberId") val memberId: String?, @SerializedName("postId") - val postId: Int? + val postId: Long?, + @SerializedName("profileImage") + val profileImage: String?, ) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkApiService.kt b/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkApiService.kt index 05ed70ad..f9a48729 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkApiService.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkApiService.kt @@ -9,7 +9,7 @@ interface BookmarkApiService { suspend fun requestBookmarkPost(@Body body: CreateBookmarkRequest): NetworkResponse @POST("/api/v1/bookmark/delete/{postId}") - suspend fun requestDeleteBookmarkPost(@Path("postId") postId: Int): NetworkResponse + suspend fun requestDeleteBookmarkPost(@Path("postId") postId: Long): NetworkResponse @GET("/api/v1/bookmark/list") suspend fun requestAllMyBookmarkPostList(@Query("end") end: Int): NetworkResponse diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkRequest.kt b/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkRequest.kt index 552a8b6a..54f583dc 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkRequest.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkRequest.kt @@ -4,5 +4,5 @@ import com.google.gson.annotations.SerializedName data class CreateBookmarkRequest( @SerializedName("postId") - val postId: Int + val postId: Long ) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkResponse.kt b/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkResponse.kt index 2c773cca..ba597279 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkResponse.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/bookmark/BookmarkResponse.kt @@ -6,7 +6,7 @@ data class CreateBookmarkResponse( @SerializedName("memberId") val memberId: String, @SerializedName("postId") - val postId: Int + val postId: Long ) data class ListAllMyBookmarkPostResponse( @@ -20,7 +20,7 @@ data class ListAllMyBookmarkPostResponse( data class BookmarkPostDto( @SerializedName("postId") - val postId: Int, + val postId: Long, @SerializedName("thumbnailImage") val thumbnailImage: String ) diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentApiService.kt b/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentApiService.kt index 1864c247..562756ed 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentApiService.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentApiService.kt @@ -8,12 +8,17 @@ import retrofit2.http.Path interface CommentApiService { - @POST("/api/v1/comments") + /*** v2 ***/ + @GET("/api/v2/comments/{postId}") + suspend fun requestPostComment(@Path("postId") postId: Long): NetworkResponse + + @POST("/api/v2/comments") suspend fun requestCreatePostComment(@Body body: CreateCommentRequest): NetworkResponse - @GET("/api/v1/comments/{postId}") - suspend fun requestPostComment(@Path("postId") postId: Int): NetworkResponse + @POST("/api/v2/comments/reply") + suspend fun requestCreatePostCommentReply(@Body body: CreateCommentReplyRequest): NetworkResponse + /*** v1 ***/ @POST("/api/v1/comments/delete/{commentId}") - suspend fun requestDeletePostComment(@Path("commentId") commentId: Int): NetworkResponse + suspend fun requestDeletePostComment(@Path("commentId") commentId: Long): NetworkResponse } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentRequest.kt b/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentRequest.kt index c2ba1b72..2ada924a 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentRequest.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentRequest.kt @@ -6,5 +6,18 @@ data class CreateCommentRequest( @SerializedName("contents") val contents: String, @SerializedName("postId") - val postId: Int + val postId: Long, + @SerializedName("mentionList") + val mentionList: List +) + +data class CreateCommentReplyRequest( + @SerializedName("commentId") + val commentId: Long, + @SerializedName("contents") + val contents: String, + @SerializedName("postId") + val postId: Long, + @SerializedName("mentionList") + val mentionList: List ) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentResponse.kt b/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentResponse.kt index 89d51e8d..9684a640 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentResponse.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/comment/CommentResponse.kt @@ -16,15 +16,26 @@ data class ListAllCommentResponse( data class CommentDto( @SerializedName("commentId") - val commentId: Int, + val commentId: Long, + @SerializedName("memberId") + val memberId: String, + @SerializedName("nickname") + val nickname: String, + @SerializedName("profileImg") + val profileImg: String, @SerializedName("contents") val contents: String, @SerializedName("createTime") val createTime: String, + @SerializedName("replyList") + val replyList: List, + @SerializedName("mentionList") + val mentionList: List +) + +data class MentionUserDto( @SerializedName("memberId") val memberId: String, @SerializedName("nickname") - val nickname: String, - @SerializedName("profileImg") - val profileImg: String + val nickname: String ) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderApiService.kt b/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderApiService.kt index 31e24a9f..6d2f5b4c 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderApiService.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderApiService.kt @@ -18,7 +18,7 @@ interface FolderApiService { @Multipart @POST("/api/v1/folders/patch") suspend fun requestEditFolder( - @Part("folderId") folderId: Int, + @Part("folderId") folderId: Long, @Part("name") name: String, @Part("privacy") privacy: String, @Part("subheading") subheading: String?, @@ -30,23 +30,27 @@ interface FolderApiService { suspend fun requestCreateFolderInPost(@Body body: CreateFolderInPostRequest): NetworkResponse @POST("/api/v1/folders/delete/{folderId}") - suspend fun requestDeleteFolder(@Path("folderId") folderId: Int): NetworkResponse + suspend fun requestDeleteFolder(@Path("folderId") folderId: Long): NetworkResponse - @POST("/api/v1/folders/order") - suspend fun requestOrderFolder(@Body body: List): NetworkResponse + @POST("/api/v2/folders/move") + suspend fun requestFolderMove(@Body body: FolderMoveRequest): NetworkResponse // ํด๋” ๋ฆฌ์ŠคํŠธ - @GET("/api/v1/folders/list/{memberId}") + @GET("/api/v2/folders/list/{memberId}") suspend fun requestAllFolderList(@Path("memberId") memberId: String): NetworkResponse - @GET("/api/v1/folders/my") + @GET("/api/v2/folders/my") suspend fun requestAllMyFolderList(): NetworkResponse // ํด๋” ์ •๋ณด - @GET("/api/v1/folders/{folderId}/info") - suspend fun requestFolderInfo(@Path("folderId") folderId: Int): NetworkResponse + @GET("/api/v2/folders/{folderId}/info") + suspend fun requestFolderInfo(@Path("folderId") folderId: Long): NetworkResponse // ํด๋” ๋‚ด ๊ฒŒ์‹œ๊ธ€ - @GET("/api/v1/folders/{folderId}") - suspend fun requestDetailListFolder(@Path("folderId") folderId: Int, @Query("end") end: Int): NetworkResponse + @GET("/api/v2/folders/{folderId}") + suspend fun requestDetailListFolder( + @Path("folderId") folderId: Long, + @Query("end") end: Int, + @Query("order") order: String + ): NetworkResponse } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderPagingSource.kt b/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderPagingSource.kt index 0670582a..ef192587 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderPagingSource.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderPagingSource.kt @@ -3,20 +3,22 @@ package daily.dayo.data.datasource.remote.folder import androidx.paging.PagingSource import androidx.paging.PagingState import daily.dayo.data.mapper.toFolderPost +import daily.dayo.domain.model.FolderOrder import daily.dayo.domain.model.FolderPost import daily.dayo.domain.model.NetworkResponse class FolderPagingSource( private val apiService: FolderApiService, private val size: Int, - private val folderId: Int + private val folderId: Long, + private val folderOrder: FolderOrder ) : PagingSource() { override suspend fun load( params: LoadParams ): LoadResult { val nextPageNumber = params.key ?: 0 - apiService.requestDetailListFolder(folderId = folderId, end = nextPageNumber).let { ApiResponse -> + apiService.requestDetailListFolder(folderId = folderId, end = nextPageNumber, order = folderOrder.toString()).let { ApiResponse -> return try { when (ApiResponse) { is NetworkResponse.Success -> { diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderRequest.kt b/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderRequest.kt index 08de26e1..b1a57ac1 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderRequest.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderRequest.kt @@ -1,11 +1,20 @@ package daily.dayo.data.datasource.remote.folder -import daily.dayo.domain.model.Privacy import com.google.gson.annotations.SerializedName +import daily.dayo.domain.model.Privacy data class CreateFolderInPostRequest( @SerializedName("name") val name: String, + @SerializedName("subheading") + val subheading: String, @SerializedName("privacy") val privacy: Privacy +) + +data class FolderMoveRequest( + @SerializedName("postIdList") + val postIdList: List, + @SerializedName("targetFolderId") + val targetFolderId: Long ) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderResponse.kt b/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderResponse.kt index e32e07cb..52667052 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderResponse.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/folder/FolderResponse.kt @@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName data class CreateFolderResponse( @SerializedName("folderId") - val id: Int + val id: Long ) data class ListAllFolderResponse( @@ -48,17 +48,17 @@ data class DetailFolderResponse( data class CreateFolderInPostResponse( @SerializedName("folderId") - val folderId: Int + val folderId: Long ) data class EditFolderResponse( @SerializedName("folderId") - val folderId: Int + val folderId: Long ) data class FolderDto( @SerializedName("folderId") - val folderId: Int, + val folderId: Long, @SerializedName("name") val name: String, @SerializedName("postCount") @@ -75,14 +75,14 @@ data class FolderPostDto( @SerializedName("createDate") val createDate: String, @SerializedName("postId") - val postId: Int, + val postId: Long, @SerializedName("thumbnailImage") val thumbnailImage: String ) data class EditOrderDto( @SerializedName("folderId") - var folderId: Int, + var folderId: Long, @SerializedName("orderIndex") var orderIndex: Int ) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartApiService.kt b/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartApiService.kt index be20bcbd..e618e1a8 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartApiService.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartApiService.kt @@ -9,11 +9,11 @@ interface HeartApiService { suspend fun requestLikePost(@Body body: CreateHeartRequest): NetworkResponse @POST("/api/v1/heart/delete/{postId}") - suspend fun requestUnlikePost(@Path("postId") postId: Int): NetworkResponse + suspend fun requestUnlikePost(@Path("postId") postId: Long): NetworkResponse @GET("/api/v1/heart/list") suspend fun requestAllMyLikePostList(@Query("end") end: Int): NetworkResponse @GET("/api/v1/heart/post/{postId}/list") - suspend fun requestPostLikeUsers(@Path("postId") postId: Int, @Query("end") end: Int): NetworkResponse + suspend fun requestPostLikeUsers(@Path("postId") postId: Long, @Query("end") end: Int): NetworkResponse } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartPostUsersPagingSource.kt b/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartPostUsersPagingSource.kt index d47dfe15..fce3c3af 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartPostUsersPagingSource.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartPostUsersPagingSource.kt @@ -9,7 +9,7 @@ import daily.dayo.domain.model.NetworkResponse class HeartPostUsersPagingSource( private val apiService: HeartApiService, private val size: Int, - private val postId: Int + private val postId: Long ) : PagingSource() { override suspend fun load( diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartRequest.kt b/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartRequest.kt index d9010490..b8c43319 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartRequest.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartRequest.kt @@ -4,5 +4,5 @@ import com.google.gson.annotations.SerializedName data class CreateHeartRequest( @SerializedName("postId") - val postId: Int + val postId: Long ) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartResponse.kt b/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartResponse.kt index 8bdd7b26..b2653e15 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartResponse.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/heart/HeartResponse.kt @@ -6,7 +6,7 @@ data class CreateHeartResponse( @SerializedName("memberId") val memberId: String, @SerializedName("postId") - val postId: Int, + val postId: Long, @SerializedName("allCount") val allCount: Int ) @@ -27,7 +27,7 @@ data class ListAllMyHeartPostResponse( data class MyHeartPostDto( @SerializedName("postId") - val postId: Int, + val postId: Long, @SerializedName("thumbnailImage") val thumbnailImage: String ) diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/member/MemberApiService.kt b/data/src/main/java/daily/dayo/data/datasource/remote/member/MemberApiService.kt index bc82488d..745ee12a 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/member/MemberApiService.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/member/MemberApiService.kt @@ -24,10 +24,10 @@ interface MemberApiService { ): NetworkResponse @POST("/api/v1/members/kakaoOAuth") - suspend fun requestLoginKakao(@Body body: MemberOAuthRequest): NetworkResponse + suspend fun requestSignInKakao(@Body body: MemberOAuthRequest): NetworkResponse @POST("/api/v1/members/signIn") - suspend fun requestLoginEmail(@Body body: MemberSignInRequest): NetworkResponse + suspend fun requestSignInEmail(@Body body: MemberSignInRequest): NetworkResponse @GET("/api/v1/members/myInfo") suspend fun requestMemberInfo(): NetworkResponse @@ -56,6 +56,18 @@ interface MemberApiService { @POST("/api/v1/members/resign") suspend fun requestResign(@Query("content") content: String): NetworkResponse + @GET("/api/v2/members/delete-member/record/images/{guideFileName}") + suspend fun requestResignGuideRecordImage(@Path("guideFileName") guideFileName: String): NetworkResponse + + @GET("/api/v2/members/delete-member/record/words") + suspend fun requestResignGuideRecordWords(): NetworkResponse> + + @GET("/api/v2/members/delete-member/follow/images/{guideFileName}") + suspend fun requestResignGuideFollowImage(@Path("guideFileName") guideFileName: String): NetworkResponse + + @GET("/api/v2/members/delete-member/follow/words") + suspend fun requestResignGuideFollowWords(): NetworkResponse> + @GET("/api/v1/members/receiveAlarm") suspend fun requestReceiveAlarm(): NetworkResponse @@ -63,11 +75,14 @@ interface MemberApiService { suspend fun requestChangeReceiveAlarm(@Body body: ChangeReceiveAlarmRequest): NetworkResponse @POST("/api/v1/members/logout") - suspend fun requestLogout(): NetworkResponse + suspend fun requestSignOut(): NetworkResponse @GET("/api/v1/members/search/{email}") suspend fun requestCheckEmail(@Path("email") email: String): NetworkResponse + @GET("/api/v2/members/find-oauth-email/{email}") + suspend fun requestCheckOAuthEmail(@Path("email") email: String): NetworkResponse + @GET("/api/v1/members/search/code/{email}") suspend fun requestCheckEmailAuth(@Path("email") email: String): NetworkResponse diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/notice/NoticeApiService.kt b/data/src/main/java/daily/dayo/data/datasource/remote/notice/NoticeApiService.kt index 69fe5743..eddc631e 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/notice/NoticeApiService.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/notice/NoticeApiService.kt @@ -11,5 +11,5 @@ interface NoticeApiService { suspend fun requestAllNoticeList(@Query("end") end: Int): NetworkResponse @GET("/api/v1/notice/{noticeId}") - suspend fun requestDetailNotice(@Path("noticeId") noticeId: Int): NetworkResponse + suspend fun requestDetailNotice(@Path("noticeId") noticeId: Long): NetworkResponse } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/notice/NoticeResponse.kt b/data/src/main/java/daily/dayo/data/datasource/remote/notice/NoticeResponse.kt index 72173ea5..3c52dfe4 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/notice/NoticeResponse.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/notice/NoticeResponse.kt @@ -18,7 +18,7 @@ data class NoticeDetailResponse( data class NoticeDto( @SerializedName("id") - val id: Int, + val id: Long, @SerializedName("title") val title: String, @SerializedName("createdDate") diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/post/FeedPagingSource.kt b/data/src/main/java/daily/dayo/data/datasource/remote/post/FeedPagingSource.kt index fa7520e4..163663d7 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/post/FeedPagingSource.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/post/FeedPagingSource.kt @@ -3,10 +3,12 @@ package daily.dayo.data.datasource.remote.post import androidx.paging.PagingSource import androidx.paging.PagingState import daily.dayo.data.mapper.toPost +import daily.dayo.domain.model.Category import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.model.Post class FeedPagingSource( + private val category: Category, private val apiService: PostApiService, private val size: Int ) : PagingSource() { @@ -15,16 +17,21 @@ class FeedPagingSource( params: LoadParams ): LoadResult { val nextPageNumber = params.key ?: 0 - apiService.requestFeedList(end = nextPageNumber).let { ApiResponse -> + if (category == Category.ALL) { + apiService.requestAllFeedList(end = nextPageNumber) + } else { + apiService.requestFeedListByCategory(category = category, end = nextPageNumber) + }.let { response -> return try { - when (ApiResponse) { + when (response) { is NetworkResponse.Success -> { return LoadResult.Page( - data = ApiResponse.body!!.data.map { it.toPost() }, + data = response.body?.data?.map { it.toPost() } ?: emptyList(), prevKey = if (nextPageNumber == 0) null else nextPageNumber - size, - nextKey = if (ApiResponse.body!!.last || ApiResponse.body!!.count == 0) null else nextPageNumber + size + nextKey = if (response.body?.last != false || response.body?.count == 0) null else nextPageNumber + size ) } + else -> { throw Exception("LoadResult Error") } diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/post/PostApiService.kt b/data/src/main/java/daily/dayo/data/datasource/remote/post/PostApiService.kt index edb7a243..fab6bd10 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/post/PostApiService.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/post/PostApiService.kt @@ -1,10 +1,15 @@ package daily.dayo.data.datasource.remote.post -import daily.dayo.data.datasource.remote.heart.DeleteHeartResponse import daily.dayo.domain.model.Category import daily.dayo.domain.model.NetworkResponse import okhttp3.MultipartBody -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part +import retrofit2.http.Path +import retrofit2.http.Query interface PostApiService { @@ -14,13 +19,13 @@ interface PostApiService { @Part("category") category: String, @Part("contents") contents: String, @Part files: List, - @Part("folderId") folderId: Int, + @Part("folderId") folderId: Long, @Part("tags") tags: Array ): NetworkResponse @POST("/api/v1/posts/{postId}/edit") suspend fun requestEditPost( - @Path("postId") postId: Int, + @Path("postId") postId: Long, @Body body: EditPostRequest ): NetworkResponse @@ -37,11 +42,17 @@ interface PostApiService { suspend fun requestDayoPickPostListCategory(@Path("category") category: Category): NetworkResponse @GET("/api/v1/posts/{postId}") - suspend fun requestPostDetail(@Path("postId") postId: Int): NetworkResponse + suspend fun requestPostDetail(@Path("postId") postId: Long): NetworkResponse @POST("/api/v1/posts/delete/{postId}") - suspend fun requestDeletePost(@Path("postId") postId: Int): NetworkResponse + suspend fun requestDeletePost(@Path("postId") postId: Long): NetworkResponse @GET("/api/v1/posts/feed/list") - suspend fun requestFeedList(@Query("end") end: Int): NetworkResponse + suspend fun requestAllFeedList(@Query("end") end: Int): NetworkResponse + + @GET("/api/v1/posts/feed/{category}") + suspend fun requestFeedListByCategory( + @Path("category") category: Category, + @Query("end") end: Int + ): NetworkResponse } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/post/PostRequest.kt b/data/src/main/java/daily/dayo/data/datasource/remote/post/PostRequest.kt index af4ac497..1aa9000b 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/post/PostRequest.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/post/PostRequest.kt @@ -9,7 +9,7 @@ data class EditPostRequest ( @SerializedName("contents") val contents : String, @SerializedName("folderId") - val folderId : Int, + val folderId : Long, @SerializedName("hashtags") val hashtags : List ) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/post/PostResponse.kt b/data/src/main/java/daily/dayo/data/datasource/remote/post/PostResponse.kt index ae3e2fdd..58491e14 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/post/PostResponse.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/post/PostResponse.kt @@ -1,7 +1,7 @@ package daily.dayo.data.datasource.remote.post -import daily.dayo.domain.model.Category import com.google.gson.annotations.SerializedName +import daily.dayo.domain.model.Category data class ListFeedResponse( @SerializedName("count") @@ -14,12 +14,12 @@ data class ListFeedResponse( data class EditPostResponse( @SerializedName("postId") - val postId: Int + val postId: Long ) data class CreatePostResponse( @SerializedName("id") - val id: Int + val id: Long ) data class ListAllPostResponse( @@ -53,7 +53,7 @@ data class DetailPostResponse( @SerializedName("createDateTime") val createDateTime: String, @SerializedName("folderId") - val folderId: Int, + val folderId: Long, @SerializedName("folderName") val folderName: String, @SerializedName("hashtags") @@ -80,7 +80,7 @@ data class PostDto( @SerializedName("heartCount") val heartCount: Int, @SerializedName("id") - val postId: Int, + val postId: Long, @SerializedName("memberId") val memberId: String, @SerializedName("nickname") @@ -99,7 +99,7 @@ data class DayoPick( @SerializedName("heartCount") val heartCount: Int, @SerializedName("id") - val postId: Int, + val postId: Long, @SerializedName("memberId") val memberId: String, @SerializedName("nickname") @@ -128,7 +128,7 @@ data class FeedDto( @SerializedName("heartCount") val heartCount: Int, @SerializedName("id") - val postId: Int, + val postId: Long, @SerializedName("memberId") val memberId: String, @SerializedName("nickname") diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/report/ReportApiService.kt b/data/src/main/java/daily/dayo/data/datasource/remote/report/ReportApiService.kt index 4d4e4cdc..664eaba2 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/report/ReportApiService.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/report/ReportApiService.kt @@ -11,4 +11,8 @@ interface ReportApiService { @POST("/api/v1/reports/post") suspend fun requestSavePostReport(@Body body: CreateReportPostRequest): NetworkResponse + + @POST("/api/v2/reports/comment") + suspend fun requestSaveCommentReport(@Body body: CreateReportCommentRequest): NetworkResponse + } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/report/ReportRequest.kt b/data/src/main/java/daily/dayo/data/datasource/remote/report/ReportRequest.kt index 5c12bd68..c747a6d8 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/report/ReportRequest.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/report/ReportRequest.kt @@ -13,5 +13,12 @@ data class CreateReportPostRequest( @SerializedName("comment") val comment: String, @SerializedName("postId") - val postId: Int + val postId: Long +) + +data class CreateReportCommentRequest( + @SerializedName("comment") + val comment: String, + @SerializedName("commentId") + val commentId: Long ) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchApiService.kt b/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchApiService.kt index 07f68429..0b6ca61e 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchApiService.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchApiService.kt @@ -7,5 +7,20 @@ import retrofit2.http.Query interface SearchApiService { @GET("/api/v1/search") - suspend fun requestSearchTag(@Query("tag") tag: String, @Query("end") end: Int): NetworkResponse + suspend fun requestSearchTag( + @Query("tag") tag: String, + @Query("end") end: Int + ): NetworkResponse + + @GET("/api/v1/search/member") + suspend fun requestSearchUser( + @Query("nickname") nickname: String, + @Query("end") end: Int + ): NetworkResponse + + @GET("/api/v1/search/comment/member") + suspend fun requestSearchFollowUser( + @Query("nickname") nickname: String, + @Query("end") end: Int + ): NetworkResponse } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchFollowUserPagingSource.kt b/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchFollowUserPagingSource.kt new file mode 100644 index 00000000..6f5c89c9 --- /dev/null +++ b/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchFollowUserPagingSource.kt @@ -0,0 +1,47 @@ +package daily.dayo.data.datasource.remote.search + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import daily.dayo.data.mapper.toSearchUser +import daily.dayo.domain.model.NetworkResponse +import daily.dayo.domain.model.SearchUser + +class SearchFollowUserPagingSource( + private val apiService: SearchApiService, + private val size: Int, + private val nickname: String +) : PagingSource() { + + override suspend fun load( + params: LoadParams + ): LoadResult { + val nextPageNumber = params.key ?: 0 + apiService.requestSearchFollowUser(nickname = nickname, end = nextPageNumber) + .let { ApiResponse -> + return try { + when (ApiResponse) { + is NetworkResponse.Success -> { + return LoadResult.Page( + data = ApiResponse.body!!.data.map { it.toSearchUser() }, + prevKey = if (nextPageNumber == 0) null else nextPageNumber - size, + nextKey = if (ApiResponse.body!!.last || ApiResponse.body!!.count == 0) null else nextPageNumber + size + ) + } + + else -> { + throw Exception("LoadResult Error") + } + } + } catch (e: Exception) { + LoadResult.Error(e) + } + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(size) ?: anchorPage?.nextKey?.minus(size) + } + } +} diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchResponse.kt b/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchResponse.kt index 2c4913fc..a219f5de 100644 --- a/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchResponse.kt +++ b/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchResponse.kt @@ -15,7 +15,29 @@ data class SearchResultResponse( data class SearchDto( @SerializedName("postId") - val postId: Int, + val postId: Long, @SerializedName("thumbnailImage") val thumbnailImage: String, ) + +data class SearchUserResponse( + @SerializedName("allCount") + val totalCount: Int, + @SerializedName("count") + val count: Int, + @SerializedName("last") + val last: Boolean, + @SerializedName("data") + val data: List, +) + +data class SearchUserDto( + @SerializedName("memberId") + val memberId: String, + @SerializedName("nickname") + val nickname: String, + @SerializedName("profileImg") + val profileImg: String, + @SerializedName("isFollow") + val isFollow: Boolean, +) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchUserPagingSource.kt b/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchUserPagingSource.kt new file mode 100644 index 00000000..0d273c69 --- /dev/null +++ b/data/src/main/java/daily/dayo/data/datasource/remote/search/SearchUserPagingSource.kt @@ -0,0 +1,47 @@ +package daily.dayo.data.datasource.remote.search + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import daily.dayo.data.mapper.toSearchUser +import daily.dayo.domain.model.NetworkResponse +import daily.dayo.domain.model.SearchUser + +class SearchUserPagingSource( + private val apiService: SearchApiService, + private val size: Int, + private val nickname: String +) : PagingSource() { + + override suspend fun load( + params: LoadParams + ): LoadResult { + val nextPageNumber = params.key ?: 0 + apiService.requestSearchUser(nickname = nickname, end = nextPageNumber) + .let { ApiResponse -> + return try { + when (ApiResponse) { + is NetworkResponse.Success -> { + return LoadResult.Page( + data = ApiResponse.body!!.data.map { it.toSearchUser() }, + prevKey = if (nextPageNumber == 0) null else nextPageNumber - size, + nextKey = if (ApiResponse.body!!.last || ApiResponse.body!!.count == 0) null else nextPageNumber + size + ) + } + + else -> { + throw Exception("LoadResult Error") + } + } + } catch (e: Exception) { + LoadResult.Error(e) + } + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(size) ?: anchorPage?.nextKey?.minus(size) + } + } +} diff --git a/data/src/main/java/daily/dayo/data/mapper/AlarmMapper.kt b/data/src/main/java/daily/dayo/data/mapper/AlarmMapper.kt index c812d39b..424a3541 100644 --- a/data/src/main/java/daily/dayo/data/mapper/AlarmMapper.kt +++ b/data/src/main/java/daily/dayo/data/mapper/AlarmMapper.kt @@ -13,6 +13,7 @@ fun AlarmDto.toNotification(): Notification { image = image, nickname = nickname, memberId = memberId, - postId = postId + postId = postId, + profileImage = profileImage, ) } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/mapper/CommentMapper.kt b/data/src/main/java/daily/dayo/data/mapper/CommentMapper.kt index b9857576..528ec5ca 100644 --- a/data/src/main/java/daily/dayo/data/mapper/CommentMapper.kt +++ b/data/src/main/java/daily/dayo/data/mapper/CommentMapper.kt @@ -2,16 +2,20 @@ package daily.dayo.data.mapper import daily.dayo.data.datasource.remote.comment.CommentDto import daily.dayo.data.datasource.remote.comment.ListAllCommentResponse +import daily.dayo.data.datasource.remote.comment.MentionUserDto import daily.dayo.domain.model.Comment import daily.dayo.domain.model.Comments +import daily.dayo.domain.model.MentionUser fun CommentDto.toComment(): Comment = Comment( commentId = commentId, - contents = contents, - createTime = createTime, memberId = memberId, nickname = nickname, - profileImg = profileImg + profileImg = profileImg, + contents = contents, + createTime = createTime, + replyList = replyList.map { it.toComment() }, + mentionList = mentionList.map { it.toMentionUser() }, ) fun ListAllCommentResponse.toComments(): Comments { @@ -19,4 +23,11 @@ fun ListAllCommentResponse.toComments(): Comments { count = count, data = data.map { it.toComment() } ) +} + +fun MentionUserDto.toMentionUser(): MentionUser { + return MentionUser( + memberId = memberId, + nickname = nickname + ) } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/mapper/FolderMapper.kt b/data/src/main/java/daily/dayo/data/mapper/FolderMapper.kt index 7332f71c..fc9217c9 100644 --- a/data/src/main/java/daily/dayo/data/mapper/FolderMapper.kt +++ b/data/src/main/java/daily/dayo/data/mapper/FolderMapper.kt @@ -34,7 +34,7 @@ fun FolderInfoResponse.toFolderInfo(): FolderInfo = name = name, postCount = postCount, privacy = privacy, - subheading = subheading, + subheading = subheading ?: "", thumbnailImage = thumbnailImage ) @@ -81,16 +81,4 @@ fun FolderPostDto.toFolderPost(): FolderPost = createDate = createDate, postId = postId, thumbnailImage = thumbnailImage - ) - -fun FolderOrder.toEditOrderDto(): EditOrderDto = - EditOrderDto( - folderId = folderId, - orderIndex = orderIndex - ) - -fun EditOrderDto.toFolderOrder(): FolderOrder = - FolderOrder( - folderId = folderId, - orderIndex = orderIndex ) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/mapper/FollowMapper.kt b/data/src/main/java/daily/dayo/data/mapper/FollowMapper.kt index 014d6eb7..37ebba16 100644 --- a/data/src/main/java/daily/dayo/data/mapper/FollowMapper.kt +++ b/data/src/main/java/daily/dayo/data/mapper/FollowMapper.kt @@ -5,11 +5,11 @@ import daily.dayo.data.datasource.remote.follow.CreateFollowUpResponse import daily.dayo.data.datasource.remote.follow.ListAllFollowerResponse import daily.dayo.data.datasource.remote.follow.ListAllFollowingResponse import daily.dayo.data.datasource.remote.follow.MyFollowerDto +import daily.dayo.domain.model.Follow import daily.dayo.domain.model.FollowCreateResponse import daily.dayo.domain.model.FollowUpCreateResponse -import daily.dayo.domain.model.Followers -import daily.dayo.domain.model.Followings -import daily.dayo.domain.model.MyFollower +import daily.dayo.domain.model.Follower +import daily.dayo.domain.model.Following fun CreateFollowResponse.toFollowCreateResponse(): FollowCreateResponse = FollowCreateResponse(followerId = followerId, isAccept = isAccept, memberId = memberId) @@ -17,14 +17,14 @@ fun CreateFollowResponse.toFollowCreateResponse(): FollowCreateResponse = fun CreateFollowUpResponse.toFollowUpCreateResponse(): FollowUpCreateResponse = FollowUpCreateResponse(followId = followId, isAccept = isAccept, memberId = memberId) -fun ListAllFollowingResponse.toFollowings(): Followings = - Followings(count = count, data = data.map { it.toMyFollower() }) +fun ListAllFollowingResponse.toFollowings(): Following = + Following(count = count, data = data.map { it.toMyFollower() }) -fun ListAllFollowerResponse.toFollowers(): Followers = - Followers(count = count, data = data.map { it.toMyFollower() }) +fun ListAllFollowerResponse.toFollowers(): Follower = + Follower(count = count, data = data.map { it.toMyFollower() }) -fun MyFollowerDto.toMyFollower(): MyFollower = - MyFollower( +fun MyFollowerDto.toMyFollower(): Follow = + Follow( isFollow = isFollow, memberId = memberId, nickname = nickname, diff --git a/data/src/main/java/daily/dayo/data/mapper/HeartMapper.kt b/data/src/main/java/daily/dayo/data/mapper/HeartMapper.kt index 20fe3aeb..2cab2aab 100644 --- a/data/src/main/java/daily/dayo/data/mapper/HeartMapper.kt +++ b/data/src/main/java/daily/dayo/data/mapper/HeartMapper.kt @@ -5,15 +5,14 @@ import daily.dayo.data.datasource.remote.heart.DeleteHeartResponse import daily.dayo.data.datasource.remote.heart.HeartMemberDto import daily.dayo.data.datasource.remote.heart.MyHeartPostDto import daily.dayo.domain.model.LikePost -import daily.dayo.domain.model.LikePostDeleteResponse import daily.dayo.domain.model.LikePostResponse import daily.dayo.domain.model.LikeUser fun CreateHeartResponse.toLikePostResponse(): LikePostResponse = - LikePostResponse(memberId = memberId, postId = postId, allCount = allCount) + LikePostResponse(allCount = allCount) -fun DeleteHeartResponse.toLikePostDeleteResponse(): LikePostDeleteResponse = - LikePostDeleteResponse(allCount = allCount) +fun DeleteHeartResponse.toLikePostDeleteResponse(): LikePostResponse = + LikePostResponse(allCount = allCount) fun MyHeartPostDto.toLikePost(): LikePost = LikePost( diff --git a/data/src/main/java/daily/dayo/data/mapper/SearchMapper.kt b/data/src/main/java/daily/dayo/data/mapper/SearchMapper.kt index 25556ed3..248192ea 100644 --- a/data/src/main/java/daily/dayo/data/mapper/SearchMapper.kt +++ b/data/src/main/java/daily/dayo/data/mapper/SearchMapper.kt @@ -1,10 +1,19 @@ package daily.dayo.data.mapper import daily.dayo.data.datasource.remote.search.SearchDto +import daily.dayo.data.datasource.remote.search.SearchUserDto import daily.dayo.domain.model.Search +import daily.dayo.domain.model.SearchUser fun SearchDto.toSearch() : Search = Search( postId = postId, thumbnailImage = thumbnailImage + ) +fun SearchUserDto.toSearchUser() : SearchUser = + SearchUser( + memberId = memberId, + profileImg = profileImg, + nickname = nickname, + isFollow = isFollow ) \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/repository/AlarmRepositoryImpl.kt b/data/src/main/java/daily/dayo/data/repository/AlarmRepositoryImpl.kt index df09ba31..2899904a 100644 --- a/data/src/main/java/daily/dayo/data/repository/AlarmRepositoryImpl.kt +++ b/data/src/main/java/daily/dayo/data/repository/AlarmRepositoryImpl.kt @@ -17,8 +17,8 @@ class AlarmRepositoryImpl @Inject constructor( AlarmPagingSource(alarmApiService, ALARM_PAGE_SIZE) }.flow - override suspend fun requestIsCheckAlarm(alarmId: Int): NetworkResponse = - alarmApiService.requestIsCheckAlarm(alarmId) + override suspend fun markAlarmAsChecked(alarmId: Int): NetworkResponse = + alarmApiService.markAlarmAsChecked(alarmId) companion object { private const val ALARM_PAGE_SIZE = 10 diff --git a/data/src/main/java/daily/dayo/data/repository/BookmarkRepositoryImpl.kt b/data/src/main/java/daily/dayo/data/repository/BookmarkRepositoryImpl.kt index a1e03375..dfd03f16 100644 --- a/data/src/main/java/daily/dayo/data/repository/BookmarkRepositoryImpl.kt +++ b/data/src/main/java/daily/dayo/data/repository/BookmarkRepositoryImpl.kt @@ -14,7 +14,7 @@ import javax.inject.Inject class BookmarkRepositoryImpl @Inject constructor( private val bookmarkApiService: BookmarkApiService ) : BookmarkRepository { - override suspend fun requestBookmarkPost(postId: Int): NetworkResponse { + override suspend fun requestBookmarkPost(postId: Long): NetworkResponse { return when (val response = bookmarkApiService.requestBookmarkPost(CreateBookmarkRequest(postId = postId))) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toBookmarkPostResponse) is NetworkResponse.NetworkError -> response @@ -23,7 +23,7 @@ class BookmarkRepositoryImpl @Inject constructor( } } - override suspend fun requestDeleteBookmarkPost(postId: Int): NetworkResponse = + override suspend fun requestDeleteBookmarkPost(postId: Long): NetworkResponse = bookmarkApiService.requestDeleteBookmarkPost(postId) override suspend fun requestAllMyBookmarkPostList() = @@ -31,7 +31,15 @@ class BookmarkRepositoryImpl @Inject constructor( BookmarkPagingSource(bookmarkApiService, BOOKMARK_PAGE_SIZE) }.flow + override suspend fun requestBookmarkCount(): Int { + return when (val response = bookmarkApiService.requestAllMyBookmarkPostList(INIT_PAGE)) { + is NetworkResponse.Success -> response.body?.count ?: 0 + else -> 0 + } + } + companion object { private const val BOOKMARK_PAGE_SIZE = 10 + private const val INIT_PAGE = 0 } } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/repository/CommentRepositoryImpl.kt b/data/src/main/java/daily/dayo/data/repository/CommentRepositoryImpl.kt index 433aeaab..69ffc834 100644 --- a/data/src/main/java/daily/dayo/data/repository/CommentRepositoryImpl.kt +++ b/data/src/main/java/daily/dayo/data/repository/CommentRepositoryImpl.kt @@ -1,9 +1,12 @@ package daily.dayo.data.repository import daily.dayo.data.datasource.remote.comment.CommentApiService +import daily.dayo.data.datasource.remote.comment.CreateCommentReplyRequest import daily.dayo.data.datasource.remote.comment.CreateCommentRequest +import daily.dayo.data.datasource.remote.comment.MentionUserDto import daily.dayo.data.mapper.toComments import daily.dayo.domain.model.Comments +import daily.dayo.domain.model.MentionUser import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.repository.CommentRepository import javax.inject.Inject @@ -12,7 +15,7 @@ class CommentRepositoryImpl @Inject constructor( private val commentApiService: CommentApiService ) : CommentRepository { - override suspend fun requestPostComment(postId: Int): NetworkResponse = + override suspend fun requestPostComment(postId: Long): NetworkResponse = when (val response = commentApiService.requestPostComment(postId)) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toComments()) @@ -23,11 +26,18 @@ class CommentRepositoryImpl @Inject constructor( override suspend fun requestCreatePostComment( contents: String, - postId: Int + postId: Long, + mentionList: List ): NetworkResponse = when (val response = commentApiService.requestCreatePostComment( - CreateCommentRequest(contents = contents, postId = postId) + CreateCommentRequest( + contents = contents, + postId = postId, + mentionList = mentionList.map { + MentionUserDto(memberId = it.memberId, nickname = it.nickname) + } + ) )) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.commentId) is NetworkResponse.NetworkError -> response @@ -35,6 +45,29 @@ class CommentRepositoryImpl @Inject constructor( is NetworkResponse.UnknownError -> response } - override suspend fun requestDeletePostComment(commentId: Int): NetworkResponse = + override suspend fun requestCreatePostCommentReply( + commentId: Long, + contents: String, + postId: Long, + mentionList: List + ): NetworkResponse = + when (val response = + commentApiService.requestCreatePostCommentReply( + CreateCommentReplyRequest( + commentId = commentId, + contents = contents, + postId = postId, + mentionList = mentionList.map { + MentionUserDto(memberId = it.memberId, nickname = it.nickname) + } + ) + )) { + is NetworkResponse.Success -> NetworkResponse.Success(response.body?.commentId) + is NetworkResponse.NetworkError -> response + is NetworkResponse.ApiError -> response + is NetworkResponse.UnknownError -> response + } + + override suspend fun requestDeletePostComment(commentId: Long): NetworkResponse = commentApiService.requestDeletePostComment(commentId) } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/repository/FolderRepositoryImpl.kt b/data/src/main/java/daily/dayo/data/repository/FolderRepositoryImpl.kt index 819788f4..6cdb639d 100644 --- a/data/src/main/java/daily/dayo/data/repository/FolderRepositoryImpl.kt +++ b/data/src/main/java/daily/dayo/data/repository/FolderRepositoryImpl.kt @@ -3,7 +3,6 @@ package daily.dayo.data.repository import androidx.paging.Pager import androidx.paging.PagingConfig import daily.dayo.data.datasource.remote.folder.* -import daily.dayo.data.mapper.toEditOrderDto import daily.dayo.data.mapper.toFolderCreateInPostResponse import daily.dayo.data.mapper.toFolderCreateResponse import daily.dayo.data.mapper.toFolderEditResponse @@ -42,7 +41,7 @@ class FolderRepositoryImpl @Inject constructor( } override suspend fun requestEditFolder( - folderId: Int, + folderId: Long, name: String, privacy: Privacy, subheading: String?, @@ -63,20 +62,30 @@ class FolderRepositoryImpl @Inject constructor( is NetworkResponse.UnknownError -> response } - override suspend fun requestCreateFolderInPost(name: String, privacy: Privacy): NetworkResponse = + override suspend fun requestCreateFolderInPost( + name: String, + description: String, + privacy: Privacy + ): NetworkResponse = when (val response = - folderApiService.requestCreateFolderInPost(CreateFolderInPostRequest(name = name, privacy = privacy))) { + folderApiService.requestCreateFolderInPost( + CreateFolderInPostRequest( + name = name, + subheading = description, + privacy = privacy + ) + )) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toFolderCreateInPostResponse()) is NetworkResponse.NetworkError -> response is NetworkResponse.ApiError -> response is NetworkResponse.UnknownError -> response } - override suspend fun requestDeleteFolder(folderId: Int): NetworkResponse = + override suspend fun requestDeleteFolder(folderId: Long): NetworkResponse = folderApiService.requestDeleteFolder(folderId) - override suspend fun requestOrderFolder(folderOrders: List): NetworkResponse = - folderApiService.requestOrderFolder(folderOrders.map { it.toEditOrderDto() }) + override suspend fun requestFolderMove(postIdList: List, targetFolderId: Long): NetworkResponse = + folderApiService.requestFolderMove(FolderMoveRequest(postIdList, targetFolderId)) override suspend fun requestAllFolderList(memberId: String): NetworkResponse = when (val response = folderApiService.requestAllFolderList(memberId)) { @@ -94,7 +103,7 @@ class FolderRepositoryImpl @Inject constructor( is NetworkResponse.UnknownError -> response } - override suspend fun requestFolderInfo(folderId: Int): NetworkResponse = + override suspend fun requestFolderInfo(folderId: Long): NetworkResponse = when (val response = folderApiService.requestFolderInfo(folderId)) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toFolderInfo()) is NetworkResponse.NetworkError -> response @@ -102,9 +111,9 @@ class FolderRepositoryImpl @Inject constructor( is NetworkResponse.UnknownError -> response } - override suspend fun requestDetailListFolder(folderId: Int) = + override suspend fun requestDetailListFolder(folderId: Long, folderOrder: FolderOrder) = Pager(PagingConfig(pageSize = FOLDER_POST_PAGE_SIZE)) { - FolderPagingSource(folderApiService, FOLDER_POST_PAGE_SIZE, folderId) + FolderPagingSource(folderApiService, FOLDER_POST_PAGE_SIZE, folderId, folderOrder) }.flow companion object { diff --git a/data/src/main/java/daily/dayo/data/repository/FollowRepositoryImpl.kt b/data/src/main/java/daily/dayo/data/repository/FollowRepositoryImpl.kt index e8c12fb5..15fd5abe 100644 --- a/data/src/main/java/daily/dayo/data/repository/FollowRepositoryImpl.kt +++ b/data/src/main/java/daily/dayo/data/repository/FollowRepositoryImpl.kt @@ -1,14 +1,16 @@ package daily.dayo.data.repository -import daily.dayo.data.datasource.remote.follow.* +import daily.dayo.data.datasource.remote.follow.CreateFollowRequest +import daily.dayo.data.datasource.remote.follow.CreateFollowUpRequest +import daily.dayo.data.datasource.remote.follow.FollowApiService import daily.dayo.data.mapper.toFollowCreateResponse import daily.dayo.data.mapper.toFollowUpCreateResponse import daily.dayo.data.mapper.toFollowers import daily.dayo.data.mapper.toFollowings import daily.dayo.domain.model.FollowCreateResponse import daily.dayo.domain.model.FollowUpCreateResponse -import daily.dayo.domain.model.Followers -import daily.dayo.domain.model.Followings +import daily.dayo.domain.model.Follower +import daily.dayo.domain.model.Following import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.repository.FollowRepository import javax.inject.Inject @@ -18,17 +20,17 @@ class FollowRepositoryImpl @Inject constructor( ) : FollowRepository { override suspend fun requestCreateFollow(followerId: String): NetworkResponse = - when (val response = followApiService.requestCreateFollow(CreateFollowRequest(followerId = followerId))) { - is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toFollowCreateResponse()) - is NetworkResponse.NetworkError -> response - is NetworkResponse.ApiError -> response - is NetworkResponse.UnknownError -> response - } + when (val response = followApiService.requestCreateFollow(CreateFollowRequest(followerId = followerId))) { + is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toFollowCreateResponse()) + is NetworkResponse.NetworkError -> response + is NetworkResponse.ApiError -> response + is NetworkResponse.UnknownError -> response + } override suspend fun requestDeleteFollow(followerId: String): NetworkResponse = followApiService.requestDeleteFollow(followerId) - override suspend fun requestListAllFollower(memberId: String): NetworkResponse = + override suspend fun requestListAllFollower(memberId: String): NetworkResponse = when (val response = followApiService.requestListAllFollower(memberId)) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toFollowers()) is NetworkResponse.NetworkError -> response @@ -36,7 +38,7 @@ class FollowRepositoryImpl @Inject constructor( is NetworkResponse.UnknownError -> response } - override suspend fun requestListAllFollowing(memberId: String): NetworkResponse = + override suspend fun requestListAllFollowing(memberId: String): NetworkResponse = when (val response = followApiService.requestListAllFollowing(memberId)) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toFollowings()) is NetworkResponse.NetworkError -> response diff --git a/data/src/main/java/daily/dayo/data/repository/HeartRepositoryImpl.kt b/data/src/main/java/daily/dayo/data/repository/HeartRepositoryImpl.kt index 0d677c7d..ad870fff 100644 --- a/data/src/main/java/daily/dayo/data/repository/HeartRepositoryImpl.kt +++ b/data/src/main/java/daily/dayo/data/repository/HeartRepositoryImpl.kt @@ -8,7 +8,6 @@ import daily.dayo.data.datasource.remote.heart.HeartPagingSource import daily.dayo.data.datasource.remote.heart.HeartPostUsersPagingSource import daily.dayo.data.mapper.toLikePostDeleteResponse import daily.dayo.data.mapper.toLikePostResponse -import daily.dayo.domain.model.LikePostDeleteResponse import daily.dayo.domain.model.LikePostResponse import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.repository.HeartRepository @@ -18,7 +17,7 @@ class HeartRepositoryImpl @Inject constructor( private val heartApiService: HeartApiService ) : HeartRepository { - override suspend fun requestLikePost(postId: Int): NetworkResponse = + override suspend fun requestLikePost(postId: Long): NetworkResponse = when (val response = heartApiService.requestLikePost(CreateHeartRequest(postId))) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toLikePostResponse()) is NetworkResponse.NetworkError -> response @@ -26,7 +25,7 @@ class HeartRepositoryImpl @Inject constructor( is NetworkResponse.UnknownError -> response } - override suspend fun requestUnlikePost(postId: Int): NetworkResponse = + override suspend fun requestUnlikePost(postId: Long): NetworkResponse = when (val response = heartApiService.requestUnlikePost(postId)) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toLikePostDeleteResponse()) is NetworkResponse.NetworkError -> response @@ -38,7 +37,7 @@ class HeartRepositoryImpl @Inject constructor( HeartPagingSource(heartApiService, HEART_PAGE_SIZE) }.flow - override suspend fun requestPostLikeUsers(postId: Int) = Pager(PagingConfig(pageSize = HEART_PAGE_SIZE)) { + override suspend fun requestPostLikeUsers(postId: Long) = Pager(PagingConfig(pageSize = HEART_PAGE_SIZE)) { HeartPostUsersPagingSource(heartApiService, HEART_PAGE_SIZE, postId) }.flow diff --git a/data/src/main/java/daily/dayo/data/repository/MemberRepositoryImpl.kt b/data/src/main/java/daily/dayo/data/repository/MemberRepositoryImpl.kt index e5b68023..96103196 100644 --- a/data/src/main/java/daily/dayo/data/repository/MemberRepositoryImpl.kt +++ b/data/src/main/java/daily/dayo/data/repository/MemberRepositoryImpl.kt @@ -45,19 +45,19 @@ class MemberRepositoryImpl @Inject constructor( ): NetworkResponse = memberApiService.requestUpdateMyProfile(nickname, profileImg, onBasicProfileImg) - override suspend fun requestLoginKakao(accessToken: String): NetworkResponse = - when (val response = memberApiService.requestLoginKakao(MemberOAuthRequest(accessToken))) { + override suspend fun requestSignInKakao(accessToken: String): NetworkResponse = + when (val response = memberApiService.requestSignInKakao(MemberOAuthRequest(accessToken))) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toUserTokenResponse()) is NetworkResponse.NetworkError -> response is NetworkResponse.ApiError -> response is NetworkResponse.UnknownError -> response } - override suspend fun requestLoginEmail( + override suspend fun requestSignInEmail( email: String, password: String ): NetworkResponse = - when (val response = memberApiService.requestLoginEmail( + when (val response = memberApiService.requestSignInEmail( MemberSignInRequest( email = email, password = password @@ -138,6 +138,38 @@ class MemberRepositoryImpl @Inject constructor( override suspend fun requestResign(content: String): NetworkResponse = memberApiService.requestResign(content) + override suspend fun requestResignGuideRecordImage(guideFileName: String): NetworkResponse = + when (val response = memberApiService.requestResignGuideRecordImage(guideFileName)) { + is NetworkResponse.Success -> NetworkResponse.Success(response.body?.bytes()) + is NetworkResponse.NetworkError -> response + is NetworkResponse.ApiError -> response + is NetworkResponse.UnknownError -> response + } + + override suspend fun requestResignGuideRecordWords(): NetworkResponse> = + when (val response = memberApiService.requestResignGuideRecordWords()) { + is NetworkResponse.Success -> NetworkResponse.Success(response.body ?: emptyList()) + is NetworkResponse.NetworkError -> response + is NetworkResponse.ApiError -> response + is NetworkResponse.UnknownError -> response + } + + override suspend fun requestResignGuideFollowImage(guideFileName: String): NetworkResponse = + when (val response = memberApiService.requestResignGuideFollowImage(guideFileName)) { + is NetworkResponse.Success -> NetworkResponse.Success(response.body?.bytes()) + is NetworkResponse.NetworkError -> response + is NetworkResponse.ApiError -> response + is NetworkResponse.UnknownError -> response + } + + override suspend fun requestResignGuideFollowWords(): NetworkResponse> = + when (val response = memberApiService.requestResignGuideFollowWords()) { + is NetworkResponse.Success -> NetworkResponse.Success(response.body ?: emptyList()) + is NetworkResponse.NetworkError -> response + is NetworkResponse.ApiError -> response + is NetworkResponse.UnknownError -> response + } + override suspend fun requestReceiveAlarm(): NetworkResponse = when (val response = memberApiService.requestReceiveAlarm()) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.onReceiveAlarm) @@ -149,13 +181,16 @@ class MemberRepositoryImpl @Inject constructor( override suspend fun requestChangeReceiveAlarm(onReceiveAlarm: Boolean): NetworkResponse = memberApiService.requestChangeReceiveAlarm(ChangeReceiveAlarmRequest(onReceiveAlarm)) - override suspend fun requestLogout(): NetworkResponse = - memberApiService.requestLogout() + override suspend fun requestSignOut(): NetworkResponse = + memberApiService.requestSignOut() override suspend fun requestCheckEmail(email: String): NetworkResponse = memberApiService.requestCheckEmail(email) - override suspend fun requestCheckEmailAuth(email: String): NetworkResponse = + override suspend fun requestCheckOAuthEmail(email: String): NetworkResponse = + memberApiService.requestCheckOAuthEmail(email) + + override suspend fun requestCertificateEmailPasswordReset(email: String): NetworkResponse = when (val response = memberApiService.requestCheckEmailAuth(email)) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.authCode) is NetworkResponse.NetworkError -> response diff --git a/data/src/main/java/daily/dayo/data/repository/NoticeRepositoryImpl.kt b/data/src/main/java/daily/dayo/data/repository/NoticeRepositoryImpl.kt index 8832020a..8ed8b0bd 100644 --- a/data/src/main/java/daily/dayo/data/repository/NoticeRepositoryImpl.kt +++ b/data/src/main/java/daily/dayo/data/repository/NoticeRepositoryImpl.kt @@ -19,7 +19,7 @@ class NoticeRepositoryImpl @Inject constructor( NoticePagingSource(noticeApiService, NOTICE_PAGE_SIZE) }.flow - override suspend fun requestDetailNotice(noticeId: Int): NetworkResponse = + override suspend fun requestDetailNotice(noticeId: Long): NetworkResponse = when ( val response = noticeApiService.requestDetailNotice(noticeId)) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toNoticeDetail()) diff --git a/data/src/main/java/daily/dayo/data/repository/PostRepositoryImpl.kt b/data/src/main/java/daily/dayo/data/repository/PostRepositoryImpl.kt index 44c69fbc..bbef09c6 100644 --- a/data/src/main/java/daily/dayo/data/repository/PostRepositoryImpl.kt +++ b/data/src/main/java/daily/dayo/data/repository/PostRepositoryImpl.kt @@ -2,8 +2,9 @@ package daily.dayo.data.repository import androidx.paging.Pager import androidx.paging.PagingConfig -import daily.dayo.data.datasource.remote.post.* -import daily.dayo.data.mapper.toLikePostDeleteResponse +import daily.dayo.data.datasource.remote.post.EditPostRequest +import daily.dayo.data.datasource.remote.post.FeedPagingSource +import daily.dayo.data.datasource.remote.post.PostApiService import daily.dayo.data.mapper.toPostCreateResponse import daily.dayo.data.mapper.toPostDetail import daily.dayo.data.mapper.toPostEditResponse @@ -11,7 +12,6 @@ import daily.dayo.data.mapper.toPosts import daily.dayo.data.mapper.toPostsCategorized import daily.dayo.data.mapper.toPostsDayoPick import daily.dayo.domain.model.Category -import daily.dayo.domain.model.LikePostDeleteResponse import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.model.PostCreateResponse import daily.dayo.domain.model.PostDetail @@ -31,7 +31,7 @@ class PostRepositoryImpl @Inject constructor( category: Category, contents: String, files: List, - folderId: Int, + folderId: Long, tags: Array ): NetworkResponse = when (val response = @@ -43,10 +43,10 @@ class PostRepositoryImpl @Inject constructor( } override suspend fun requestEditPost( - postId: Int, + postId: Long, category: Category, contents: String, - folderId: Int, + folderId: Long, hashtags: List ): NetworkResponse = when ( @@ -93,7 +93,7 @@ class PostRepositoryImpl @Inject constructor( is NetworkResponse.UnknownError -> response } - override suspend fun requestPostDetail(postId: Int): NetworkResponse = + override suspend fun requestPostDetail(postId: Long): NetworkResponse = when (val response = postApiService.requestPostDetail(postId)) { is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toPostDetail()) is NetworkResponse.NetworkError -> response @@ -101,16 +101,16 @@ class PostRepositoryImpl @Inject constructor( is NetworkResponse.UnknownError -> response } - override suspend fun requestDeletePost(postId: Int): NetworkResponse = + override suspend fun requestDeletePost(postId: Long): NetworkResponse = when (val response = postApiService.requestDeletePost(postId)) { - is NetworkResponse.Success -> NetworkResponse.Success(response.body?.toLikePostDeleteResponse()) + is NetworkResponse.Success -> NetworkResponse.Success(response.body) is NetworkResponse.NetworkError -> response is NetworkResponse.ApiError -> response is NetworkResponse.UnknownError -> response } - override suspend fun requestFeedList() = Pager(PagingConfig(pageSize = FEED_PAGE_SIZE)) { - FeedPagingSource(postApiService, FEED_PAGE_SIZE) + override suspend fun requestFeedList(category: Category) = Pager(PagingConfig(pageSize = FEED_PAGE_SIZE)) { + FeedPagingSource(category, postApiService, FEED_PAGE_SIZE) }.flow companion object { diff --git a/data/src/main/java/daily/dayo/data/repository/ReportRepositoryImpl.kt b/data/src/main/java/daily/dayo/data/repository/ReportRepositoryImpl.kt index 8bfa00a7..c1b226da 100644 --- a/data/src/main/java/daily/dayo/data/repository/ReportRepositoryImpl.kt +++ b/data/src/main/java/daily/dayo/data/repository/ReportRepositoryImpl.kt @@ -1,5 +1,6 @@ package daily.dayo.data.repository +import daily.dayo.data.datasource.remote.report.CreateReportCommentRequest import daily.dayo.data.datasource.remote.report.CreateReportMemberRequest import daily.dayo.data.datasource.remote.report.CreateReportPostRequest import daily.dayo.data.datasource.remote.report.ReportApiService @@ -23,7 +24,7 @@ class ReportRepositoryImpl @Inject constructor( override suspend fun requestSavePostReport( comment: String, - postId: Int + postId: Long ): NetworkResponse = reportApiService.requestSavePostReport( CreateReportPostRequest( @@ -31,4 +32,15 @@ class ReportRepositoryImpl @Inject constructor( postId = postId ) ) + + override suspend fun requestSaveCommentReport( + comment: String, + commentId: Long + ): NetworkResponse = + reportApiService.requestSaveCommentReport( + CreateReportCommentRequest( + comment = comment, + commentId = commentId + ) + ) } \ No newline at end of file diff --git a/data/src/main/java/daily/dayo/data/repository/SearchRepositoryImpl.kt b/data/src/main/java/daily/dayo/data/repository/SearchRepositoryImpl.kt index ed06891a..ea8232b8 100644 --- a/data/src/main/java/daily/dayo/data/repository/SearchRepositoryImpl.kt +++ b/data/src/main/java/daily/dayo/data/repository/SearchRepositoryImpl.kt @@ -6,9 +6,15 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import daily.dayo.data.datasource.local.SharedManager import daily.dayo.data.datasource.remote.search.SearchApiService +import daily.dayo.data.datasource.remote.search.SearchFollowUserPagingSource +import daily.dayo.data.datasource.remote.search.SearchUserPagingSource import daily.dayo.data.datasource.remote.search.SearchPagingSource import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.model.Search +import daily.dayo.domain.model.SearchHistory +import daily.dayo.domain.model.SearchHistoryDetail +import daily.dayo.domain.model.SearchHistoryType +import daily.dayo.domain.model.SearchUser import daily.dayo.domain.repository.SearchRepository import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -18,41 +24,70 @@ class SearchRepositoryImpl @Inject constructor( private val context: Context ) : SearchRepository { - override fun requestSearchKeywordRecentList(): ArrayList = + override fun requestSearchKeywordRecentList(): SearchHistory = SharedManager(context = context).getSearchKeywordRecent() - override fun deleteSearchKeywordRecent(keyword: String) { - val initialSearchTagList = requestSearchKeywordRecentList() - initialSearchTagList.remove(keyword) - SharedManager(context = context).setSearchKeywordRecent(initialSearchTagList) + override fun deleteSearchKeywordRecent(keyword: String, deleteKeywordType: SearchHistoryType): SearchHistory { + val deletedSearchHistory = SharedManager(context = context).getSearchKeywordRecent().copy( + count = SharedManager(context = context).getSearchKeywordRecent().count - 1, + data = (SharedManager(context = context).getSearchKeywordRecent().data).filter { + !(it.history == keyword && it.searchHistoryType == deleteKeywordType) + } + ) + + SharedManager(context = context).saveSearchHistory(deletedSearchHistory) + return requestSearchKeywordRecentList() } - override fun clearSearchKeywordRecent() { - SharedManager(context = context).setSearchKeywordRecent(ArrayList()) + override fun clearSearchKeywordRecent(): SearchHistory { + SharedManager(context = context).clearSearchHistory() + return requestSearchKeywordRecentList() } - override fun requestSearchKeyword(keyword: String): Flow> { - val initialSearchTagList = requestSearchKeywordRecentList() - if (initialSearchTagList.contains(keyword)) { // ๊ฒ€์ƒ‰ํ•œ ์  ์žˆ๋Š” ๊ฒฝ์šฐ ์ตœ์‹ ํ™”๋ฅผ ์œ„ํ•˜์—ฌ ์‚ญ์ œํ•˜๊ณ  ์ถ”๊ฐ€ - initialSearchTagList.remove(keyword) - } - initialSearchTagList.add(keyword) - SharedManager(context = context).setSearchKeywordRecent(initialSearchTagList) - return requestSearchTag(tag = keyword) + override fun updateSearchKeywordRecentList(keyword: String, requestSearchType: SearchHistoryType) { + SharedManager(context = context).updateSearchHistory( + SearchHistoryDetail( + history = keyword, + searchHistoryType = requestSearchType, + searchId = 0 + )) } - override fun requestSearchTag(tag: String) = Pager(PagingConfig(pageSize = SEARCH_PAGE_SIZE)) { + override fun requestSearchUser(nickname: String): Flow> = Pager(PagingConfig(pageSize = SEARCH_PAGE_SIZE)) { + SearchUserPagingSource(searchApiService, SEARCH_PAGE_SIZE, nickname) + }.flow + + override fun requestSearchFollowUser(nickname: String): Flow> = Pager(PagingConfig(pageSize = SEARCH_PAGE_SIZE)) { + SearchFollowUserPagingSource(searchApiService, SEARCH_PAGE_SIZE, nickname) + }.flow + + override fun requestSearchTag(tag: String): Flow> = Pager(PagingConfig(pageSize = SEARCH_PAGE_SIZE)) { SearchPagingSource(searchApiService, SEARCH_PAGE_SIZE, tag) }.flow - override suspend fun requestSearchTotalCount(tag: String, end: Int) : Int = - searchApiService.requestSearchTag(tag, end).let { ApiResponse -> - when(ApiResponse) { - is NetworkResponse.Success -> { - return ApiResponse.body!!.totalCount + override suspend fun requestSearchTotalCount(tag: String, end: Int, searchHistoryType: SearchHistoryType) : Int = + when (searchHistoryType) { + SearchHistoryType.TAG -> { + searchApiService.requestSearchTag(tag, end).let { ApiResponse -> + when(ApiResponse) { + is NetworkResponse.Success -> { + return ApiResponse.body!!.totalCount + } + else -> return 0 + } + } + } + SearchHistoryType.USER -> { + searchApiService.requestSearchUser(tag, end).let { ApiResponse -> + when(ApiResponse) { + is NetworkResponse.Success -> { + return ApiResponse.body!!.totalCount + } + else -> return 0 + } } - else -> return 0 } + else -> 0 } companion object { diff --git a/domain/src/main/java/daily/dayo/domain/model/Bookmark.kt b/domain/src/main/java/daily/dayo/domain/model/Bookmark.kt index e1e6525e..cc5c8bdf 100644 --- a/domain/src/main/java/daily/dayo/domain/model/Bookmark.kt +++ b/domain/src/main/java/daily/dayo/domain/model/Bookmark.kt @@ -1,10 +1,10 @@ package daily.dayo.domain.model data class BookmarkPost( - val postId: Int, + val postId: Long, val thumbnailImage: String ) data class BookmarkPostResponse( val memberId: String, - val postId: Int + val postId: Long ) \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/model/Comment.kt b/domain/src/main/java/daily/dayo/domain/model/Comment.kt index de396f97..50d97b72 100644 --- a/domain/src/main/java/daily/dayo/domain/model/Comment.kt +++ b/domain/src/main/java/daily/dayo/domain/model/Comment.kt @@ -1,15 +1,22 @@ package daily.dayo.domain.model data class Comment( - val commentId: Int, - val contents: String, - val createTime: String, + val commentId: Long, val memberId: String, val nickname: String, - val profileImg: String + val profileImg: String, + val contents: String, + val createTime: String, + val replyList: List, + val mentionList: List, ) data class Comments( val count: Int, val data: List +) + +data class MentionUser( + val memberId: String, + val nickname: String ) \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/model/Folder.kt b/domain/src/main/java/daily/dayo/domain/model/Folder.kt index bfa67931..e98330dc 100644 --- a/domain/src/main/java/daily/dayo/domain/model/Folder.kt +++ b/domain/src/main/java/daily/dayo/domain/model/Folder.kt @@ -1,7 +1,15 @@ package daily.dayo.domain.model +enum class FolderOrder { + NEW, OLD; + + override fun toString(): String { + return name.lowercase() + } +} + data class Folder( - val folderId: Int?, + val folderId: Long?, val title: String, val memberId: String?, val privacy: Privacy, @@ -14,6 +22,7 @@ data class Folders( val count: Int, val data: List ) + data class FoldersMine( val count: Int, val data: List @@ -21,25 +30,20 @@ data class FoldersMine( data class FolderPost( val createDate: String, - val postId: Int, + val postId: Long, val thumbnailImage: String ) -data class FolderOrder( - var folderId: Int, - var orderIndex: Int -) - data class FolderCreateResponse( - val folderId: Int + val folderId: Long ) data class FolderCreateInPostResponse( - val folderId: Int + val folderId: Long ) data class FolderEditResponse( - val folderId: Int + val folderId: Long ) data class FolderInfo( @@ -47,6 +51,6 @@ data class FolderInfo( val name: String, val postCount: Int, val privacy: Privacy, - var subheading: String?, + var subheading: String, val thumbnailImage: String ) \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/model/Follow.kt b/domain/src/main/java/daily/dayo/domain/model/Follow.kt index 2766e4ce..844d81b8 100644 --- a/domain/src/main/java/daily/dayo/domain/model/Follow.kt +++ b/domain/src/main/java/daily/dayo/domain/model/Follow.kt @@ -7,19 +7,14 @@ data class Follow( val profileImg: String ) -data class Followings( +data class Following( val count: Int, - val data: List + val data: List ) -data class Followers( + +data class Follower( val count: Int, - val data: List -) -data class MyFollower( - val isFollow: Boolean, - val memberId: String, - val nickname: String, - val profileImg: String + val data: List ) data class FollowCreateResponse( diff --git a/domain/src/main/java/daily/dayo/domain/model/Like.kt b/domain/src/main/java/daily/dayo/domain/model/Like.kt index 5384c88d..767ea419 100644 --- a/domain/src/main/java/daily/dayo/domain/model/Like.kt +++ b/domain/src/main/java/daily/dayo/domain/model/Like.kt @@ -1,16 +1,10 @@ package daily.dayo.domain.model data class LikePost( - val postId: Int, + val postId: Long, val thumbnailImage: String ) data class LikePostResponse( - val memberId: String, - val postId: Int, - val allCount: Int -) - -data class LikePostDeleteResponse( val allCount: Int ) \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/model/Notice.kt b/domain/src/main/java/daily/dayo/domain/model/Notice.kt index ee1cc21c..f70d015c 100644 --- a/domain/src/main/java/daily/dayo/domain/model/Notice.kt +++ b/domain/src/main/java/daily/dayo/domain/model/Notice.kt @@ -3,7 +3,7 @@ package daily.dayo.domain.model import java.io.Serializable data class Notice( - val noticeId: Int, + val noticeId: Long, val title: String, val uploadDate: String ) : Serializable diff --git a/domain/src/main/java/daily/dayo/domain/model/Notification.kt b/domain/src/main/java/daily/dayo/domain/model/Notification.kt index 4d519043..e9922a6b 100644 --- a/domain/src/main/java/daily/dayo/domain/model/Notification.kt +++ b/domain/src/main/java/daily/dayo/domain/model/Notification.kt @@ -9,5 +9,6 @@ data class Notification( val image: String?, val nickname: String?, val memberId: String?, - val postId: Int? + val postId: Long?, + val profileImage: String?, ) \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/model/Post.kt b/domain/src/main/java/daily/dayo/domain/model/Post.kt index fec1d746..73f7a0a5 100644 --- a/domain/src/main/java/daily/dayo/domain/model/Post.kt +++ b/domain/src/main/java/daily/dayo/domain/model/Post.kt @@ -1,7 +1,7 @@ package daily.dayo.domain.model data class Post( - val postId: Int?, + val postId: Long?, val memberId: String?, val nickname: String, val userProfileImage: String, @@ -16,7 +16,7 @@ data class Post( var bookmark: Boolean?, var heart: Boolean, var heartCount: Int, - val folderId: Int?, + val folderId: Long?, val folderName: String?, var preLoadThumbnail: ByteArray?=null, var preLoadUserImg: ByteArray?=null @@ -27,7 +27,7 @@ data class PostDetail( val category: Category, val contents: String, val createDateTime: String, - val folderId: Int, + val folderId: Long, val folderName: String, val hashtags: List, var heart: Boolean, @@ -39,11 +39,11 @@ data class PostDetail( ) data class PostCreateResponse( - val id: Int + val id: Long ) data class PostEditResponse( - val postId: Int + val postId: Long ) data class Posts( diff --git a/domain/src/main/java/daily/dayo/domain/model/Search.kt b/domain/src/main/java/daily/dayo/domain/model/Search.kt index f345922a..e0c2f799 100644 --- a/domain/src/main/java/daily/dayo/domain/model/Search.kt +++ b/domain/src/main/java/daily/dayo/domain/model/Search.kt @@ -1,6 +1,30 @@ package daily.dayo.domain.model data class Search( - val postId: Int, + val postId: Long, val thumbnailImage: String -) \ No newline at end of file +) + +data class SearchUser( + val isFollow: Boolean, + val memberId: String, + val nickname: String, + val profileImg: String, +) + +data class SearchHistory( + val count: Int, + val data: List +) + +data class SearchHistoryDetail( + val history: String, + val searchId: Int, + val searchHistoryType: SearchHistoryType +) + + +enum class SearchHistoryType { + USER, + TAG +} \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/model/Topic.kt b/domain/src/main/java/daily/dayo/domain/model/Topic.kt index b1e30fed..8477d462 100644 --- a/domain/src/main/java/daily/dayo/domain/model/Topic.kt +++ b/domain/src/main/java/daily/dayo/domain/model/Topic.kt @@ -1,5 +1,5 @@ package daily.dayo.domain.model enum class Topic { - HEART, COMMENT, NOTICE, FOLLOW + HEART, COMMENT, NOTICE, FOLLOW, MENTION } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/model/WithdrawalReason.kt b/domain/src/main/java/daily/dayo/domain/model/WithdrawalReason.kt new file mode 100644 index 00000000..4e1d6a6d --- /dev/null +++ b/domain/src/main/java/daily/dayo/domain/model/WithdrawalReason.kt @@ -0,0 +1,9 @@ +package daily.dayo.domain.model + +enum class WithdrawalReason { + INCONVENIENT_USE, + WANT_TO_DELETE_HISTORY, + RARELY_USED, + CONTENT_NOT_SATISFYING, + OTHER +} diff --git a/domain/src/main/java/daily/dayo/domain/repository/AlarmRepository.kt b/domain/src/main/java/daily/dayo/domain/repository/AlarmRepository.kt index 3ea858fc..61189278 100644 --- a/domain/src/main/java/daily/dayo/domain/repository/AlarmRepository.kt +++ b/domain/src/main/java/daily/dayo/domain/repository/AlarmRepository.kt @@ -8,5 +8,5 @@ import kotlinx.coroutines.flow.Flow interface AlarmRepository { suspend fun requestAllAlarmList(): Flow> - suspend fun requestIsCheckAlarm(alarmId: Int): NetworkResponse + suspend fun markAlarmAsChecked(alarmId: Int): NetworkResponse } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/repository/BookmarkRepository.kt b/domain/src/main/java/daily/dayo/domain/repository/BookmarkRepository.kt index 24c4fe8a..ad0fade0 100644 --- a/domain/src/main/java/daily/dayo/domain/repository/BookmarkRepository.kt +++ b/domain/src/main/java/daily/dayo/domain/repository/BookmarkRepository.kt @@ -8,7 +8,8 @@ import kotlinx.coroutines.flow.Flow interface BookmarkRepository { - suspend fun requestBookmarkPost(postId: Int): NetworkResponse - suspend fun requestDeleteBookmarkPost(postId: Int): NetworkResponse + suspend fun requestBookmarkPost(postId: Long): NetworkResponse + suspend fun requestDeleteBookmarkPost(postId: Long): NetworkResponse suspend fun requestAllMyBookmarkPostList(): Flow> + suspend fun requestBookmarkCount(): Int } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/repository/CommentRepository.kt b/domain/src/main/java/daily/dayo/domain/repository/CommentRepository.kt index e5644172..6844a2ff 100644 --- a/domain/src/main/java/daily/dayo/domain/repository/CommentRepository.kt +++ b/domain/src/main/java/daily/dayo/domain/repository/CommentRepository.kt @@ -1,11 +1,13 @@ package daily.dayo.domain.repository import daily.dayo.domain.model.Comments +import daily.dayo.domain.model.MentionUser import daily.dayo.domain.model.NetworkResponse interface CommentRepository { - suspend fun requestCreatePostComment(contents: String, postId: Int): NetworkResponse - suspend fun requestPostComment(postId: Int): NetworkResponse - suspend fun requestDeletePostComment(commentId: Int): NetworkResponse + suspend fun requestCreatePostComment(contents: String, postId: Long, mentionList: List): NetworkResponse + suspend fun requestCreatePostCommentReply(commentId: Long, contents: String, postId: Long, mentionList: List): NetworkResponse + suspend fun requestPostComment(postId: Long): NetworkResponse + suspend fun requestDeletePostComment(commentId: Long): NetworkResponse } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/repository/FolderRepository.kt b/domain/src/main/java/daily/dayo/domain/repository/FolderRepository.kt index 05f4aeca..2ee4ae09 100644 --- a/domain/src/main/java/daily/dayo/domain/repository/FolderRepository.kt +++ b/domain/src/main/java/daily/dayo/domain/repository/FolderRepository.kt @@ -24,7 +24,7 @@ interface FolderRepository { ): NetworkResponse suspend fun requestEditFolder( - folderId: Int, + folderId: Long, name: String, privacy: Privacy, subheading: String?, @@ -34,13 +34,14 @@ interface FolderRepository { suspend fun requestCreateFolderInPost( name: String, + description: String, privacy: Privacy ): NetworkResponse - suspend fun requestDeleteFolder(folderId: Int): NetworkResponse - suspend fun requestOrderFolder(folderOrders: List): NetworkResponse + suspend fun requestDeleteFolder(folderId: Long): NetworkResponse + suspend fun requestFolderMove(postIdList: List, targetFolderId: Long): NetworkResponse suspend fun requestAllFolderList(memberId: String): NetworkResponse suspend fun requestAllMyFolderList(): NetworkResponse - suspend fun requestFolderInfo(folderId: Int): NetworkResponse - suspend fun requestDetailListFolder(folderId: Int): Flow> + suspend fun requestFolderInfo(folderId: Long): NetworkResponse + suspend fun requestDetailListFolder(folderId: Long, folderOrder: FolderOrder): Flow> } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/repository/FollowRepository.kt b/domain/src/main/java/daily/dayo/domain/repository/FollowRepository.kt index 95eb28c2..f0cdbb53 100644 --- a/domain/src/main/java/daily/dayo/domain/repository/FollowRepository.kt +++ b/domain/src/main/java/daily/dayo/domain/repository/FollowRepository.kt @@ -2,15 +2,15 @@ package daily.dayo.domain.repository import daily.dayo.domain.model.FollowCreateResponse import daily.dayo.domain.model.FollowUpCreateResponse -import daily.dayo.domain.model.Followers -import daily.dayo.domain.model.Followings +import daily.dayo.domain.model.Follower +import daily.dayo.domain.model.Following import daily.dayo.domain.model.NetworkResponse interface FollowRepository { suspend fun requestCreateFollow(followerId: String): NetworkResponse suspend fun requestDeleteFollow(followerId: String): NetworkResponse - suspend fun requestListAllFollower(memberId: String): NetworkResponse - suspend fun requestListAllFollowing(memberId: String): NetworkResponse + suspend fun requestListAllFollower(memberId: String): NetworkResponse + suspend fun requestListAllFollowing(memberId: String): NetworkResponse suspend fun requestCreateFollowUp(followerId: String): NetworkResponse } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/repository/HeartRepository.kt b/domain/src/main/java/daily/dayo/domain/repository/HeartRepository.kt index 15b965dc..21530a56 100644 --- a/domain/src/main/java/daily/dayo/domain/repository/HeartRepository.kt +++ b/domain/src/main/java/daily/dayo/domain/repository/HeartRepository.kt @@ -2,7 +2,6 @@ package daily.dayo.domain.repository import androidx.paging.PagingData import daily.dayo.domain.model.LikePost -import daily.dayo.domain.model.LikePostDeleteResponse import daily.dayo.domain.model.LikePostResponse import daily.dayo.domain.model.LikeUser import daily.dayo.domain.model.NetworkResponse @@ -10,8 +9,8 @@ import kotlinx.coroutines.flow.Flow interface HeartRepository { - suspend fun requestLikePost(postId: Int): NetworkResponse - suspend fun requestUnlikePost(postId: Int): NetworkResponse + suspend fun requestLikePost(postId: Long): NetworkResponse + suspend fun requestUnlikePost(postId: Long): NetworkResponse suspend fun requestAllMyLikePostList(): Flow> - suspend fun requestPostLikeUsers(postId: Int): Flow> + suspend fun requestPostLikeUsers(postId: Long): Flow> } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/repository/MemberRepository.kt b/domain/src/main/java/daily/dayo/domain/repository/MemberRepository.kt index 5687cd20..afc83cb5 100644 --- a/domain/src/main/java/daily/dayo/domain/repository/MemberRepository.kt +++ b/domain/src/main/java/daily/dayo/domain/repository/MemberRepository.kt @@ -22,8 +22,8 @@ interface MemberRepository { onBasicProfileImg: Boolean ): NetworkResponse - suspend fun requestLoginKakao(accessToken: String): NetworkResponse - suspend fun requestLoginEmail(email: String, password: String): NetworkResponse + suspend fun requestSignInKakao(accessToken: String): NetworkResponse + suspend fun requestSignInEmail(email: String, password: String): NetworkResponse suspend fun requestMemberInfo(): NetworkResponse suspend fun requestCheckEmailDuplicate(email: String): NetworkResponse suspend fun requestCheckNicknameDuplicate(nickname: String): NetworkResponse @@ -33,11 +33,16 @@ interface MemberRepository { suspend fun requestMyProfile(): NetworkResponse suspend fun requestOtherProfile(memberId: String): NetworkResponse suspend fun requestResign(content: String): NetworkResponse + suspend fun requestResignGuideRecordImage(guideFileName: String): NetworkResponse + suspend fun requestResignGuideRecordWords(): NetworkResponse> + suspend fun requestResignGuideFollowImage(guideFileName: String): NetworkResponse + suspend fun requestResignGuideFollowWords(): NetworkResponse> suspend fun requestReceiveAlarm(): NetworkResponse suspend fun requestChangeReceiveAlarm(onReceiveAlarm: Boolean): NetworkResponse - suspend fun requestLogout(): NetworkResponse + suspend fun requestSignOut(): NetworkResponse suspend fun requestCheckEmail(email: String): NetworkResponse - suspend fun requestCheckEmailAuth(email: String): NetworkResponse + suspend fun requestCheckOAuthEmail(email: String): NetworkResponse + suspend fun requestCertificateEmailPasswordReset(email: String): NetworkResponse suspend fun requestCheckCurrentPassword(password: String): NetworkResponse suspend fun requestChangePassword(email: String, password: String): NetworkResponse suspend fun requestSettingChangePassword(email: String, password: String): NetworkResponse diff --git a/domain/src/main/java/daily/dayo/domain/repository/NoticeRepository.kt b/domain/src/main/java/daily/dayo/domain/repository/NoticeRepository.kt index 04f85a62..537abffb 100644 --- a/domain/src/main/java/daily/dayo/domain/repository/NoticeRepository.kt +++ b/domain/src/main/java/daily/dayo/domain/repository/NoticeRepository.kt @@ -8,5 +8,5 @@ import kotlinx.coroutines.flow.Flow interface NoticeRepository { suspend fun requestAllNoticeList(): Flow> - suspend fun requestDetailNotice(noticeId: Int): NetworkResponse + suspend fun requestDetailNotice(noticeId: Long): NetworkResponse } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/repository/PostRepository.kt b/domain/src/main/java/daily/dayo/domain/repository/PostRepository.kt index 1e59edf6..727e5e95 100644 --- a/domain/src/main/java/daily/dayo/domain/repository/PostRepository.kt +++ b/domain/src/main/java/daily/dayo/domain/repository/PostRepository.kt @@ -2,7 +2,6 @@ package daily.dayo.domain.repository import androidx.paging.PagingData import daily.dayo.domain.model.Category -import daily.dayo.domain.model.LikePostDeleteResponse import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.model.Post import daily.dayo.domain.model.PostCreateResponse @@ -20,7 +19,7 @@ interface PostRepository { category: Category, contents: String, files: List, - folderId: Int, + folderId: Long, tags: Array ): NetworkResponse @@ -28,15 +27,15 @@ interface PostRepository { suspend fun requestNewPostListCategory(category: Category): NetworkResponse suspend fun requestDayoPickPostList(): NetworkResponse suspend fun requestDayoPickPostListCategory(category: Category): NetworkResponse - suspend fun requestPostDetail(postId: Int): NetworkResponse - suspend fun requestDeletePost(postId: Int): NetworkResponse + suspend fun requestPostDetail(postId: Long): NetworkResponse + suspend fun requestDeletePost(postId: Long): NetworkResponse suspend fun requestEditPost( - postId: Int, + postId: Long, category: Category, contents: String, - folderId: Int, + folderId: Long, hashtags: List ): NetworkResponse - suspend fun requestFeedList(): Flow> + suspend fun requestFeedList(category: Category): Flow> } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/repository/ReportRepository.kt b/domain/src/main/java/daily/dayo/domain/repository/ReportRepository.kt index 6e3b5b4f..d0212638 100644 --- a/domain/src/main/java/daily/dayo/domain/repository/ReportRepository.kt +++ b/domain/src/main/java/daily/dayo/domain/repository/ReportRepository.kt @@ -5,5 +5,6 @@ import daily.dayo.domain.model.NetworkResponse interface ReportRepository { suspend fun requestSaveMemberReport(comment: String, memberId: String): NetworkResponse - suspend fun requestSavePostReport(comment: String, postId: Int): NetworkResponse + suspend fun requestSavePostReport(comment: String, postId: Long): NetworkResponse + suspend fun requestSaveCommentReport(comment: String, commentId: Long): NetworkResponse } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/repository/SearchRepository.kt b/domain/src/main/java/daily/dayo/domain/repository/SearchRepository.kt index 6a1de159..e9d37df8 100644 --- a/domain/src/main/java/daily/dayo/domain/repository/SearchRepository.kt +++ b/domain/src/main/java/daily/dayo/domain/repository/SearchRepository.kt @@ -2,13 +2,26 @@ package daily.dayo.domain.repository import androidx.paging.PagingData import daily.dayo.domain.model.Search +import daily.dayo.domain.model.SearchHistory +import daily.dayo.domain.model.SearchHistoryType +import daily.dayo.domain.model.SearchUser import kotlinx.coroutines.flow.Flow interface SearchRepository { - fun requestSearchKeyword(keyword: String): Flow> fun requestSearchTag(tag: String): Flow> - fun requestSearchKeywordRecentList(): ArrayList - fun clearSearchKeywordRecent() - fun deleteSearchKeywordRecent(keyword: String) - suspend fun requestSearchTotalCount(tag: String, end: Int): Int + fun requestSearchUser(nickname: String): Flow> + fun requestSearchFollowUser(nickname: String): Flow> + fun requestSearchKeywordRecentList(): SearchHistory + fun updateSearchKeywordRecentList(keyword: String, requestSearchType: SearchHistoryType) + fun clearSearchKeywordRecent(): SearchHistory + fun deleteSearchKeywordRecent( + keyword: String, + deleteKeywordType: SearchHistoryType + ): SearchHistory + + suspend fun requestSearchTotalCount( + tag: String, + end: Int, + searchHistoryType: SearchHistoryType + ): Int } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/bookmark/RequestBookmarkPostUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/bookmark/RequestBookmarkPostUseCase.kt index f80efab5..9e7c40fc 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/bookmark/RequestBookmarkPostUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/bookmark/RequestBookmarkPostUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestBookmarkPostUseCase @Inject constructor( private val bookmarkRepository: BookmarkRepository ) { - suspend operator fun invoke(postId: Int) = + suspend operator fun invoke(postId: Long) = bookmarkRepository.requestBookmarkPost(postId = postId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/bookmark/RequestDeleteBookmarkPostUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/bookmark/RequestDeleteBookmarkPostUseCase.kt index 1c7eef00..b7ae8465 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/bookmark/RequestDeleteBookmarkPostUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/bookmark/RequestDeleteBookmarkPostUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestDeleteBookmarkPostUseCase @Inject constructor( private val bookmarkRepository: BookmarkRepository ) { - suspend operator fun invoke(postId: Int) = + suspend operator fun invoke(postId: Long) = bookmarkRepository.requestDeleteBookmarkPost(postId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestCreatePostCommentReplyUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestCreatePostCommentReplyUseCase.kt new file mode 100644 index 00000000..9f24d805 --- /dev/null +++ b/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestCreatePostCommentReplyUseCase.kt @@ -0,0 +1,12 @@ +package daily.dayo.domain.usecase.comment + +import daily.dayo.domain.model.MentionUser +import daily.dayo.domain.repository.CommentRepository +import javax.inject.Inject + +class RequestCreatePostCommentReplyUseCase @Inject constructor( + private val commentRepository: CommentRepository +) { + suspend operator fun invoke(commentId: Long, contents: String, postId: Long, mentionList: List) = + commentRepository.requestCreatePostCommentReply(commentId = commentId, contents = contents, postId = postId, mentionList = mentionList) +} \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestCreatePostCommentUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestCreatePostCommentUseCase.kt index 09135940..5f21a797 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestCreatePostCommentUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestCreatePostCommentUseCase.kt @@ -1,11 +1,12 @@ package daily.dayo.domain.usecase.comment +import daily.dayo.domain.model.MentionUser import daily.dayo.domain.repository.CommentRepository import javax.inject.Inject class RequestCreatePostCommentUseCase @Inject constructor( private val commentRepository: CommentRepository ) { - suspend operator fun invoke(contents: String, postId: Int) = - commentRepository.requestCreatePostComment(contents = contents, postId = postId) + suspend operator fun invoke(contents: String, postId: Long, mentionList: List) = + commentRepository.requestCreatePostComment(contents = contents, postId = postId, mentionList = mentionList) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestDeletePostCommentUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestDeletePostCommentUseCase.kt index 324aef05..1969f9d2 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestDeletePostCommentUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestDeletePostCommentUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestDeletePostCommentUseCase @Inject constructor( private val commentRepository: CommentRepository ) { - suspend operator fun invoke(commentId: Int) = + suspend operator fun invoke(commentId: Long) = commentRepository.requestDeletePostComment(commentId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestPostCommentUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestPostCommentUseCase.kt index bed567bf..b3c7ec8c 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestPostCommentUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/comment/RequestPostCommentUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestPostCommentUseCase @Inject constructor( private val commentRepository: CommentRepository ) { - suspend operator fun invoke(postId: Int) = + suspend operator fun invoke(postId: Long) = commentRepository.requestPostComment(postId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestCreateFolderInPostUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestCreateFolderInPostUseCase.kt index 959dafc1..3384ad76 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestCreateFolderInPostUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestCreateFolderInPostUseCase.kt @@ -7,6 +7,10 @@ import javax.inject.Inject class RequestCreateFolderInPostUseCase @Inject constructor( private val folderRepository: FolderRepository ) { - suspend operator fun invoke(name: String, privacy: Privacy) = - folderRepository.requestCreateFolderInPost(name = name, privacy = privacy) + suspend operator fun invoke(name: String, description: String, privacy: Privacy) = + folderRepository.requestCreateFolderInPost( + name = name, + description = description, + privacy = privacy + ) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestDeleteFolderUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestDeleteFolderUseCase.kt index 22981697..cd0ac73b 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestDeleteFolderUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestDeleteFolderUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestDeleteFolderUseCase @Inject constructor( private val folderRepository: FolderRepository ) { - suspend operator fun invoke(folderId: Int) = + suspend operator fun invoke(folderId: Long) = folderRepository.requestDeleteFolder(folderId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestEditFolderUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestEditFolderUseCase.kt index f3e83a5a..e4ff0f6f 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestEditFolderUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestEditFolderUseCase.kt @@ -14,7 +14,7 @@ class RequestEditFolderUseCase @Inject constructor( private val folderRepository: FolderRepository ) { suspend operator fun invoke( - folderId: Int, + folderId: Long, name: String, privacy: Privacy, subheading: String?, diff --git a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestFolderInfoUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestFolderInfoUseCase.kt index a43934a6..657a22c4 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestFolderInfoUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestFolderInfoUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestFolderInfoUseCase @Inject constructor( private val folderRepository: FolderRepository ) { - suspend operator fun invoke(folderId: Int) = + suspend operator fun invoke(folderId: Long) = folderRepository.requestFolderInfo(folderId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestFolderMoveUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestFolderMoveUseCase.kt new file mode 100644 index 00000000..04cf1f33 --- /dev/null +++ b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestFolderMoveUseCase.kt @@ -0,0 +1,11 @@ +package daily.dayo.domain.usecase.folder + +import daily.dayo.domain.repository.FolderRepository +import javax.inject.Inject + +class RequestFolderMoveUseCase @Inject constructor( + private val folderRepository: FolderRepository +) { + suspend operator fun invoke(postIdList: List, targetFolderId: Long) = + folderRepository.requestFolderMove(postIdList, targetFolderId) +} diff --git a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestFolderPostListUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestFolderPostListUseCase.kt index b087f9bd..9572ecfb 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestFolderPostListUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestFolderPostListUseCase.kt @@ -1,11 +1,12 @@ package daily.dayo.domain.usecase.folder +import daily.dayo.domain.model.FolderOrder import daily.dayo.domain.repository.FolderRepository import javax.inject.Inject class RequestFolderPostListUseCase @Inject constructor( private val folderRepository: FolderRepository ) { - suspend operator fun invoke(folderId: Int) = - folderRepository.requestDetailListFolder(folderId) -} \ No newline at end of file + suspend operator fun invoke(folderId: Long, folderOrder: FolderOrder) = + folderRepository.requestDetailListFolder(folderId, folderOrder) +} diff --git a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestOrderFolderUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestOrderFolderUseCase.kt deleted file mode 100644 index bc56eaf0..00000000 --- a/domain/src/main/java/daily/dayo/domain/usecase/folder/RequestOrderFolderUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package daily.dayo.domain.usecase.folder - -import daily.dayo.domain.model.FolderOrder -import daily.dayo.domain.repository.FolderRepository -import javax.inject.Inject - -class RequestOrderFolderUseCase @Inject constructor( - private val folderRepository: FolderRepository -) { - suspend operator fun invoke(folderOrder: List) = - folderRepository.requestOrderFolder(folderOrder) -} \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/like/RequestLikePostUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/like/RequestLikePostUseCase.kt index 70c63460..f02ea441 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/like/RequestLikePostUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/like/RequestLikePostUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestLikePostUseCase @Inject constructor( private val heartRepository: HeartRepository ) { - suspend operator fun invoke(postId: Int) = + suspend operator fun invoke(postId: Long) = heartRepository.requestLikePost(postId = postId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/like/RequestPostLikeUsersUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/like/RequestPostLikeUsersUseCase.kt index a53bdd92..71383fbc 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/like/RequestPostLikeUsersUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/like/RequestPostLikeUsersUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestPostLikeUsersUseCase @Inject constructor( private val heartRepository: HeartRepository ) { - suspend operator fun invoke(postId: Int) = + suspend operator fun invoke(postId: Long) = heartRepository.requestPostLikeUsers(postId = postId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/like/RequestUnlikePostUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/like/RequestUnlikePostUseCase.kt index 10bc4d13..48192e7f 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/like/RequestUnlikePostUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/like/RequestUnlikePostUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestUnlikePostUseCase @Inject constructor( private val heartRepository: HeartRepository ) { - suspend operator fun invoke(postId: Int) = + suspend operator fun invoke(postId: Long) = heartRepository.requestUnlikePost(postId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestCertificateEmailPasswordResetUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestCertificateEmailPasswordResetUseCase.kt new file mode 100644 index 00000000..272fe68c --- /dev/null +++ b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestCertificateEmailPasswordResetUseCase.kt @@ -0,0 +1,8 @@ +package daily.dayo.domain.usecase.member + +import daily.dayo.domain.repository.MemberRepository +import javax.inject.Inject + +class RequestCertificateEmailPasswordResetUseCase @Inject constructor(private val memberRepository: MemberRepository) { + suspend operator fun invoke(email: String) = memberRepository.requestCertificateEmailPasswordReset(email) +} \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestCheckEmailAuthUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestCheckOAuthEmailUseCase.kt similarity index 57% rename from domain/src/main/java/daily/dayo/domain/usecase/member/RequestCheckEmailAuthUseCase.kt rename to domain/src/main/java/daily/dayo/domain/usecase/member/RequestCheckOAuthEmailUseCase.kt index 01a3a460..75c4416a 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestCheckEmailAuthUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestCheckOAuthEmailUseCase.kt @@ -3,6 +3,6 @@ package daily.dayo.domain.usecase.member import daily.dayo.domain.repository.MemberRepository import javax.inject.Inject -class RequestCheckEmailAuthUseCase @Inject constructor(private val memberRepository: MemberRepository) { - suspend operator fun invoke(email: String) = memberRepository.requestCheckEmailAuth(email) +class RequestCheckOAuthEmailUseCase @Inject constructor(private val memberRepository: MemberRepository) { + suspend operator fun invoke(email: String) = memberRepository.requestCheckOAuthEmail(email) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestResignGuideImageUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestResignGuideImageUseCase.kt new file mode 100644 index 00000000..ceeaf110 --- /dev/null +++ b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestResignGuideImageUseCase.kt @@ -0,0 +1,17 @@ +package daily.dayo.domain.usecase.member + +import daily.dayo.domain.model.WithdrawalReason +import daily.dayo.domain.model.NetworkResponse +import daily.dayo.domain.repository.MemberRepository +import javax.inject.Inject + +class RequestResignGuideImageUseCase @Inject constructor( + private val memberRepository: MemberRepository +) { + suspend operator fun invoke(fileName: String, withdrawalReason: WithdrawalReason): NetworkResponse? = + when (withdrawalReason) { + WithdrawalReason.WANT_TO_DELETE_HISTORY -> memberRepository.requestResignGuideRecordImage(fileName) + WithdrawalReason.CONTENT_NOT_SATISFYING -> memberRepository.requestResignGuideFollowImage(fileName) + else -> null + } +} \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestResignGuideWordsUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestResignGuideWordsUseCase.kt new file mode 100644 index 00000000..c743429b --- /dev/null +++ b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestResignGuideWordsUseCase.kt @@ -0,0 +1,17 @@ +package daily.dayo.domain.usecase.member + +import daily.dayo.domain.model.NetworkResponse +import daily.dayo.domain.model.WithdrawalReason +import daily.dayo.domain.repository.MemberRepository +import javax.inject.Inject + +class RequestResignGuideWordsUseCase @Inject constructor( + private val memberRepository: MemberRepository +) { + suspend operator fun invoke(withdrawalReason: WithdrawalReason): NetworkResponse>? = + when (withdrawalReason) { + WithdrawalReason.WANT_TO_DELETE_HISTORY -> memberRepository.requestResignGuideRecordWords() + WithdrawalReason.CONTENT_NOT_SATISFYING -> memberRepository.requestResignGuideFollowWords() + else -> null // ๊ฐ€์ด๋“œ๊ฐ€ ํ•„์š” ์—†๋Š” ๊ฒฝ์šฐ null ๋ฐ˜ํ™˜ + } +} diff --git a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestLoginEmailUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestSignInEmailUseCase.kt similarity index 68% rename from domain/src/main/java/daily/dayo/domain/usecase/member/RequestLoginEmailUseCase.kt rename to domain/src/main/java/daily/dayo/domain/usecase/member/RequestSignInEmailUseCase.kt index 1e0de459..8ef8d016 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestLoginEmailUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestSignInEmailUseCase.kt @@ -5,9 +5,9 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class RequestLoginEmailUseCase @Inject constructor( +class RequestSignInEmailUseCase @Inject constructor( private val memberRepository: MemberRepository ) { suspend operator fun invoke(email: String, password: String) = - memberRepository.requestLoginEmail(email = email, password = password) + memberRepository.requestSignInEmail(email = email, password = password) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestLoginKakaoUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestSignInKakaoUseCase.kt similarity index 68% rename from domain/src/main/java/daily/dayo/domain/usecase/member/RequestLoginKakaoUseCase.kt rename to domain/src/main/java/daily/dayo/domain/usecase/member/RequestSignInKakaoUseCase.kt index 0e31bb3b..648e3c30 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestLoginKakaoUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestSignInKakaoUseCase.kt @@ -5,9 +5,9 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class RequestLoginKakaoUseCase @Inject constructor( +class RequestSignInKakaoUseCase @Inject constructor( private val memberRepository: MemberRepository ) { suspend operator fun invoke(accessToken: String) = - memberRepository.requestLoginKakao(accessToken = accessToken) + memberRepository.requestSignInKakao(accessToken = accessToken) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestLogoutUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestSignOutUseCase.kt similarity index 70% rename from domain/src/main/java/daily/dayo/domain/usecase/member/RequestLogoutUseCase.kt rename to domain/src/main/java/daily/dayo/domain/usecase/member/RequestSignOutUseCase.kt index 1bd89a9a..85655396 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestLogoutUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestSignOutUseCase.kt @@ -3,9 +3,9 @@ package daily.dayo.domain.usecase.member import daily.dayo.domain.repository.MemberRepository import javax.inject.Inject -class RequestLogoutUseCase @Inject constructor( +class RequestSignOutUseCase @Inject constructor( private val memberRepository: MemberRepository ) { suspend operator fun invoke() = - memberRepository.requestLogout() + memberRepository.requestSignOut() } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestUpdateMyProfileUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestUpdateMyProfileUseCase.kt index 6fbec8ab..b7318faa 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/member/RequestUpdateMyProfileUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/member/RequestUpdateMyProfileUseCase.kt @@ -5,6 +5,7 @@ import daily.dayo.domain.repository.MemberRepository import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody import java.io.File import javax.inject.Inject @@ -20,7 +21,7 @@ class RequestUpdateMyProfileUseCase @Inject constructor( MultipartBody.Part.createFormData( "profileImg", profileImg.name, - RequestBody.create("image/*".toMediaTypeOrNull(), profileImg) + profileImg.asRequestBody("image/*".toMediaTypeOrNull()) ) else null return memberRepository.requestUpdateMyProfile( diff --git a/domain/src/main/java/daily/dayo/domain/usecase/notice/RequestDetailNoticeUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/notice/RequestDetailNoticeUseCase.kt index 32bb03cc..b99db497 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/notice/RequestDetailNoticeUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/notice/RequestDetailNoticeUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestDetailNoticeUseCase @Inject constructor( private val noticeRepository: NoticeRepository ) { - suspend operator fun invoke(noticeId: Int) = + suspend operator fun invoke(noticeId: Long) = noticeRepository.requestDetailNotice(noticeId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/notification/RequestIsCheckAlarmUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/notification/MarkAlarmAsCheckedUseCase.kt similarity index 68% rename from domain/src/main/java/daily/dayo/domain/usecase/notification/RequestIsCheckAlarmUseCase.kt rename to domain/src/main/java/daily/dayo/domain/usecase/notification/MarkAlarmAsCheckedUseCase.kt index f4e8542f..91f6e723 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/notification/RequestIsCheckAlarmUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/notification/MarkAlarmAsCheckedUseCase.kt @@ -3,9 +3,9 @@ package daily.dayo.domain.usecase.notification import daily.dayo.domain.repository.AlarmRepository import javax.inject.Inject -class RequestIsCheckAlarmUseCase @Inject constructor( +class MarkAlarmAsCheckedUseCase @Inject constructor( private val alarmRepository: AlarmRepository ) { suspend operator fun invoke(alarmId: Int) = - alarmRepository.requestIsCheckAlarm(alarmId) + alarmRepository.markAlarmAsChecked(alarmId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/post/RequestDeletePostUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/post/RequestDeletePostUseCase.kt index 7eb80030..cef8011b 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/post/RequestDeletePostUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/post/RequestDeletePostUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestDeletePostUseCase @Inject constructor( private val postRepository: PostRepository ) { - suspend operator fun invoke(postId: Int) = + suspend operator fun invoke(postId: Long) = postRepository.requestDeletePost(postId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/post/RequestEditPostUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/post/RequestEditPostUseCase.kt index 40f6caf5..4fd86d0b 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/post/RequestEditPostUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/post/RequestEditPostUseCase.kt @@ -8,10 +8,10 @@ class RequestEditPostUseCase @Inject constructor( private val postRepository: PostRepository ) { suspend operator fun invoke( - postId: Int, + postId: Long, category: Category, contents: String, - folderId: Int, + folderId: Long, hashtags: List ) = postRepository.requestEditPost(postId, category, contents, folderId, hashtags) diff --git a/domain/src/main/java/daily/dayo/domain/usecase/post/RequestFeedListUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/post/RequestFeedListUseCase.kt index ebb50a6e..e91a7d70 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/post/RequestFeedListUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/post/RequestFeedListUseCase.kt @@ -1,11 +1,12 @@ package daily.dayo.domain.usecase.post +import daily.dayo.domain.model.Category import daily.dayo.domain.repository.PostRepository import javax.inject.Inject class RequestFeedListUseCase @Inject constructor( private val postRepository: PostRepository ) { - suspend operator fun invoke() = - postRepository.requestFeedList() + suspend operator fun invoke(category: Category) = + postRepository.requestFeedList(category) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/post/RequestPostDetailUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/post/RequestPostDetailUseCase.kt index 8167d252..65c37ec6 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/post/RequestPostDetailUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/post/RequestPostDetailUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestPostDetailUseCase @Inject constructor( private val postRepository: PostRepository ) { - suspend operator fun invoke(postId: Int) = + suspend operator fun invoke(postId: Long) = postRepository.requestPostDetail(postId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/post/RequestUploadPostUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/post/RequestUploadPostUseCase.kt index 9071aeb8..3c5b6457 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/post/RequestUploadPostUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/post/RequestUploadPostUseCase.kt @@ -17,7 +17,7 @@ class RequestUploadPostUseCase @Inject constructor( category: Category, contents: String, files: Array, - folderId: Int, + folderId: Long, tags: Array ): NetworkResponse { val uploadFiles: ArrayList = ArrayList() diff --git a/domain/src/main/java/daily/dayo/domain/usecase/report/RequestSaveCommentReportUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/report/RequestSaveCommentReportUseCase.kt new file mode 100644 index 00000000..db2100a1 --- /dev/null +++ b/domain/src/main/java/daily/dayo/domain/usecase/report/RequestSaveCommentReportUseCase.kt @@ -0,0 +1,12 @@ +package daily.dayo.domain.usecase.report + +import daily.dayo.domain.model.NetworkResponse +import daily.dayo.domain.repository.ReportRepository +import javax.inject.Inject + +class RequestSaveCommentReportUseCase @Inject constructor( + private val reportRepository: ReportRepository +) { + suspend operator fun invoke(comment: String, commentId: Long): NetworkResponse = + reportRepository.requestSaveCommentReport(comment = comment, commentId = commentId) +} \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/report/RequestSavePostReportUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/report/RequestSavePostReportUseCase.kt index 8a65b7f0..70f571ab 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/report/RequestSavePostReportUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/report/RequestSavePostReportUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class RequestSavePostReportUseCase @Inject constructor( private val reportRepository: ReportRepository ) { - suspend operator fun invoke(comment: String, postId: Int) = + suspend operator fun invoke(comment: String, postId: Long) = reportRepository.requestSavePostReport(comment = comment, postId = postId) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/search/DeleteSearchKeywordRecentUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/search/DeleteSearchKeywordRecentUseCase.kt index 2373d3b7..ad53efd3 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/search/DeleteSearchKeywordRecentUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/search/DeleteSearchKeywordRecentUseCase.kt @@ -1,11 +1,12 @@ package daily.dayo.domain.usecase.search +import daily.dayo.domain.model.SearchHistoryType import daily.dayo.domain.repository.SearchRepository import javax.inject.Inject class DeleteSearchKeywordRecentUseCase @Inject constructor( private val searchRepository: SearchRepository ) { - operator fun invoke(keyword: String) = - searchRepository.deleteSearchKeywordRecent(keyword) + operator fun invoke(keyword: String, keywordType: SearchHistoryType) = + searchRepository.deleteSearchKeywordRecent(keyword, keywordType) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchFollowUserUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchFollowUserUseCase.kt new file mode 100644 index 00000000..69a1c965 --- /dev/null +++ b/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchFollowUserUseCase.kt @@ -0,0 +1,14 @@ +package daily.dayo.domain.usecase.search + +import androidx.paging.PagingData +import daily.dayo.domain.model.SearchUser +import daily.dayo.domain.repository.SearchRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class RequestSearchFollowUserUseCase @Inject constructor( + private val searchRepository: SearchRepository +) { + operator fun invoke(nickname: String): Flow> = + searchRepository.requestSearchFollowUser(nickname) +} \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchKeywordUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchKeywordUseCase.kt deleted file mode 100644 index d22e1e98..00000000 --- a/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchKeywordUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package daily.dayo.domain.usecase.search - -import daily.dayo.domain.repository.SearchRepository -import javax.inject.Inject - -class RequestSearchKeywordUseCase @Inject constructor( - private val searchRepository: SearchRepository -) { - operator fun invoke(keyword: String) = - searchRepository.requestSearchKeyword(keyword) -} \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchKeywordUserUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchKeywordUserUseCase.kt new file mode 100644 index 00000000..bd9833f0 --- /dev/null +++ b/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchKeywordUserUseCase.kt @@ -0,0 +1,14 @@ +package daily.dayo.domain.usecase.search + +import androidx.paging.PagingData +import daily.dayo.domain.model.SearchUser +import daily.dayo.domain.repository.SearchRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class RequestSearchKeywordUserUseCase @Inject constructor( + private val searchRepository: SearchRepository +) { + operator fun invoke(nickname: String): Flow> = + searchRepository.requestSearchUser(nickname) +} \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchTagUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchTagUseCase.kt index 1c6126b4..a22ffea5 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchTagUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchTagUseCase.kt @@ -6,6 +6,5 @@ import javax.inject.Inject class RequestSearchTagUseCase @Inject constructor( private val searchRepository: SearchRepository ) { - suspend operator fun invoke(tag: String) = - searchRepository.requestSearchTag(tag) + operator fun invoke(tag: String) = searchRepository.requestSearchTag(tag) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchTotalCountUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchTotalCountUseCase.kt index f58e2495..b6821421 100644 --- a/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchTotalCountUseCase.kt +++ b/domain/src/main/java/daily/dayo/domain/usecase/search/RequestSearchTotalCountUseCase.kt @@ -1,11 +1,12 @@ package daily.dayo.domain.usecase.search +import daily.dayo.domain.model.SearchHistoryType import daily.dayo.domain.repository.SearchRepository import javax.inject.Inject class RequestSearchTotalCountUseCase @Inject constructor( private val searchRepository: SearchRepository ) { - suspend operator fun invoke(tag: String) = - searchRepository.requestSearchTotalCount(tag, 0) + suspend operator fun invoke(tag: String, searchHistoryType: SearchHistoryType) = + searchRepository.requestSearchTotalCount(tag, 0, searchHistoryType) } \ No newline at end of file diff --git a/domain/src/main/java/daily/dayo/domain/usecase/search/UpdateSearchKeywordRecentUseCase.kt b/domain/src/main/java/daily/dayo/domain/usecase/search/UpdateSearchKeywordRecentUseCase.kt new file mode 100644 index 00000000..932986c9 --- /dev/null +++ b/domain/src/main/java/daily/dayo/domain/usecase/search/UpdateSearchKeywordRecentUseCase.kt @@ -0,0 +1,12 @@ +package daily.dayo.domain.usecase.search + +import daily.dayo.domain.model.SearchHistoryType +import daily.dayo.domain.repository.SearchRepository +import javax.inject.Inject + +class UpdateSearchKeywordRecentUseCase @Inject constructor( + private val searchRepository: SearchRepository +) { + operator fun invoke(keyword: String, keywordType: SearchHistoryType) = + searchRepository.updateSearchKeywordRecentList(keyword, keywordType) +} \ No newline at end of file diff --git a/presentation/build.gradle b/presentation/build.gradle index 92432ef4..73d2042e 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -20,14 +20,14 @@ kotlin { android { namespace 'daily.dayo.presentation' - compileSdk 33 + compileSdk rootProject.ext.compileSdkVersion defaultConfig { - minSdk 26 + minSdk rootProject.ext.minSdkVersion versionCode 10000 versionName "1.0.0" - buildConfigField ("String", "NATIVE_APP_KEY", properties['NATIVE_APP_KEY_STR']) + buildConfigField("String", "NATIVE_APP_KEY", properties['NATIVE_APP_KEY_STR']) manifestPlaceholders = [NATIVE_APP_KEY: NATIVE_APP_KEY] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -43,10 +43,19 @@ android { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - buildConfigField("String", "BASE_URL", properties['BASE_URL_RELEASE']) } - debug { - buildConfigField("String", "BASE_URL", properties['BASE_URL_DEBUG']) + } + flavorDimensions = ["environment"] + productFlavors { + dev { + dimension "environment" + buildConfigField("String", "BASE_URL", properties['BASE_URL_DEV']) + buildConfigField("String", "REWARDED_AD_UNIT_ID_FOLDER", properties['REWARDED_AD_UNIT_ID_FOLDER_DEV']) + } + prod { + dimension "environment" + buildConfigField("String", "BASE_URL", properties['BASE_URL_PROD']) + buildConfigField("String", "REWARDED_AD_UNIT_ID_FOLDER", properties['REWARDED_AD_UNIT_ID_FOLDER_PROD']) } } compileOptions { @@ -64,7 +73,7 @@ android { buildFeatures { dataBinding true viewBinding true - compose = true + compose true } } @@ -79,9 +88,11 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' // Compose - def composeBom = platform("androidx.compose:compose-bom:2023.06.01") - implementation(composeBom) - androidTestImplementation(composeBom) + Dependency composeBom = platform('androidx.compose:compose-bom:2025.05.00') + implementation composeBom + testImplementation composeBom + androidTestImplementation composeBom + implementation "androidx.compose.material:material:1.5.4" implementation 'androidx.legacy:legacy-support-v4:1.0.0' def nav_version = "2.8.3" @@ -92,10 +103,10 @@ dependencies { def landscapist_glide_version = "2.2.5" def fragment_version = "1.6.1" def activity_version = "1.7.2" + def emoji_version = "1.0.0-alpha03" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.10.1' - implementation("androidx.compose.material3:material3") implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // Java language implementation @@ -108,6 +119,9 @@ dependencies { implementation("androidx.activity:activity-ktx:$activity_version") // Jetpack Compose + // Material + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") // Android Studio Preview support implementation("androidx.compose.ui:ui-tooling-preview") debugImplementation("androidx.compose.ui:ui-tooling") @@ -115,13 +129,13 @@ dependencies { androidTestImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-test-manifest") // Optional - Integration with activities - implementation("androidx.activity:activity-compose:1.7.2") + implementation("androidx.activity:activity-compose:1.10.1") // Optional - Integration with LiveData implementation("androidx.compose.runtime:runtime-livedata") // Optional - Integration with RxJava implementation("androidx.compose.runtime:runtime-rxjava2") // To use constraintlayout in compose - implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1") + implementation("androidx.constraintlayout:constraintlayout-compose:1.1.1") // Jetpack Navigation // Java language implementation @@ -135,7 +149,8 @@ dependencies { // Testing Navigation androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" // Jetpack Compose Integration - implementation("androidx.navigation:navigation-compose:$nav_version") + implementation "androidx.navigation:navigation-compose:$nav_version" + implementation "androidx.hilt:hilt-navigation-compose:1.2.0" // ViewPager2 implementation "androidx.viewpager2:viewpager2:1.0.0" @@ -194,6 +209,9 @@ dependencies { // Glide-For-Compose implementation "com.github.skydoves:landscapist-glide:$landscapist_glide_version" + // Coil + implementation "io.coil-kt:coil-compose:2.5.0" + // Preference implementation 'androidx.preference:preference-ktx:1.1.1' @@ -217,6 +235,10 @@ dependencies { // optional - Jetpack Compose integration implementation "androidx.paging:paging-compose:3.2.0" + // emoji + implementation "androidx.emoji2:emoji2:$emoji_version" + implementation "androidx.emoji2:emoji2-views:$emoji_version" + // Google In-App Update implementation 'com.google.android.play:app-update-ktx:2.1.0' @@ -235,4 +257,7 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2" // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' + + // Google Ads + implementation 'com.google.android.gms:play-services-ads:23.6.0' } \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/activity/ImageSelectorActivity.kt b/presentation/src/main/java/daily/dayo/presentation/activity/ImageSelectorActivity.kt new file mode 100644 index 00000000..9a33eeec --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/activity/ImageSelectorActivity.kt @@ -0,0 +1,142 @@ +package daily.dayo.presentation.activity + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Build +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.provider.MediaStore +import android.provider.Settings +import android.widget.Toast +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.app.ActivityCompat +import daily.dayo.presentation.R + +class ImageSelectorActivity : AppCompatActivity() { + private val pickImageLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { uri -> + val resultIntent = Intent() + resultIntent.putExtra("image_uri", uri.toString()) + setResult(Activity.RESULT_OK, resultIntent) + finish() + } + } else { + finish() // ์ด๋ฏธ์ง€ ์„ ํƒ ์ทจ์†Œ ์‹œ Activity ์ข…๋ฃŒ + } + } + + private val requestOpenGallery = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + val deniedList: List = permissions.filter { !it.value }.map { it.key } + + when { + deniedList.isNotEmpty() -> { + val map = deniedList.groupBy { permission -> + if (shouldShowRequestPermissionRationale(permission)) getString(R.string.permission_fail_second) + else getString(R.string.permission_fail_final) + } + map[getString(R.string.permission_fail_second)]?.let { + // request denied , request again + Toast.makeText( + this, + getString(R.string.permission_fail_message_gallery), + Toast.LENGTH_SHORT + ).show() + ActivityCompat.requestPermissions( + this, + PERMISSIONS_GALLERY, + 1000 + ) + } + map[getString(R.string.permission_fail_final)]?.let { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).also { + val uri = Uri.parse("package:${this.packageName}") + it.flags = Intent.FLAG_ACTIVITY_NEW_TASK + it.data = uri + } + Toast.makeText( + this, + getString(R.string.permission_fail_final_message_gallery), + Toast.LENGTH_SHORT + ).show() + //request denied, send to settings + } + finish() + } + + else -> { + //All request are permitted + openGallery() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requestOpenGallery.launch(PERMISSIONS_GALLERY) + } + + private fun openGallery() { + val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) + pickImageLauncher.launch(intent) + } + + companion object { + val PERMISSIONS_GALLERY = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arrayOf( + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO + ) + } else { + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + } + } +} + +/*** Activity Launch ํ•˜๋Š” ๊ฒฝ์šฐ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉ **/ + +/* +// onCreate() +val intent = Intent(this, ImageSelectorActivity::class.java) +imagePickerLauncher.launch(intent) + + private val imagePickerLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + val uriString = result.data?.getStringExtra("image_uri") + uriString?.let { + val uri = Uri.parse(it) + val bitmap = getBitmapFromUri(uri) + Glide.with(this) + .load(bitmap) + .into(binding.testImageLoad) + } + } + } + + private fun getBitmapFromUri(uri: Uri): Bitmap? { + return try { + val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val source = ImageDecoder.createSource(contentResolver, uri) + ImageDecoder.decodeBitmap(source) + } else { + MediaStore.Images.Media.getBitmap(contentResolver, uri) + } + bitmap.copy(Bitmap.Config.ARGB_8888, true) // Bitmap์„ mutable๋กœ ๋ณ€ํ™˜ + } catch (e: IOException) { + e.printStackTrace() + null + } + } + + */ \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/activity/LoginActivity.kt b/presentation/src/main/java/daily/dayo/presentation/activity/LoginActivity.kt index 89ab12ce..e7220b94 100644 --- a/presentation/src/main/java/daily/dayo/presentation/activity/LoginActivity.kt +++ b/presentation/src/main/java/daily/dayo/presentation/activity/LoginActivity.kt @@ -6,63 +6,45 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.util.Log -import android.view.ViewTreeObserver import android.widget.Toast -import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import com.google.android.play.core.appupdate.AppUpdateManager import com.google.android.play.core.appupdate.AppUpdateManagerFactory import com.google.android.play.core.install.model.UpdateAvailability -import daily.dayo.presentation.databinding.ActivityLoginBinding import daily.dayo.presentation.viewmodel.AccountViewModel import dagger.hilt.android.AndroidEntryPoint import daily.dayo.presentation.R import daily.dayo.presentation.common.dialog.DefaultDialogAlert +import daily.dayo.presentation.screen.account.AccountScreen +import daily.dayo.presentation.theme.DayoTheme import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @AndroidEntryPoint class LoginActivity : AppCompatActivity() { - private lateinit var binding: ActivityLoginBinding private val loginViewModel by viewModels() private var isReady = false private lateinit var updateDialog: AlertDialog private lateinit var appUpdateManager: AppUpdateManager override fun onCreate(savedInstanceState: Bundle?) { - installSplashScreen() + installSplashScreen().apply { + setKeepOnScreenCondition { !isReady } + } super.onCreate(savedInstanceState) - binding = ActivityLoginBinding.inflate(layoutInflater) createDialogUpdate() checkUpdate() - setContentView(binding.root) - setSystemBackClickListener() observeNetworkException() observeApiException() - setSplash() - } - - private fun setSystemBackClickListener() { - this.onBackPressedDispatcher.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - val navHostFragment = - supportFragmentManager.findFragmentById(R.id.nav_host_fragment) - val backStackEntryCount = - navHostFragment?.childFragmentManager?.backStackEntryCount - - if (backStackEntryCount == 0) { - this@LoginActivity.finish() - } else { - navHostFragment?.childFragmentManager?.popBackStack() - } - } + setContent { + DayoTheme { + AccountScreen() } - ) + } } private fun setFCM() { @@ -83,7 +65,7 @@ class LoginActivity : AppCompatActivity() { } private fun loginSuccess() { - loginViewModel.loginSuccess.observe(this) { isSuccess -> + loginViewModel.autoSignInSuccess.observe(this) { isSuccess -> if (isSuccess.peekContent()) { setFCM() if (loginViewModel.getCurrentUserInfo().nickname == "") { @@ -107,20 +89,6 @@ class LoginActivity : AppCompatActivity() { this@LoginActivity.finish() } - private fun setSplash() { - binding.root.viewTreeObserver.addOnPreDrawListener( - object : ViewTreeObserver.OnPreDrawListener { - override fun onPreDraw(): Boolean { - return if (isReady) { - binding.root.viewTreeObserver.removeOnPreDrawListener(this) - true - } else - false - } - } - ) - } - private fun checkUpdate() { appUpdateManager = AppUpdateManagerFactory.create(this) val appUpdateInfoTask = appUpdateManager.appUpdateInfo diff --git a/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt b/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt index b92702f9..678e8a4c 100644 --- a/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt +++ b/presentation/src/main/java/daily/dayo/presentation/activity/MainActivity.kt @@ -3,48 +3,81 @@ package daily.dayo.presentation.activity import android.Manifest import android.content.Intent import android.content.pm.PackageManager -import android.graphics.Rect import android.os.Build import android.os.Bundle import android.provider.Settings -import android.view.MotionEvent -import android.view.View +import android.util.Log import android.widget.Toast import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat -import androidx.core.view.forEach -import androidx.navigation.NavController -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.setupWithNavController +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.rewarded.RewardedAd +import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback import dagger.hilt.android.AndroidEntryPoint +import daily.dayo.presentation.BuildConfig import daily.dayo.presentation.R -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.databinding.ActivityMainBinding -import daily.dayo.presentation.fragment.home.HomeFragmentDirections +import daily.dayo.presentation.screen.main.MainScreen +import daily.dayo.presentation.theme.DayoTheme import daily.dayo.presentation.viewmodel.AccountViewModel import daily.dayo.presentation.viewmodel.SettingNotificationViewModel @AndroidEntryPoint class MainActivity : AppCompatActivity() { - private lateinit var binding: ActivityMainBinding private val accountViewModel by viewModels() private val settingNotificationViewModel by viewModels() + private var rewardedAd: RewardedAd? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) setSystemBackClickListener() checkCurrentNotification() - initBottomNavigation() - setBottomNaviVisibility() - disableBottomNaviTooltip() getNotificationData() askNotificationPermission() + loadRewardedAd() + setContent { + DayoTheme { + MainScreen( + onAdRequest = { onRewardSuccess -> + showAdIfAvailable(onRewardSuccess) + } + ) + } + } + } + + private fun loadRewardedAd() { + val adRequest = AdRequest.Builder().build() + RewardedAd.load(this, BuildConfig.REWARDED_AD_UNIT_ID_FOLDER, adRequest, object : RewardedAdLoadCallback() { + override fun onAdLoaded(ad: RewardedAd) { + rewardedAd = ad + } + + override fun onAdFailedToLoad(error: LoadAdError) { + rewardedAd = null + } + }) + } + + private fun showAdIfAvailable(onRewardSuccess: () -> Unit) { + rewardedAd?.let { ad -> + ad.show(this) { + // ๊ด‘๊ณ ๋ฅผ ๋๊นŒ์ง€ ๋ดค์„ ๋•Œ๋งŒ ํ˜ธ์ถœ๋จ + onRewardSuccess() + + // ๊ด‘๊ณ  ๋‹ค์‹œ ๋กœ๋“œ + rewardedAd = null + loadRewardedAd() + } + } ?: run { + Log.d("Ad", "The rewarded ad wasn't ready yet.") + loadRewardedAd() + } } private fun setSystemBackClickListener() { @@ -59,8 +92,6 @@ class MainActivity : AppCompatActivity() { if (backStackEntryCount == 0) { this@MainActivity.finish() - } else { - findNavController().popBackStack() } } } @@ -82,24 +113,6 @@ class MainActivity : AppCompatActivity() { if (extraFragment != null && extraFragment == "Notification") { val postId = intent.getStringExtra("PostId")?.toInt() val memberId = intent.getStringExtra("MemberId") - if (postId != null) findNavController().navigateSafe( - currentDestinationId = R.id.HomeFragment, - action = R.id.action_homeFragment_to_postFragment, - args = HomeFragmentDirections.actionHomeFragmentToPostFragment( - postId = postId - ).arguments - ) - else if (memberId != null) findNavController().navigateSafe( - currentDestinationId = R.id.HomeFragment, - action = R.id.action_homeFragment_to_profileFragment, - args = HomeFragmentDirections.actionHomeFragmentToProfileFragment( - memberId = memberId - ).arguments - ) - else findNavController().navigateSafe( - currentDestinationId = R.id.HomeFragment, - action = R.id.action_homeFragment_to_notificationFragment - ) } } @@ -114,7 +127,7 @@ class MainActivity : AppCompatActivity() { when { deniedList.isNotEmpty() -> { accountViewModel.requestCurrentUserNotiDevicePermit(false) - accountViewModel.requestCurrentUserNotiNoticePermit(false) + accountViewModel.changeNoticeNotificationSetting(false) val map = deniedList.groupBy { permission -> if (shouldShowRequestPermissionRationale(permission)) getString(R.string.permission_fail_second) else getString( R.string.permission_fail_final @@ -150,7 +163,7 @@ class MainActivity : AppCompatActivity() { //All request are permitted // ์•Œ๋ฆผ ์ตœ์ดˆ ํ—ˆ์šฉ์‹œ์— ๋ชจ๋“  ์•Œ๋ฆผ ํ—ˆ์šฉ์ฒ˜๋ฆฌ accountViewModel.requestCurrentUserNotiDevicePermit(true) - accountViewModel.requestCurrentUserNotiNoticePermit(true) + accountViewModel.changeNoticeNotificationSetting(true) settingNotificationViewModel.registerDeviceToken() settingNotificationViewModel.requestReceiveAlarm() } @@ -178,86 +191,6 @@ class MainActivity : AppCompatActivity() { } } - private fun initBottomNavigation() { - binding.bottomNavigationMainBar.setupWithNavController(findNavController()) - } - - private fun findNavController(): NavController { - val navHostFragment = - supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment - return navHostFragment.navController - } - - private fun setBottomNaviVisibility() { - binding.bottomNavigationMainBar.itemIconTintList = null - findNavController().addOnDestinationChangedListener { _, destination, _ -> - binding.layoutBottomNavigationMain.visibility = when (destination.id) { - R.id.HomeFragment -> View.VISIBLE - R.id.FeedFragment -> View.VISIBLE - R.id.NotificationFragment -> View.VISIBLE - R.id.MyPageFragment -> View.VISIBLE - else -> View.GONE - } - } - // WriteFragment - binding.bottomNavigationMainBar.setItemOnTouchListener(R.id.WriteFragment, - object : View.OnTouchListener { - var rect = Rect() - var isInside = true - override fun onTouch(v: View?, event: MotionEvent?): Boolean { - when (event?.action) { - MotionEvent.ACTION_DOWN -> { - binding.bottomNavigationMainBar.menu.findItem(R.id.WriteFragment) - .setIcon(R.drawable.ic_write_filled) - rect = Rect(v!!.left, v.top, v.right, v.bottom) - isInside = true - return true - } - - MotionEvent.ACTION_MOVE -> { - isInside = - rect.contains(v!!.left + event.x.toInt(), v.top + event.y.toInt()) - binding.bottomNavigationMainBar.clearFocus() - return false - } - - MotionEvent.ACTION_UP -> { - binding.bottomNavigationMainBar.menu.findItem(R.id.WriteFragment) - .setIcon(R.drawable.ic_write) - if (isInside) { - when (findNavController().currentDestination!!.id) { - R.id.HomeFragment -> findNavController().navigate(R.id.action_homeFragment_to_writeFragment) - R.id.FeedFragment -> findNavController().navigate(R.id.action_feedFragment_to_writeFragment) - R.id.NotificationFragment -> findNavController().navigate(R.id.action_notificationFragment_to_writeFragment) - R.id.MyPageFragment -> findNavController().navigate(R.id.action_myPageFragment_to_writeFragment) - } - } - return true - } - - else -> return true - } - } - }) - } - - private fun disableBottomNaviTooltip() { - binding.bottomNavigationMainBar.menu.forEach { - val view = binding.bottomNavigationMainBar.findViewById(it.itemId) - view.setOnLongClickListener { - true - } - } - } - - fun setBottomNavigationIconClickListener(reselectedIconId: Int, reselectAction: () -> Unit) { - binding.bottomNavigationMainBar.setOnItemReselectedListener { - when (it.itemId) { - reselectedIconId -> reselectAction() - } - } - } - companion object { val notificationPermission = arrayOf(Manifest.permission.POST_NOTIFICATIONS) } diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/BlockListAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/BlockListAdapter.kt deleted file mode 100644 index f7abe000..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/BlockListAdapter.kt +++ /dev/null @@ -1,79 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.CheckBox -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.common.GlideLoadUtil.loadImageView -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.ItemBlockBinding -import daily.dayo.domain.model.UserBlocked - -class BlockListAdapter(private val requestManager: RequestManager) : - RecyclerView.Adapter() { - - companion object { - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: UserBlocked, newItem: UserBlocked) = - oldItem.memberId == newItem.memberId - - override fun areContentsTheSame(oldItem: UserBlocked, newItem: UserBlocked): Boolean = - oldItem == newItem - } - } - - private val differ = AsyncListDiffer(this, diffCallback) - fun submitList(list: List) = differ.submitList(list) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BlockListViewHolder { - return BlockListViewHolder( - ItemBlockBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) - } - - override fun onBindViewHolder(holder: BlockListViewHolder, position: Int) { - val item = differ.currentList[position] - holder.bind(item) - } - - override fun getItemCount(): Int { - return differ.currentList.size - } - - interface OnItemClickListener { - fun onItemClick(checkbox: CheckBox, blockUser: UserBlocked, position: Int) - } - - private var listener: OnItemClickListener? = null - fun setOnItemClickListener(listener: OnItemClickListener) { - this.listener = listener - } - - inner class BlockListViewHolder(private val binding: ItemBlockBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(blockUser: UserBlocked) { - binding.blockUser = blockUser.nickname - loadImageView( - requestManager = requestManager, - width = binding.imgBlockUserProfile.width, - height = binding.imgBlockUserProfile.height, - imgName = blockUser.profileImg ?: "", - imgView = binding.imgBlockUserProfile - ) - setUnblockButtonClickListener(blockUser) - } - - private fun setUnblockButtonClickListener(blockUser: UserBlocked) { - val pos = adapterPosition - if (pos != RecyclerView.NO_POSITION) { - binding.btnBlockUserCancel.setOnDebounceClickListener { - listener?.onItemClick(binding.btnBlockUserCancel, blockUser, pos) - } - } - } - } -} diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/FeedListAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/FeedListAdapter.kt deleted file mode 100644 index fc896f25..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/FeedListAdapter.kt +++ /dev/null @@ -1,407 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.content.res.ColorStateList -import android.util.TypedValue -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageButton -import android.widget.ImageView -import android.widget.LinearLayout -import androidx.core.content.ContextCompat -import androidx.databinding.library.baseAdapters.BR -import androidx.navigation.Navigation -import androidx.paging.PagingDataAdapter -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager2.widget.ViewPager2 -import com.airbnb.lottie.LottieAnimationView -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import com.google.android.material.chip.Chip -import daily.dayo.domain.model.Post -import daily.dayo.domain.model.User -import daily.dayo.domain.model.categoryKR -import daily.dayo.presentation.R -import daily.dayo.presentation.common.GlideLoadUtil.loadImageView -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.TimeChangerUtil.timeChange -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.ItemFeedPostBinding -import daily.dayo.presentation.fragment.feed.FeedFragmentDirections - -class FeedListAdapter(private val requestManager: RequestManager, private val currentUserInfo: User) : - PagingDataAdapter(diffCallback) { - - private var glideRequestManager: RequestManager? = null - private var postImageSliderAdapter: PostImageSliderAdapter? = null - private var indicators: Array? = null - - companion object { - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Post, newItem: Post) = - oldItem.postId == newItem.postId - - override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean = - oldItem.apply { - preLoadThumbnail = null - preLoadUserImg = null - } == newItem.apply { - preLoadThumbnail = null - preLoadUserImg = null - } - - // areItemTheSame()์ด true, areContentsTheSame()์ด false์„ ํ˜ธ์ถœ ํ•˜๋ฉด ๋ฐ˜ํ™˜ - override fun getChangePayload(oldItem: Post, newItem: Post): Any? { - return if (oldItem.heart != newItem.heart - || oldItem.heartCount != newItem.heartCount - || oldItem.commentCount != newItem.commentCount - || oldItem.bookmark != newItem.bookmark - ) true else null - } - } - } - - interface OnItemClickListener { - fun likePostClick(button: ImageButton, post: Post, position: Int) - fun likeCountClick(postId: Int) - fun bookmarkPostClick(button: ImageButton, post: Post, position: Int) - fun tagPostClick(chip: Chip) - } - - private var listener: OnItemClickListener? = null - fun setOnItemClickListener(listener: OnItemClickListener) { - this.listener = listener - } - - fun updateItemAtPosition(position: Int, newPost: Post) { - getItem(position).run { newPost } - notifyItemChanged(position) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedListViewHolder { - return FeedListViewHolder( - ItemFeedPostBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) - } - - override fun onBindViewHolder( - holder: FeedListViewHolder, - position: Int, - payloads: MutableList - ) { - if (payloads.isEmpty()) { - this.onBindViewHolder(holder, position) - } else { - if (payloads[0] == true) { - if (getItem(position) == null) { - this.onBindViewHolder(holder, position) - } else { - holder.bindReactionState(getItem(position)!!) - } - } - } - } - - override fun onBindViewHolder(holder: FeedListViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { - super.onDetachedFromRecyclerView(recyclerView) - glideRequestManager = null - postImageSliderAdapter = null - indicators = null - } - - inner class FeedListViewHolder(private val binding: ItemFeedPostBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind(post: Post?) { - with(binding) { - post?.let { - this.post = it - this.createDateTime = timeChange( - context = binding.tvFeedPostTime.context, - time = it.createDateTime ?: "" - ) - this.heart = it.heart - this.heartCountStr = it.heartCount.toString() - this.commentCountStr = it.commentCount?.toString() ?: "" - this.bookmark = it.bookmark - } - } - - binding.categoryKR = post?.category?.let { categoryKR(it) } - loadImageView( - requestManager = requestManager, - width = binding.imgFeedPostUserProfile.width, - height = binding.imgFeedPostUserProfile.height, - imgName = post?.userProfileImage ?: "", - imgView = binding.imgFeedPostUserProfile - ) - - // ์ด๋ฏธ์ง€ - setImageSlider() - binding.viewFeedPostImageIndicators.removeAllViews() - post?.postImages?.let { - postImageSliderAdapter?.submitList(it) - if (it.size > 1) setUpIndicators(it.size) - } - - // ์˜ต์…˜ - val isMine = (post?.memberId == currentUserInfo.memberId) - setPostOptionClickListener( - isMine = isMine, - postId = post?.postId!!, - memberId = post.memberId!! - ) - post.memberId?.let { memberId -> - setOnUserProfileClickListener(postMemberId = memberId) - } - post.postId?.let { postId -> - setOnPostClickListener(postId = postId, nickname = post.nickname) - } - - // ํ•ด์‹œํƒœ๊ทธ - if (post.hashtags?.isNotEmpty() == true) { - binding.layoutFeedPostTagList.visibility = View.VISIBLE - post.hashtags?.let { hashTags -> - setTagList(hashTags) - } - } else { - binding.layoutFeedPostTagList.visibility = View.GONE - } - - // ์ข‹์•„์š” - binding.btnFeedPostLike.setOnDebounceClickListener { - listener?.likePostClick( - button = binding.btnFeedPostLike, - post = post, - position = bindingAdapterPosition - ) - } - - binding.tvFeedPostLikeCount.setOnDebounceClickListener { - post.postId?.let { postId -> - if (post.heartCount > 0) listener?.likeCountClick(postId = postId) - } - } - - postImageSliderAdapter?.setOnItemClickListener(object : - PostImageSliderAdapter.OnItemClickListener { - override fun postImageDoubleTap(lottieAnimationView: LottieAnimationView) { - if (!post.heart) { - lottieAnimationView.visibility = View.VISIBLE - lottieAnimationView.playAnimation() - listener?.likePostClick( - button = binding.btnFeedPostLike, - post = post, - position = bindingAdapterPosition - ) - } - } - }) - - // ๋ถ๋งˆํฌ - binding.btnFeedPostBookmark.setOnDebounceClickListener { - listener?.bookmarkPostClick( - button = binding.btnFeedPostBookmark, - post = post, - position = bindingAdapterPosition - ) - } - } - - private fun setImageSlider() { - glideRequestManager = Glide.with(binding.root) - postImageSliderAdapter = glideRequestManager?.let { requestManager -> - PostImageSliderAdapter(requestManager = requestManager) - } - - with(binding.vpFeedPostImage) { - adapter = postImageSliderAdapter - overScrollMode = View.OVER_SCROLL_NEVER - offscreenPageLimit = 1 - registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - setCurrentIndicator(position) - } - }) - } - } - - private fun setUpIndicators(count: Int) { - indicators = arrayOfNulls(count) - val params = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT - ) - params.setMargins(16, 8, 16, 8) - - indicators?.let { indicators -> - for (i in indicators.indices) { - indicators[i] = ImageView(binding.root.context) - indicators[i]!!.setImageDrawable( - ContextCompat.getDrawable(binding.root.context, R.drawable.ic_indicator_inactive) - ) - indicators[i]!!.layoutParams = params - binding.viewFeedPostImageIndicators.addView(indicators[i]) - } - } - setCurrentIndicator(0) - } - - private fun setCurrentIndicator(position: Int) { - val childCount: Int = binding.viewFeedPostImageIndicators.childCount - for (i in 0 until childCount) { - val imageView = binding.viewFeedPostImageIndicators.getChildAt(i) as ImageView - if (i == position) { - imageView.setImageDrawable( - ContextCompat.getDrawable(binding.root.context, R.drawable.ic_indicator_active) - ) - } else { - imageView.setImageDrawable( - ContextCompat.getDrawable(binding.root.context, R.drawable.ic_indicator_inactive) - ) - } - } - } - - private fun setTagList(tagList: List) { - binding.chipgroupFeedPostTagList.removeAllViews() - if (!tagList.isNullOrEmpty()) { - (tagList.indices).mapNotNull { index -> - val chip = LayoutInflater.from(binding.chipgroupFeedPostTagList.context) - .inflate(R.layout.item_post_tag, null) as Chip - val layoutParams = ViewGroup.MarginLayoutParams( - ViewGroup.MarginLayoutParams.WRAP_CONTENT, - ViewGroup.MarginLayoutParams.WRAP_CONTENT - ) - with(chip) { - chipBackgroundColor = - ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_pressed), - intArrayOf(android.R.attr.state_pressed) - ), - intArrayOf( - resources.getColor(R.color.gray_6_F0F1F3, context?.theme), - resources.getColor( - R.color.primary_green_23C882, context?.theme - ) - ) - ) - - setTextColor( - ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_pressed), - intArrayOf(android.R.attr.state_pressed) - ), - intArrayOf( - resources.getColor(R.color.gray_1_313131, context?.theme), - resources.getColor( - R.color.white_FFFFFF, context?.theme - ) - ) - ) - ) - setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12F) - text = "# ${trimBlankText(tagList[index])}" - - setOnDebounceClickListener { - listener?.tagPostClick(chip = this) - } - } - binding.chipgroupFeedPostTagList.addView(chip, layoutParams) - } - } - } - - private fun setOnUserProfileClickListener(postMemberId: String) { - binding.imgFeedPostUserProfile.setOnDebounceClickListener { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.FeedFragment, - action = R.id.action_feedFragment_to_profileFragment, - args = FeedFragmentDirections.actionFeedFragmentToProfileFragment(memberId = postMemberId).arguments - ) - } - binding.tvFeedPostUserNickname.setOnDebounceClickListener { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.FeedFragment, - action = R.id.action_feedFragment_to_profileFragment, - args = FeedFragmentDirections.actionFeedFragmentToProfileFragment(memberId = postMemberId).arguments - ) - } - } - - private fun setOnPostClickListener(postId: Int, nickname: String) { - binding.tvFeedPostContent.setOnDebounceClickListener { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.FeedFragment, - action = R.id.action_feedFragment_to_postFragment, - args = FeedFragmentDirections.actionFeedFragmentToPostFragment(postId = postId).arguments - ) - } - binding.btnFeedPostComment.setOnDebounceClickListener { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.FeedFragment, - action = R.id.action_feedFragment_to_postFragment, - args = FeedFragmentDirections.actionFeedFragmentToPostFragment(postId = postId).arguments - ) - } - } - - private fun setPostOptionClickListener(isMine: Boolean, postId: Int, memberId: String) { - binding.btnFeedPostOption.setOnDebounceClickListener { - if (isMine) { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.FeedFragment, - action = R.id.action_feedFragment_to_postOptionFragment, - args = FeedFragmentDirections.actionFeedFragmentToPostOptionMineFragment( - postId = postId - ).arguments - ) - } else { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.FeedFragment, - action = R.id.action_feedFragment_to_postOptionFragment, - args = FeedFragmentDirections.actionFeedFragmentToPostOptionFragment( - postId = postId, memberId = memberId - ).arguments - ) - } - } - } - - fun bindReactionState(post: Post) { - setHeartState(post) - } - - private fun setHeartState(post: Post) { - binding.post?.let { - it.heart = post.heart - it.heartCount = post.heartCount - it.bookmark = post.bookmark - } - setBindingSetVariable(post) - } - - private fun setBindingSetVariable(post: Post) { - with(binding) { - setVariable(BR.heart, post.heart) - setVariable(BR.heartCountStr, post.heartCount.toString()) - setVariable(BR.commentCountStr, post.commentCount.toString()) - setVariable(BR.bookmark, post.bookmark) - executePendingBindings() - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/FolderPostListAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/FolderPostListAdapter.kt deleted file mode 100644 index 18c8080b..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/FolderPostListAdapter.kt +++ /dev/null @@ -1,89 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.navigation.Navigation -import androidx.paging.PagingDataAdapter -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.RequestManager -import daily.dayo.domain.model.FolderPost -import daily.dayo.presentation.R -import daily.dayo.presentation.common.GlideLoadUtil.loadImageView -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.ItemFolderPostBinding -import daily.dayo.presentation.fragment.mypage.folder.FolderFragmentDirections -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class FolderPostListAdapter(private val requestManager: RequestManager) : - PagingDataAdapter(diffCallback) { - - companion object { - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: FolderPost, newItem: FolderPost) = - oldItem.postId == newItem.postId - - override fun areContentsTheSame(oldItem: FolderPost, newItem: FolderPost): Boolean = - oldItem == newItem - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderPostListViewHolder { - return FolderPostListViewHolder( - ItemFolderPostBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) - } - - override fun onBindViewHolder(holder: FolderPostListViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - inner class FolderPostListViewHolder(private val binding: ItemFolderPostBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(folderPost: FolderPost?) { - val layoutParams = ViewGroup.MarginLayoutParams( - ViewGroup.MarginLayoutParams.MATCH_PARENT, - ViewGroup.MarginLayoutParams.MATCH_PARENT - ) - - binding.layoutFolderPostContentsShimmer.startShimmer() - binding.layoutFolderPostContentsShimmer.visibility = View.VISIBLE - binding.imgFolderPost.visibility = View.INVISIBLE - - CoroutineScope(Dispatchers.Main).launch { - loadImageView( - requestManager = requestManager, - width = layoutParams.width, - height = layoutParams.width, - imgName = folderPost?.thumbnailImage ?: "", - imgView = binding.imgFolderPost - ) - }.invokeOnCompletion { throwable -> - when (throwable) { - is CancellationException -> Log.e("Image Loading", "CANCELLED") - null -> { - binding.layoutFolderPostContentsShimmer.stopShimmer() - binding.layoutFolderPostContentsShimmer.visibility = View.GONE - binding.imgFolderPost.visibility = View.VISIBLE - } - } - } - - binding.root.setOnDebounceClickListener { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.FolderFragment, - action = R.id.action_folderFragment_to_postFragment, - args = FolderFragmentDirections.actionFolderFragmentToPostFragment(folderPost!!.postId).arguments - ) - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/FolderSettingAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/FolderSettingAdapter.kt deleted file mode 100644 index 3b3143a4..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/FolderSettingAdapter.kt +++ /dev/null @@ -1,78 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.ViewGroup -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import daily.dayo.presentation.common.ItemTouchHelperCallback -import daily.dayo.presentation.databinding.ItemFolderListBinding -import daily.dayo.domain.model.Folder -import daily.dayo.domain.model.FolderOrder -import java.util.* - -class FolderSettingAdapter(private val isChange: Boolean) : - RecyclerView.Adapter(), - ItemTouchHelperCallback.OnItemMoveListener{ - - private lateinit var dragListener: OnStartDragListener - lateinit var folderOrderList: MutableList - - companion object { - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Folder, newItem: Folder) = - oldItem.folderId == newItem.folderId - - override fun areContentsTheSame(oldItem: Folder, newItem: Folder): Boolean = - oldItem == newItem - } - } - - private val differ = AsyncListDiffer(this, diffCallback) - fun submitList(list: List) = differ.submitList(list) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderSettingViewHolder { - return FolderSettingViewHolder(ItemFolderListBinding.inflate(LayoutInflater.from(parent.context),parent, false)) - } - - override fun onBindViewHolder(holder: FolderSettingViewHolder, position: Int) { - val item = differ.currentList[position] - holder.isChange = isChange - if(isChange) { - holder.itemView.setOnTouchListener { view, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - dragListener.onStartDrag(holder) - } - return@setOnTouchListener false - } - } - holder.bind(item) - } - - override fun getItemCount(): Int { - return differ.currentList.size - } - - interface OnStartDragListener { - fun onStartDrag(viewHolder: RecyclerView.ViewHolder) - } - - fun startDrag(listener: OnStartDragListener) { - this.dragListener = listener - } - - override fun onItemMoved(fromPosition: Int, toPosition: Int) { - Collections.swap(folderOrderList, fromPosition, toPosition) - folderOrderList[toPosition].orderIndex = folderOrderList[fromPosition].orderIndex.also { folderOrderList[fromPosition].orderIndex = folderOrderList[toPosition].orderIndex } - notifyItemMoved(fromPosition, toPosition) - } - - inner class FolderSettingViewHolder(private val binding: ItemFolderListBinding) : RecyclerView.ViewHolder(binding.root) { - var isChange = false - fun bind(folder: Folder) { - binding.folder = folder - binding.isChangeEnable = isChange - } - } -} diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/FollowListAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/FollowListAdapter.kt deleted file mode 100644 index f3874b86..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/FollowListAdapter.kt +++ /dev/null @@ -1,99 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.Button -import androidx.navigation.Navigation -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.RequestManager -import daily.dayo.domain.model.MyFollower -import daily.dayo.domain.model.User -import daily.dayo.presentation.R -import daily.dayo.presentation.common.GlideLoadUtil.loadImageView -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.ItemFollowBinding -import daily.dayo.presentation.fragment.mypage.follow.FollowFragmentDirections - -class FollowListAdapter(private val requestManager: RequestManager, private val userInfo: User) : - RecyclerView.Adapter() { - - companion object { - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: MyFollower, newItem: MyFollower) = - oldItem.memberId == newItem.memberId - - override fun areContentsTheSame(oldItem: MyFollower, newItem: MyFollower): Boolean = - oldItem == newItem - } - } - - private val differ = AsyncListDiffer(this, diffCallback) - fun submitList(list: List) = differ.submitList(list) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FollowListViewHolder { - return FollowListViewHolder( - ItemFollowBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) - } - - override fun onBindViewHolder(holder: FollowListViewHolder, position: Int) { - val item = differ.currentList[position] - holder.bind(item) - } - - override fun getItemCount(): Int { - return differ.currentList.size - } - - interface OnItemClickListener { - fun onItemClick(button: Button, follow: MyFollower, position: Int) - } - - private var listener: OnItemClickListener? = null - fun setOnItemClickListener(listener: OnItemClickListener) { - this.listener = listener - } - - inner class FollowListViewHolder(private val binding: ItemFollowBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(follow: MyFollower) { - binding.follow = follow - binding.isMine = - follow.memberId == userInfo.memberId - loadImageView( - requestManager = requestManager, - width = binding.imgFollowUserProfile.width, - height = binding.imgFollowUserProfile.height, - imgName = follow.profileImg ?: "", - imgView = binding.imgFollowUserProfile - ) - - setRootClickListener(follow.memberId) - setFollowButtonClickListener(follow) - } - - private fun setRootClickListener(memberId: String) { - binding.root.setOnDebounceClickListener { - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.FollowFragment, - action = R.id.action_followFragment_to_profileFragment, - args = FollowFragmentDirections.actionFollowFragmentToProfileFragment(memberId = memberId).arguments - ) - } - } - - private fun setFollowButtonClickListener(follow: MyFollower) { - val pos = adapterPosition - if (pos != RecyclerView.NO_POSITION) { - binding.btnFollowUserFollow.setOnDebounceClickListener { - listener?.onItemClick(binding.btnFollowUserFollow, follow, pos) - } - } - } - } - -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/HomeDayoPickAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/HomeDayoPickAdapter.kt deleted file mode 100644 index ab293620..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/HomeDayoPickAdapter.kt +++ /dev/null @@ -1,332 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.animation.Animator -import android.graphics.Bitmap -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.databinding.library.baseAdapters.BR -import androidx.navigation.Navigation -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.RequestManager -import daily.dayo.domain.model.Post -import daily.dayo.presentation.R -import daily.dayo.presentation.common.GlideLoadUtil.HOME_POST_THUMBNAIL_SIZE -import daily.dayo.presentation.common.GlideLoadUtil.HOME_USER_THUMBNAIL_SIZE -import daily.dayo.presentation.common.GlideLoadUtil.loadImageBackground -import daily.dayo.presentation.common.GlideLoadUtil.loadImagePreload -import daily.dayo.presentation.common.GlideLoadUtil.loadImageView -import daily.dayo.presentation.common.GlideLoadUtil.loadImageViewProfile -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.common.toBitmap -import daily.dayo.presentation.databinding.ItemMainPostBinding -import daily.dayo.presentation.fragment.home.HomeFragmentDirections -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class HomeDayoPickAdapter( - val rankingShowing: Boolean, - private val requestManager: RequestManager -) : ListAdapter(diffCallback) { - companion object { - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Post, newItem: Post) = - oldItem.postId == newItem.postId - - override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean = - oldItem.apply { - preLoadThumbnail = null - preLoadUserImg = null - } == newItem.apply { - preLoadThumbnail = null - preLoadUserImg = null - } - - override fun getChangePayload(oldItem: Post, newItem: Post): Any? { - return if (oldItem.heart != newItem.heart || oldItem.heartCount != newItem.heartCount) true else null - } - } - } - - interface OnItemClickListener { - fun likePostClick(post: Post) - } - - private var clickListener: OnItemClickListener? = null - fun setOnItemClickListener(listener: OnItemClickListener) { - this.clickListener = listener - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeDayoPickViewHolder { - return HomeDayoPickViewHolder( - ItemMainPostBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - - override fun onBindViewHolder( - holder: HomeDayoPickViewHolder, - position: Int, - payloads: MutableList - ) { - if (payloads.isEmpty()) { - this.onBindViewHolder(holder, position) - } else { - if (payloads[0] == true) { - holder.bindLikeState(getItem(position)) - } - } - } - - override fun onBindViewHolder(holder: HomeDayoPickViewHolder, position: Int) { - val item = getItem(position) - holder.bind(item, position) - preloadFutureLoadingImages(position) - } - - override fun getItemViewType(position: Int): Int { - // ViewHolder Pattern์˜ ์ด์ ์„ ์žƒ์—ˆ์ง€๋งŒ, ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ„ ์ด๋™ํ•˜๋ฉด์„œ ๋žญํ‚น ์ˆซ์ž๊ฐ€ ์ž˜๋ชป ํ‘œ์‹œ๋˜๋Š” ์ ์„ ํ•ด๊ฒฐ - return position - } - - override fun submitList(list: MutableList?) { - super.submitList(list?.let { ArrayList(it) }) - } - - private fun preloadFutureLoadingImages(position: Int) { - if (position <= itemCount) { - val endPosition = if (position + 6 > itemCount) { - itemCount - } else { - position + 6 - } - for (i in position until endPosition) { - loadImagePreload( - requestManager = requestManager, - width = HOME_POST_THUMBNAIL_SIZE, - height = HOME_POST_THUMBNAIL_SIZE, - imgName = getItem(i).thumbnailImage ?: "" - ) - } - } - } - - inner class HomeDayoPickViewHolder( - private val binding: ItemMainPostBinding - ) : RecyclerView.ViewHolder(binding.root) { - lateinit var postContent: Post - - fun bind(postContent: Post, currentPosition: Int) { - this.postContent = postContent - loadImages() - setRanking(currentPosition) - - setBindingSetVariable( - postContent.apply { - binding.lottieMainPostLike.progress = if (heart) 1F else 0F - } - ) - postContent.postId?.let { setRootClickListener(it, postContent.nickname) } - postContent.memberId?.let { setNicknameClickListener(it) } - binding.lottieMainPostLike.setOnDebounceClickListener(300L) { - clickListener?.likePostClick(post = postContent) - } - } - - private fun setBindingSetVariable(post: Post) { - with(binding) { - setVariable(BR.post, post) - executePendingBindings() - } - } - - fun bindLikeState(post: Post) { - setLottieClickListener(post) - setHeartCount(post) - } - - private fun setLottieClickListener(post: Post) { - with(binding.lottieMainPostLike) { - setOnDebounceClickListener(300L) { - clickListener?.likePostClick(post = post) - } - - this.removeAllAnimatorListeners() - this.addAnimatorListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animation: Animator) { - this@with.setOnDebounceClickListener(0L) {} - } - - override fun onAnimationEnd(animation: Animator) { - this@with.setOnDebounceClickListener(0L) { - clickListener?.likePostClick(post = post) - } - } - - override fun onAnimationCancel(animation: Animator) {} - override fun onAnimationRepeat(animation: Animator) {} - }) - - if (post.heart) this.playAnimation() - else this.progress = 0F - } - } - - private fun setHeartCount(post: Post) { - binding.post = post - setBindingSetVariable(post) - } - - private fun setRootClickListener(postId: Int, nickname: String) { - binding.root.setOnDebounceClickListener { - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.HomeFragment, - action = R.id.action_homeFragment_to_postFragment, - args = HomeFragmentDirections.actionHomeFragmentToPostFragment(postId).arguments - ) - } - } - - private fun setNicknameClickListener(memberId: String) { - binding.tvMainPostUserNickname.setOnDebounceClickListener { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.HomeFragment, - action = R.id.action_homeFragment_to_profileFragment, - args = HomeFragmentDirections.actionHomeFragmentToProfileFragment(memberId = memberId).arguments - ) - } - } - - private fun startShimmer() { - with(binding.layoutContentsShimmer) { - startShimmer() - visibility = View.VISIBLE - setShimmerDisplay() - } - } - - private fun stopShimmer() { - with(binding.layoutContentsShimmer) { - stopShimmer() - visibility = View.GONE - setShimmerDisplay() - } - } - - private fun setShimmerDisplay() { - binding.layoutContents.isVisible = !binding.layoutContentsShimmer.isVisible - } - - private fun setRanking(currentPosition: Int) { - with(binding) { - layoutPostRankingNumber.isVisible = !(currentPosition > 3 || !rankingShowing) - tvPostRankingNumber.text = (currentPosition + 1).toString() - } - } - - private fun loadImages() { - startShimmer() - - val postImg = binding.imgMainPost - val userThumbnailImg = binding.imgMainPostUserProfile - - CoroutineScope(Dispatchers.Main).launch { - val postImgBitmap: Bitmap? - val userThumbnailImgBitmap: Bitmap? - if (postContent.preLoadThumbnail == null) { - postImgBitmap = withContext(Dispatchers.IO) { - loadImageBackground( - requestManager = requestManager, - width = HOME_POST_THUMBNAIL_SIZE, - height = HOME_POST_THUMBNAIL_SIZE, - imgName = postContent.thumbnailImage ?: "" - ) - } - userThumbnailImgBitmap = withContext(Dispatchers.IO) { - loadImageBackground( - requestManager = requestManager, - width = HOME_USER_THUMBNAIL_SIZE, - height = HOME_USER_THUMBNAIL_SIZE, - imgName = postContent.userProfileImage ?: "" - ) - } - } else { - postImgBitmap = postContent.preLoadThumbnail?.toBitmap - userThumbnailImgBitmap = postContent.preLoadUserImg?.toBitmap - postContent.preLoadThumbnail = null - postContent.preLoadUserImg = null - } - - try { - loadImageView( - requestManager = requestManager, - width = HOME_POST_THUMBNAIL_SIZE, - height = HOME_POST_THUMBNAIL_SIZE, - img = postImgBitmap!!, - imgView = postImg - ) - } catch (loadException: IllegalStateException) { - loadImageView( - requestManager = requestManager, - width = HOME_POST_THUMBNAIL_SIZE, - height = HOME_POST_THUMBNAIL_SIZE, - imgName = postContent.thumbnailImage ?: "", - imgView = postImg - ) - } catch (imgNullException: NullPointerException) { - loadImageView( - requestManager = requestManager, - width = HOME_POST_THUMBNAIL_SIZE, - height = HOME_POST_THUMBNAIL_SIZE, - imgName = postContent.thumbnailImage ?: "", - imgView = postImg - ) - } - try { - loadImageViewProfile( - requestManager = requestManager, - width = HOME_USER_THUMBNAIL_SIZE, - height = HOME_USER_THUMBNAIL_SIZE, - img = userThumbnailImgBitmap!!, - imgView = userThumbnailImg - ) - } catch (loadException: IllegalStateException) { - loadImageViewProfile( - requestManager = requestManager, - width = HOME_USER_THUMBNAIL_SIZE, - height = HOME_USER_THUMBNAIL_SIZE, - imgName = postContent.userProfileImage ?: "", - imgView = userThumbnailImg - ) - } catch (imgNullException: NullPointerException) { - loadImageViewProfile( - requestManager = requestManager, - width = HOME_USER_THUMBNAIL_SIZE, - height = HOME_USER_THUMBNAIL_SIZE, - imgName = postContent.userProfileImage ?: "", - imgView = userThumbnailImg - ) - } - }.invokeOnCompletion { throwable -> - when (throwable) { - is CancellationException -> Log.e("Image Loading", "CANCELLED") - null -> { - stopShimmer() - } - } - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/HomeNewAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/HomeNewAdapter.kt deleted file mode 100644 index 9ad33f70..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/HomeNewAdapter.kt +++ /dev/null @@ -1,327 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.animation.Animator -import android.graphics.Bitmap -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.databinding.library.baseAdapters.BR -import androidx.navigation.Navigation -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.RequestManager -import daily.dayo.domain.model.Post -import daily.dayo.presentation.R -import daily.dayo.presentation.common.GlideLoadUtil.HOME_POST_THUMBNAIL_SIZE -import daily.dayo.presentation.common.GlideLoadUtil.HOME_USER_THUMBNAIL_SIZE -import daily.dayo.presentation.common.GlideLoadUtil.loadImageBackground -import daily.dayo.presentation.common.GlideLoadUtil.loadImageBackgroundProfile -import daily.dayo.presentation.common.GlideLoadUtil.loadImagePreload -import daily.dayo.presentation.common.GlideLoadUtil.loadImageView -import daily.dayo.presentation.common.GlideLoadUtil.loadImageViewProfile -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.common.toBitmap -import daily.dayo.presentation.databinding.ItemMainPostBinding -import daily.dayo.presentation.fragment.home.HomeFragmentDirections -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class HomeNewAdapter(val rankingShowing: Boolean, private val requestManager: RequestManager) : - ListAdapter(diffCallback) { - companion object { - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Post, newItem: Post) = - oldItem.postId == newItem.postId - - override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean = - oldItem.apply { - preLoadThumbnail = null - preLoadUserImg = null - } == newItem.apply { - preLoadThumbnail = null - preLoadUserImg = null - } - - override fun getChangePayload(oldItem: Post, newItem: Post): Any? { - return if (oldItem.heart != newItem.heart || oldItem.heartCount != newItem.heartCount) true else null - } - } - } - - interface OnItemClickListener { - fun likePostClick(post: Post) - } - - private var clickListener: OnItemClickListener? = null - fun setOnItemClickListener(listener: OnItemClickListener) { - this.clickListener = listener - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeNewViewHolder { - return HomeNewViewHolder( - ItemMainPostBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - - override fun onBindViewHolder( - holder: HomeNewAdapter.HomeNewViewHolder, - position: Int, - payloads: MutableList - ) { - if (payloads.isEmpty()) { - this.onBindViewHolder(holder, position) - } else { - if (payloads[0] == true) { - holder.bindLikeState(getItem(position)) - } - } - } - - override fun onBindViewHolder(holder: HomeNewViewHolder, position: Int) { - val item = getItem(position) - holder.bind(item, position) - preloadFutureLoadingImages(position) - } - - override fun submitList(list: MutableList?) { - super.submitList(list?.let { ArrayList(it) }) - } - - private fun preloadFutureLoadingImages(position: Int) { - if (position <= itemCount) { - val endPosition = if (position + 6 > itemCount) { - itemCount - } else { - position + 6 - } - for (i in position until endPosition) { - loadImagePreload( - requestManager = requestManager, - width = HOME_POST_THUMBNAIL_SIZE, - height = HOME_POST_THUMBNAIL_SIZE, - imgName = getItem(i).thumbnailImage ?: "" - ) - } - } - } - - inner class HomeNewViewHolder( - private val binding: ItemMainPostBinding - ) : RecyclerView.ViewHolder(binding.root) { - lateinit var postContent: Post - - fun bind(postContent: Post, currentPosition: Int) { - this.postContent = postContent - loadImages() - setRanking(currentPosition) - - setBindingSetVariable( - postContent.apply { - binding.lottieMainPostLike.progress = if (heart) 1F else 0F - } - ) - postContent.postId?.let { setRootClickListener(it, postContent.nickname) } - postContent.memberId?.let { setNicknameClickListener(it) } - binding.lottieMainPostLike.setOnDebounceClickListener(300L) { - clickListener?.likePostClick(post = postContent) - } - } - - private fun setBindingSetVariable(post: Post) { - with(binding) { - setVariable(BR.post, post) - executePendingBindings() - } - } - - fun bindLikeState(post: Post) { - setLottieClickListener(post) - setHeartCount(post) - } - - private fun setLottieClickListener(post: Post) { - with(binding.lottieMainPostLike) { - setOnDebounceClickListener(300L) { - clickListener?.likePostClick(post = post) - } - - this.removeAllAnimatorListeners() - this.addAnimatorListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animation: Animator) { - this@with.setOnDebounceClickListener(0L) {} - } - - override fun onAnimationEnd(animation: Animator) { - this@with.setOnDebounceClickListener(0L) { - clickListener?.likePostClick(post = post) - } - } - - override fun onAnimationCancel(animation: Animator) {} - override fun onAnimationRepeat(animation: Animator) {} - }) - - if (post.heart) this.playAnimation() - else this.progress = 0F - } - } - - private fun setHeartCount(post: Post) { - binding.post = post - setBindingSetVariable(post) - } - - private fun setRootClickListener(postId: Int, nickname: String) { - binding.root.setOnDebounceClickListener { - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.HomeFragment, - action = R.id.action_homeFragment_to_postFragment, - args = HomeFragmentDirections.actionHomeFragmentToPostFragment(postId).arguments - ) - } - } - - private fun setNicknameClickListener(memberId: String) { - binding.tvMainPostUserNickname.setOnDebounceClickListener { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.HomeFragment, - action = R.id.action_homeFragment_to_profileFragment, - args = HomeFragmentDirections.actionHomeFragmentToProfileFragment(memberId = memberId).arguments - ) - } - } - - private fun startShimmer() { - with(binding.layoutContentsShimmer) { - startShimmer() - visibility = View.VISIBLE - setShimmerDisplay() - } - } - - private fun stopShimmer() { - with(binding.layoutContentsShimmer) { - stopShimmer() - visibility = View.GONE - setShimmerDisplay() - } - } - - private fun setShimmerDisplay() { - binding.layoutContents.isVisible = !binding.layoutContentsShimmer.isVisible - } - - private fun setRanking(currentPosition: Int) { - with(binding) { - layoutPostRankingNumber.isVisible = !(currentPosition > 3 || !rankingShowing) - tvPostRankingNumber.text = (currentPosition + 1).toString() - } - } - - private fun loadImages() { - startShimmer() - - val postImg = binding.imgMainPost - val userThumbnailImg = binding.imgMainPostUserProfile - - - CoroutineScope(Dispatchers.Main).launch { - val postImgBitmap: Bitmap? - val userThumbnailImgBitmap: Bitmap? - if (postContent.preLoadThumbnail == null) { - postImgBitmap = withContext(Dispatchers.IO) { - loadImageBackground( - requestManager = requestManager, - width = HOME_POST_THUMBNAIL_SIZE, - height = HOME_POST_THUMBNAIL_SIZE, - imgName = postContent.thumbnailImage ?: "" - ) - } - userThumbnailImgBitmap = withContext(Dispatchers.IO) { - loadImageBackgroundProfile( - requestManager = requestManager, - width = HOME_USER_THUMBNAIL_SIZE, - height = HOME_USER_THUMBNAIL_SIZE, - imgName = postContent.userProfileImage ?: "" - ) - } - } else { - postImgBitmap = postContent.preLoadThumbnail?.toBitmap - userThumbnailImgBitmap = postContent.preLoadUserImg?.toBitmap - postContent.preLoadThumbnail = null - postContent.preLoadUserImg = null - } - - try { - loadImageView( - requestManager = requestManager, - width = HOME_POST_THUMBNAIL_SIZE, - height = HOME_POST_THUMBNAIL_SIZE, - img = postImgBitmap!!, - imgView = postImg - ) - } catch (loadException: IllegalStateException) { - loadImageView( - requestManager = requestManager, - width = HOME_POST_THUMBNAIL_SIZE, - height = HOME_POST_THUMBNAIL_SIZE, - imgName = postContent.thumbnailImage ?: "", - imgView = postImg - ) - } catch (imgNullException: NullPointerException) { - loadImageView( - requestManager = requestManager, - width = HOME_POST_THUMBNAIL_SIZE, - height = HOME_POST_THUMBNAIL_SIZE, - imgName = postContent.thumbnailImage ?: "", - imgView = postImg - ) - } - try { - loadImageViewProfile( - requestManager = requestManager, - width = HOME_USER_THUMBNAIL_SIZE, - height = HOME_USER_THUMBNAIL_SIZE, - img = userThumbnailImgBitmap!!, - imgView = userThumbnailImg - ) - } catch (loadException: IllegalStateException) { - loadImageViewProfile( - requestManager = requestManager, - width = HOME_USER_THUMBNAIL_SIZE, - height = HOME_USER_THUMBNAIL_SIZE, - imgName = postContent.userProfileImage ?: "", - imgView = userThumbnailImg - ) - } catch (imgNullException: NullPointerException) { - loadImageViewProfile( - requestManager = requestManager, - width = HOME_USER_THUMBNAIL_SIZE, - height = HOME_USER_THUMBNAIL_SIZE, - imgName = postContent.userProfileImage ?: "", - imgView = userThumbnailImg - ) - } - }.invokeOnCompletion { throwable -> - when (throwable) { - is CancellationException -> Log.e("Image Loading", "CANCELLED") - null -> { - stopShimmer() - } - } - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/NoticeListAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/NoticeListAdapter.kt deleted file mode 100644 index 90d3198d..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/NoticeListAdapter.kt +++ /dev/null @@ -1,51 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.navigation.Navigation -import androidx.paging.PagingDataAdapter -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import daily.dayo.domain.model.Notice -import daily.dayo.presentation.R -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.ItemNoticePostBinding -import daily.dayo.presentation.fragment.setting.notice.NoticeListFragmentDirections - -class NoticeListAdapter : - PagingDataAdapter(diffCallback) { - companion object { - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Notice, newItem: Notice): Boolean { - return oldItem.noticeId == newItem.noticeId - } - - override fun areContentsTheSame(oldItem: Notice, newItem: Notice): Boolean = - oldItem == newItem - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoticeListViewHolder = - NoticeListViewHolder( - ItemNoticePostBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) - - override fun onBindViewHolder(holder: NoticeListAdapter.NoticeListViewHolder, position: Int) { - getItem(position)?.let { holder.bind(it) } - } - - inner class NoticeListViewHolder(private val binding: ItemNoticePostBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind(notice: Notice) { - binding.notice = notice - binding.root.setOnDebounceClickListener { - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.NoticeListFragment, - action = R.id.action_noticeListFragment_to_noticeDetailFragment, - args = NoticeListFragmentDirections.actionNoticeListFragmentToNoticeDetailFragment(notice).arguments - ) - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/NotificationListAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/NotificationListAdapter.kt deleted file mode 100644 index bce707d0..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/NotificationListAdapter.kt +++ /dev/null @@ -1,154 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.graphics.Typeface -import android.text.SpannableStringBuilder -import android.text.Spanned -import android.text.TextPaint -import android.text.method.LinkMovementMethod -import android.text.style.ClickableSpan -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.navigation.Navigation -import androidx.paging.PagingDataAdapter -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.RequestManager -import daily.dayo.domain.model.Notification -import daily.dayo.domain.model.Topic -import daily.dayo.presentation.R -import daily.dayo.presentation.common.GlideLoadUtil.loadImageView -import daily.dayo.presentation.common.TimeChangerUtil.timeChange -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.ItemNotificationBinding -import daily.dayo.presentation.fragment.notification.NotificationFragmentDirections - -class NotificationListAdapter(private val requestManager: RequestManager) : - PagingDataAdapter( - diffCallback - ) { - companion object { - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Notification, newItem: Notification) = - oldItem.alarmId == newItem.alarmId - - override fun areContentsTheSame(oldItem: Notification, newItem: Notification): Boolean = - oldItem == newItem - } - } - - interface OnItemClickListener { - fun notificationItemClick(alarmId: Int, alarmCheck: Boolean, position: Int) - } - - private var listener: OnItemClickListener? = null - fun setOnItemClickListener(listener: OnItemClickListener) { - this.listener = listener - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationListViewHolder { - return NotificationListViewHolder( - ItemNotificationBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) - } - - override fun onBindViewHolder(holder: NotificationListViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - inner class NotificationListViewHolder(private val binding: ItemNotificationBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind(notification: Notification?) { - binding.notification = notification - notification?.createdTime?.let { time -> - binding.createdTime = timeChange(context = binding.tvNotificationTime.context, time = time) - } - - notification?.image?.let { notificationImage -> - val layoutParams = ViewGroup.MarginLayoutParams( - ViewGroup.MarginLayoutParams.MATCH_PARENT, - ViewGroup.MarginLayoutParams.MATCH_PARENT - ) - loadImageView( - requestManager = requestManager, - width = layoutParams.width, - height = layoutParams.width, - imgName = notificationImage, - imgView = binding.ivNotificationThumbnail - ) - } - - val spanContent = SpannableStringBuilder() - spanContent.append(notification?.nickname) - spanContent.setSpan( - object : ClickableSpan() { - override fun onClick(v: View) { - Navigation.findNavController(v).navigateSafe( - currentDestinationId = R.id.NotificationFragment, - action = R.id.action_notificationFragment_to_profileFragment, - args = NotificationFragmentDirections.actionNotificationFragmentToProfileFragment( - memberId = notification?.memberId - ).arguments - ) - } - - override fun updateDrawState(textPaint: TextPaint) { - textPaint.color = ContextCompat.getColor( - binding.tvNotificationTitle.context, - R.color.gray_1_313131 - ) - textPaint.isUnderlineText = false - textPaint.typeface = Typeface.DEFAULT_BOLD - } - }, 0, notification?.nickname?.length ?: 0, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) - spanContent.append(notification?.content) - binding.tvNotificationTitle.movementMethod = LinkMovementMethod.getInstance() - binding.tvNotificationTitle.text = spanContent - - binding.root.setOnDebounceClickListener { - // alarm check - notification?.alarmId?.let { alarmId -> - listener?.notificationItemClick( - alarmId = alarmId, - alarmCheck = notification.check!!, - position = position - ) - } - - // move to alarm content - when (notification?.topic) { - Topic.COMMENT, Topic.HEART -> { - notification.postId?.let { postId -> - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.NotificationFragment, - action = R.id.action_notificationFragment_to_postFragment, - args = NotificationFragmentDirections.actionNotificationFragmentToPostFragment( - postId = postId - ).arguments - ) - } - } - - Topic.FOLLOW -> - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.NotificationFragment, - action = R.id.action_notificationFragment_to_profileFragment, - args = NotificationFragmentDirections.actionNotificationFragmentToProfileFragment( - memberId = notification.memberId - ).arguments - ) - - Topic.NOTICE -> { - - } - - else -> {} - } - } - } - - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/OnBoardingPagerStateAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/OnBoardingPagerStateAdapter.kt deleted file mode 100644 index 140321ba..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/OnBoardingPagerStateAdapter.kt +++ /dev/null @@ -1,24 +0,0 @@ -package daily.dayo.presentation.adapter - -import androidx.annotation.NonNull -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.lifecycle.Lifecycle -import androidx.viewpager2.adapter.FragmentStateAdapter - -class OnBoardingPagerStateAdapter(@NonNull fragmentManager: FragmentManager, @NonNull lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) { - var fragments: ArrayList = ArrayList() - - override fun getItemCount(): Int { - return fragments.size - } - - override fun createFragment(position: Int): Fragment { - return fragments[position] - } - - fun addFragment(fragment: Fragment) { - fragments.add(fragment) - notifyItemInserted(fragments.size - 1) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/WriteFolderAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/WriteFolderAdapter.kt deleted file mode 100644 index acb1c8bf..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/WriteFolderAdapter.kt +++ /dev/null @@ -1,59 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.graphics.Typeface -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.ItemFolderListBinding -import daily.dayo.domain.model.Folder - -class WriteFolderAdapter( - private val onFolderClicked: (Folder) -> Unit, - private val selectedFolderId: String -) : ListAdapter(diffCallback) { - - companion object { - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Folder, newItem: Folder) = - oldItem.folderId == newItem.folderId - - override fun areContentsTheSame(oldItem: Folder, newItem: Folder): Boolean = - oldItem == newItem - } - } - - override fun submitList(list: List?) { - super.submitList(list) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WriteFolderViewHolder { - return WriteFolderViewHolder( - ItemFolderListBinding.inflate(LayoutInflater.from(parent.context), parent, false), - onFolderClicked - ) - } - - override fun onBindViewHolder(holder: WriteFolderViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - inner class WriteFolderViewHolder( - private val binding: ItemFolderListBinding, - val onFolderClicked: (Folder) -> Unit - ) : RecyclerView.ViewHolder(binding.root) { - fun bind(folder: Folder) { - binding.folder = folder - binding.isChangeEnable = false - if (selectedFolderId != "" && selectedFolderId.toInt() == folder.folderId) - binding.tvFolderName.setTypeface(null, Typeface.BOLD) - else binding.tvFolderName.setTypeface(null, Typeface.NORMAL) - - binding.root.setOnDebounceClickListener { - onFolderClicked(folder) - } - } - } -} diff --git a/presentation/src/main/java/daily/dayo/presentation/adapter/WriteUploadImageListAdapter.kt b/presentation/src/main/java/daily/dayo/presentation/adapter/WriteUploadImageListAdapter.kt deleted file mode 100644 index 96b1db6f..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/adapter/WriteUploadImageListAdapter.kt +++ /dev/null @@ -1,115 +0,0 @@ -package daily.dayo.presentation.adapter - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.ItemWritePostUploadImageBinding -import daily.dayo.presentation.databinding.ItemWritePostUploadImageMenuBinding - -class WriteUploadImageListAdapter( - private val requestManager: RequestManager, - private val postId: Int -) : ListAdapter(diffCallback) { - companion object { - private const val MENU_ITEM_VIEW_TYPE = 1 - private const val IMAGE_ITEM_VIEW_TYPE = 2 - - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: String, newItem: String) = - oldItem == newItem - - override fun areContentsTheSame(oldItem: String, newItem: String): Boolean = - oldItem == newItem - } - } - - interface OnItemClickListener { - fun deleteUploadImageClick(pos: Int) - fun addUploadImageClick(pos: Int) - } - - private var listener: OnItemClickListener? = null - fun setOnItemClickListener(listener: OnItemClickListener) { - this.listener = listener - } - - override fun getItemViewType(position: Int): Int { - return if (position == 0 && getItem(position) == "") MENU_ITEM_VIEW_TYPE else IMAGE_ITEM_VIEW_TYPE - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is WriteUploadImageMenuViewHolder -> { - holder.bind() - } - is WriteUploadImageListViewHolder -> { - holder.bind(getItem(position)) - } - else -> {} - } - } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): RecyclerView.ViewHolder { - return when (viewType) { - MENU_ITEM_VIEW_TYPE -> { - WriteUploadImageMenuViewHolder( - ItemWritePostUploadImageMenuBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - else -> { - WriteUploadImageListViewHolder( - ItemWritePostUploadImageBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - } - } - - override fun submitList(list: List?) { - super.submitList(list?.let { ArrayList(it) }) - } - - inner class WriteUploadImageListViewHolder(private val binding: ItemWritePostUploadImageBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind(item: String) { - with(binding.imgUpload) { - clipToOutline = true - requestManager.load(item).centerCrop().into(this) - } - - with(binding.btnImgUploadDelete) { - isClickable = postId == 0 - visibility = if (postId == 0) View.VISIBLE else View.INVISIBLE - if (postId == 0) { - setOnDebounceClickListener { - listener?.deleteUploadImageClick(pos = bindingAdapterPosition) - } - } - } - } - } - - inner class WriteUploadImageMenuViewHolder(private val binding: ItemWritePostUploadImageMenuBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind() { - binding.root.setOnDebounceClickListener { - listener?.addUploadImageClick(pos = bindingAdapterPosition) - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/common/Permissions.kt b/presentation/src/main/java/daily/dayo/presentation/common/Permissions.kt new file mode 100644 index 00000000..667c9027 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/common/Permissions.kt @@ -0,0 +1,29 @@ +package daily.dayo.presentation.common + +import android.Manifest +import android.os.Build + +val PERMISSIONS_CAMERA = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO + ) +} else { + arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) +} +val PERMISSIONS_GALLERY = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arrayOf( + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO + ) +} else { + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/common/constant/FolderConstants.kt b/presentation/src/main/java/daily/dayo/presentation/common/constant/FolderConstants.kt new file mode 100644 index 00000000..6bce9afd --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/common/constant/FolderConstants.kt @@ -0,0 +1,10 @@ +package daily.dayo.presentation.common.constant + +object FolderConstants { + const val FOLDER_AD_START_COUNT = 3 + const val MAX_FOLDER_COUNT = 5 + const val FOLDER_THUMBNAIL_SIZE = 72 + const val FOLDER_THUMBNAIL_RADIUS_SIZE = 12 + const val FOLDER_NAME_MAX_LENGTH = 12 + const val FOLDER_DESCRIPTION_MAX_LENGTH = 20 +} diff --git a/presentation/src/main/java/daily/dayo/presentation/common/extension/List.kt b/presentation/src/main/java/daily/dayo/presentation/common/extension/List.kt new file mode 100644 index 00000000..785fdf1a --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/common/extension/List.kt @@ -0,0 +1,3 @@ +package daily.dayo.presentation.common.extension + +fun List.limitTo(limit: Int): List = this.take(limit) \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/common/extension/ModifierExtension.kt b/presentation/src/main/java/daily/dayo/presentation/common/extension/ModifierExtension.kt index d5653a12..100278b1 100644 --- a/presentation/src/main/java/daily/dayo/presentation/common/extension/ModifierExtension.kt +++ b/presentation/src/main/java/daily/dayo/presentation/common/extension/ModifierExtension.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.semantics.Role internal interface MultipleEventsCutter { fun processEvent(event: () -> Unit) + companion object } @@ -56,13 +57,14 @@ fun Modifier.clickableSingle( interactionSource = remember { MutableInteractionSource() } ) } + fun Modifier.clickableSingle( enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, - onClick: () -> Unit, indication: Indication?, - interactionSource: MutableInteractionSource + interactionSource: MutableInteractionSource, + onClick: () -> Unit ) = composed( inspectorInfo = debugInspectorInfo { name = "clickable" diff --git a/presentation/src/main/java/daily/dayo/presentation/common/extension/String.kt b/presentation/src/main/java/daily/dayo/presentation/common/extension/String.kt new file mode 100644 index 00000000..46ad4e71 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/common/extension/String.kt @@ -0,0 +1,5 @@ +package daily.dayo.presentation.common.extension + +fun String.limitTo(limit: Int): String { + return if (this.length > limit) this.substring(0, limit) else this +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/common/image/ImageOrientationUtil.kt b/presentation/src/main/java/daily/dayo/presentation/common/image/ImageOrientationUtil.kt new file mode 100644 index 00000000..d2db552f --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/common/image/ImageOrientationUtil.kt @@ -0,0 +1,42 @@ +package daily.dayo.presentation.common.image + +import android.content.ContentResolver +import android.graphics.Bitmap +import android.graphics.Matrix +import android.net.Uri +import androidx.exifinterface.media.ExifInterface + +data class ExifInfo( + val orientation: Int, + val rotationDegrees: Int, + val flipX: Boolean, + val flipY: Boolean +) + +fun ContentResolver.readExifInfo(uri: Uri): ExifInfo { + openFileDescriptor(uri, "r")?.use { pfd -> + val exif = ExifInterface(pfd.fileDescriptor) + val o = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + return when (o) { + ExifInterface.ORIENTATION_ROTATE_90 -> ExifInfo(o, 90, false, false) + ExifInterface.ORIENTATION_ROTATE_180 -> ExifInfo(o, 180, false, false) + ExifInterface.ORIENTATION_ROTATE_270 -> ExifInfo(o, 270, false, false) + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> ExifInfo(o, 0, true, false) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> ExifInfo(o, 0, false, true) + ExifInterface.ORIENTATION_TRANSPOSE -> ExifInfo(o, 90, true, false) // flipH + rot90 + ExifInterface.ORIENTATION_TRANSVERSE -> ExifInfo(o, 270, true, false) // flipH + rot270 + else -> ExifInfo(ExifInterface.ORIENTATION_NORMAL, 0, false, false) + } + } + return ExifInfo(ExifInterface.ORIENTATION_UNDEFINED, 0, false, false) +} + +fun Bitmap.applyExif(exif: ExifInfo): Bitmap { + if (exif.rotationDegrees == 0 && !exif.flipX && !exif.flipY) return this + val m = Matrix() + // flip ๋จผ์ € + if (exif.flipX || exif.flipY) m.preScale(if (exif.flipX) -1f else 1f, if (exif.flipY) -1f else 1f) + // ํšŒ์ „ + if (exif.rotationDegrees != 0) m.postRotate(exif.rotationDegrees.toFloat()) + return Bitmap.createBitmap(this, 0, 0, width, height, m, true) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/common/image/ImageSamplingUtils.kt b/presentation/src/main/java/daily/dayo/presentation/common/image/ImageSamplingUtils.kt new file mode 100644 index 00000000..53ead678 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/common/image/ImageSamplingUtils.kt @@ -0,0 +1,57 @@ +package daily.dayo.presentation.common.image + +import android.content.ContentResolver +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri + +/** + * OOM ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด URI์™€ ๋ชฉํ‘œ ์‚ฌ์ด์ฆˆ๋ฅผ ๋ฐ›์•„ ์ตœ์ ํ™”๋œ ๋น„ํŠธ๋งต์„ ๋กœ๋“œํ•˜๋Š” ํ•จ์ˆ˜ + **/ +fun decodeSampledBitmapFromUri( + contentResolver: ContentResolver, + uri: Uri, + reqWidth: Int, + reqHeight: Int +): Bitmap? { + // inJustDecodeBounds = true ๋กœ ์„ค์ •ํ•ด ์ด๋ฏธ์ง€์˜ ์‹ค์ œ ํฌ๊ธฐ๋งŒ ํ™•์ธ (๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น X) + val options = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, options) } + // ์ตœ์ ์˜ inSampleSize ๊ณ„์‚ฐ + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) + + // ๊ณ„์‚ฐ๋œ inSampleSize๋กœ ์‹ค์ œ ๋น„ํŠธ๋งต ๋””์ฝ”๋”ฉ (๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น O) + options.inJustDecodeBounds = false + val bitmap = contentResolver.openInputStream(uri) + ?.use { BitmapFactory.decodeStream(it, null, options) } + + // EXIF ์ •๋ณด๋ฅผ ์ฝ์–ด์„œ ์ด๋ฏธ์ง€๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํšŒ์ „ + return bitmap?.let { bmp -> + val exifInfo = contentResolver.readExifInfo(uri) + bmp.applyExif(exifInfo) + } +} + +/** + * ์ตœ์ ์˜ inSampleSize ๊ฐ’์„ ๊ณ„์‚ฐํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜ + */ +private fun calculateInSampleSize( + options: BitmapFactory.Options, + reqWidth: Int, + reqHeight: Int +): Int { + val (height: Int, width: Int) = options.run { outHeight to outWidth } + var inSampleSize = 1 + + if (height > reqHeight || width > reqWidth) { + val halfHeight: Int = height / 2 + val halfWidth: Int = width / 2 + // requested size๋ณด๋‹ค ํฌ๊ธฐ๊ฐ€ ์ž‘์•„์ง€์ง€ ์•Š๋Š” ๊ฐ€์žฅ ํฐ inSampleSize ๊ฐ’์„ ์ฐพ๋Š”๋‹ค. (2์˜ ๊ฑฐ๋“ญ์ œ๊ณฑ) + while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { + inSampleSize *= 2 + } + } + return inSampleSize +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/common/image/ImageUploadUtil.kt b/presentation/src/main/java/daily/dayo/presentation/common/image/ImageUploadUtil.kt index 091e5b3c..fd4bb192 100644 --- a/presentation/src/main/java/daily/dayo/presentation/common/image/ImageUploadUtil.kt +++ b/presentation/src/main/java/daily/dayo/presentation/common/image/ImageUploadUtil.kt @@ -1,7 +1,16 @@ package daily.dayo.presentation.common.image +import android.Manifest import android.content.ContentResolver +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.Bitmap import android.net.Uri +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.core.content.ContextCompat object ImageUploadUtil { private val PERMIT_IMAGE_EXTENSIONS = listOf("jpg", "jpeg", "png", "webp") @@ -13,4 +22,93 @@ object ImageUploadUtil { .split("/")[1] val String.isPermitExtension: Boolean get() = this in PERMIT_IMAGE_EXTENSIONS +} + +@Composable +fun launchGallery( + context: Context, + onImageSelected: (Uri?) -> Unit, + onPermissionDenied: () -> Unit +): () -> Unit { + val galleryLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.GetContent() + ) { uri -> + onImageSelected(uri) + } + + val imagePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_IMAGES + } else { + Manifest.permission.WRITE_EXTERNAL_STORAGE + } + + val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + galleryLauncher.launch("image/*") + } else { + onPermissionDenied() + } + } + + val openGallery: () -> Unit = { + val hasPermission = ContextCompat.checkSelfPermission( + context, + imagePermission + ) == PackageManager.PERMISSION_GRANTED + + if (hasPermission) { + galleryLauncher.launch("image/*") + } else { + permissionLauncher.launch(imagePermission) + } + } + + return openGallery +} + +@Composable +fun launchCamera( + context: Context, + onImageCaptured: (Bitmap?) -> Unit, + onPermissionDenied: () -> Unit +): () -> Unit { + // 1) ์นด๋ฉ”๋ผ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์„ Launcher + val cameraLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.TakePicturePreview() + ) { bitmap: Bitmap? -> + onImageCaptured(bitmap) + } + + val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + // ๊ถŒํ•œ ํ—ˆ์šฉ ์‹œ ์นด๋ฉ”๋ผ ์‹คํ–‰ + cameraLauncher.launch(null) + } else { + onPermissionDenied() + } + } + + // 3) ์™ธ๋ถ€์—์„œ ์ด ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด, + // - ๊ถŒํ•œ์ด ์—†์œผ๋ฉด ์š”์ฒญ, + // - ๊ถŒํ•œ์ด ์žˆ์œผ๋ฉด ์นด๋ฉ”๋ผ ์‹คํ–‰ + val openCamera: () -> Unit = { + val hasPermission = ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + + if (hasPermission) { + // ์ด๋ฏธ ๊ถŒํ•œ์ด ์žˆ์œผ๋ฉด ๊ณง๋ฐ”๋กœ ์นด๋ฉ”๋ผ ์‹คํ–‰ + cameraLauncher.launch(null) + } else { + // ๊ถŒํ•œ ์—†์œผ๋ฉด ์š”์ฒญ + permissionLauncher.launch(Manifest.permission.CAMERA) + } + } + + return openCamera } \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordCheckEmailCertificateFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordCheckEmailCertificateFragment.kt deleted file mode 100644 index e7b65aca..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordCheckEmailCertificateFragment.kt +++ /dev/null @@ -1,296 +0,0 @@ -package daily.dayo.presentation.fragment.account.findAccount - -import android.os.Bundle -import android.os.CountDownTimer -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.SetTextInputLayout -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFindAccountPasswordCheckEmailCertificateBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import java.util.regex.Pattern - -@AndroidEntryPoint -class FindAccountPasswordCheckEmailCertificateFragment : Fragment() { - private var binding by autoCleared { - currentCountDownTimer?.cancel() - currentCountDownTimer = null - } - private val loginViewModel by activityViewModels() - private val args by navArgs() - private var currentCountDownTimer: CountDownTimer? = null - private var isCountDownDone: Boolean = false - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFindAccountPasswordCheckEmailCertificateBinding.inflate( - inflater, - container, - false - ) - setBackClickListener() - setNextClickListener() - initEditText() - setLimitEditTextInputType() - setTextEditorActionListener() - setEmailResendClickListener() - certificateEmail() - setUserAddressEditText() - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.setOnTouchListener { _, _ -> - HideKeyBoardUtil.hide( - requireContext(), - binding.etLoginEmailFindPasswordCertificateUserInput - ) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput.text.isNullOrEmpty() - ) - true - } - } - - private fun setTextEditorActionListener() { - binding.etLoginEmailFindPasswordCertificateUserInput.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide( - requireContext(), - binding.etLoginEmailFindPasswordCertificateUserInput - ) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput.text.isNullOrEmpty() - ) - true - } - - else -> false - } - } - } - - private fun initEditText() { - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput.text.isNullOrEmpty() - ) - binding.etLoginEmailFindPasswordCertificateUserInput.setOnFocusChangeListener { _, hasFocus -> - with(binding.layoutLoginEmailFindPasswordCertificateUserInput) { - if (hasFocus) { - hint = getString(R.string.email_address_certification) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput, - false - ) - } else { - hint = getString(R.string.email_address_certificate_title) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput, - true - ) - } - } - } - currentCountDownTimer = countDownTimer - currentCountDownTimer?.start() - } - - private fun changeEditTextTitle() { - with(binding.layoutLoginEmailFindPasswordCertificateUserInput) { - if (binding.etLoginEmailFindPasswordCertificateUserInput.text.isNullOrEmpty()) { - hint = getString(R.string.email_address_certificate_title) - } else { - hint = getString(R.string.email_address_certification) - } - } - } - - private fun certificateEmail() { - // ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ๋‹ด๊ธด ์ด๋ฉ”์ผ ์ตœ์ดˆ ๋ฐœ์†ก - loginViewModel.requestCheckEmailAuth(args.email) - - binding.etLoginEmailFindPasswordCertificateUserInput.addTextChangedListener(object : - TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (!binding.etLoginEmailFindPasswordCertificateUserInput.text.isNullOrEmpty() && !isCountDownDone) { - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnLoginEmailFindPasswordCertificateNext - ) - } else { - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnLoginEmailFindPasswordCertificateNext - ) - } - } - }) - binding.btnLoginEmailFindPasswordCertificateNext.setOnDebounceClickListener { - if (binding.etLoginEmailFindPasswordCertificateUserInput.text.toString() == loginViewModel.certificateEmailAuthCode.value) { - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnLoginEmailFindPasswordCertificateNext - ) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput, - null, - true - ) - currentCountDownTimer?.cancel() - currentCountDownTimer = null - binding.tvLoginEmailFindPasswordCertificateResend.isEnabled = false - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.FindAccountPasswordCheckEmailCertificateFragment, - action = R.id.action_findAccountPasswordCheckEmailCertificateFragment_to_findAccountPasswordNewPasswordFragment, - args = FindAccountPasswordCheckEmailCertificateFragmentDirections - .actionFindAccountPasswordCheckEmailCertificateFragmentToFindAccountPasswordNewPasswordFragment(args.email).arguments - ) - } else { - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnLoginEmailFindPasswordCertificateNext - ) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput, - getString(R.string.email_address_certificate_alert_message_match_fail), - false - ) - Toast.makeText( - requireContext(), - getString(R.string.email_address_certificate_alert_message_match_fail_toast), - Toast.LENGTH_SHORT - ).show() - } - } - } - - private fun setLimitEditTextInputType() { - // InputFilter๋กœ ๋„์–ด์“ฐ๊ธฐ ์ž…๋ ฅ๋งŒ ๋ง‰๊ณ  ๋‚˜๋จธ์ง€๋Š” ์•ˆ๋‚ด๋ฉ”์‹œ์ง€๋กœ ๋„์›Œ์ง€๋„๋ก ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ๋„ - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - binding.etLoginEmailFindPasswordCertificateUserInput.filters = arrayOf(filterInputCheck) - } - - private val countDownTimer = object : CountDownTimer((1000 * 60 * 3).toLong(), 1000) { - override fun onTick(millisUntilFinished: Long) { - isCountDownDone = false - var remainMinutes = (millisUntilFinished / 1000) / 60 - var remainSeconds = millisUntilFinished / 1000 % 60 - binding.tvLoginEmailFindPasswordCertificateTimer.text = - "${"%02d".format(remainMinutes)}:${"%02d".format(remainSeconds)}" - } - - override fun onFinish() { - isCountDownDone = true - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnLoginEmailFindPasswordCertificateNext - ) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput, - getString(R.string.email_address_certificate_alert_message_time_fail), - false - ) - } - } - - private fun setEmailResendClickListener() { - binding.tvLoginEmailFindPasswordCertificateResend.setOnDebounceClickListener { - HideKeyBoardUtil.hide( - requireContext(), - binding.etLoginEmailFindPasswordCertificateUserInput - ) // 1. ํ‚ค๋ณด๋“œ๊ฐ€ ์˜ฌ๋ผ์™€ ์žˆ๋Š” ๊ฒฝ์šฐ ํ‚ค๋ณด๋“œ๊ฐ€ ๋‚ด๋ ค๊ฐ - binding.etLoginEmailFindPasswordCertificateUserInput.setText("") // 2. ์ธ์ฆ๋ฒˆํ˜ธ ์ž…๋ ฅ์ฐฝ ์ดˆ๊ธฐํ™” - currentCountDownTimer?.start() // 3. ์ œํ•œ์‹œ๊ฐ„ ์ดˆ๊ธฐํ™” - - changeEditTextTitle() - binding.layoutLoginEmailFindPasswordCertificateUserInput.isErrorEnabled = false - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutLoginEmailFindPasswordCertificateUserInput, - binding.etLoginEmailFindPasswordCertificateUserInput, - true - ) - - loginViewModel.requestCertificateEmail(args.email) - Toast.makeText( - requireContext(), - R.string.email_address_certificate_alert_message_resend, - Toast.LENGTH_SHORT - ).show() // 4. ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ๋ณด์—ฌ์ง - } - } - - private fun setUserAddressEditText() { - binding.email = args.email - } - - private fun setBackClickListener() { - binding.btnLoginEmailFindPasswordCertificateBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnLoginEmailFindPasswordCertificateNext.setOnDebounceClickListener { - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.FindAccountPasswordCheckEmailCertificateFragment, - action = R.id.action_findAccountPasswordCheckEmailCertificateFragment_to_findAccountPasswordNewPasswordFragment, - args = FindAccountPasswordCheckEmailCertificateFragmentDirections.actionFindAccountPasswordCheckEmailCertificateFragmentToFindAccountPasswordNewPasswordFragment( - trimBlankText(binding.etLoginEmailFindPasswordCertificateUserInput.text) - ).arguments - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordCheckEmailFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordCheckEmailFragment.kt deleted file mode 100644 index d9e68970..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordCheckEmailFragment.kt +++ /dev/null @@ -1,232 +0,0 @@ -package daily.dayo.presentation.fragment.account.findAccount - -import android.os.Bundle -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.util.Patterns -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.SetTextInputLayout -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFindAccountPasswordCheckEmailBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import java.util.regex.Pattern - -@AndroidEntryPoint -class FindAccountPasswordCheckEmailFragment : Fragment() { - private var binding by autoCleared() - private val loginViewModel by activityViewModels() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFindAccountPasswordCheckEmailBinding.inflate(inflater, container, false) - setBackClickListener() - setNextClickListener() - initEditText() - setLimitEditTextInputType() - setTextEditorActionListener() - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.setOnTouchListener { _, _ -> - with(binding.etFindAccountPasswordCheckEmailUserInput) { - HideKeyBoardUtil.hide(requireContext(), this) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordCheckEmailUserInput, - this, - this.text.isNullOrEmpty() - ) - } - - verifyEmailAddress() - true - } - } - - private fun setTextEditorActionListener() { - with(binding.etFindAccountPasswordCheckEmailUserInput) { - this.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide(requireContext(), this) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordCheckEmailUserInput, - this, - this.text.isNullOrEmpty() - ) - verifyEmailAddress() - true - } - - else -> false - } - } - } - } - - private fun initEditText() { - with(binding.etFindAccountPasswordCheckEmailUserInput) { - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordCheckEmailUserInput, - this, - this.text.isNullOrEmpty() - ) - - setOnFocusChangeListener { _, hasFocus -> // Title - with(binding.layoutFindAccountPasswordCheckEmailUserInput) { - if (hasFocus) { - hint = getString(R.string.email) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordCheckEmailUserInput, - binding.etFindAccountPasswordCheckEmailUserInput, - false - ) - } else { - hint = getString(R.string.email_address_example) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordCheckEmailUserInput, - binding.etFindAccountPasswordCheckEmailUserInput, - true - ) - } - } - } - addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - with(binding.layoutFindAccountPasswordCheckEmailUserInput) { - isErrorEnabled = false - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordCheckEmailUserInput, - binding.etFindAccountPasswordCheckEmailUserInput, - false - ) - } - } - }) - } - } - - private fun changeEditTextTitle() { - with(binding.layoutFindAccountPasswordCheckEmailUserInput) { - if (binding.etFindAccountPasswordCheckEmailUserInput.text.isNullOrEmpty()) { - hint = getString(R.string.email_address_example) - } else { - hint = getString(R.string.email) - } - } - } - - private fun verifyEmailAddress() { - if (Patterns.EMAIL_ADDRESS.matcher( - trimBlankText(binding.etFindAccountPasswordCheckEmailUserInput.text) - ).matches() - ) { // ์ด๋ฉ”์ผ ์ฃผ์†Œ ์–‘์‹ ๊ฒ€์‚ฌ - loginViewModel.requestCheckEmail( - inputEmail = trimBlankText(binding.etFindAccountPasswordCheckEmailUserInput.text) - ) - loginViewModel.checkEmailSuccess.observe(viewLifecycleOwner) { isSuccess -> - if (isSuccess) { // ์ด๋ฉ”์ผ ๊ฐ€์ž…๊ณผ ๋‹ฌ๋ฆฌ ์ค‘๋ณต๊ฒ€์‚ฌํ•ด์„œ false ๊ฒฝ์šฐ์—๋Š” ์žˆ๋Š” ์ด๋ฉ”์ผ ์ด๋ฏ€๋กœ ์„ฑ๊ณต ์ฒ˜๋ฆฌ - SetTextInputLayout.setEditTextErrorThemeWithIcon( - requireContext(), - binding.layoutFindAccountPasswordCheckEmailUserInput, - binding.etFindAccountPasswordCheckEmailUserInput, - null, - true - ) - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnFindAccountPasswordCheckEmailNext - ) - } else { - SetTextInputLayout.setEditTextErrorThemeWithIcon( - requireContext(), - binding.layoutFindAccountPasswordCheckEmailUserInput, - binding.etFindAccountPasswordCheckEmailUserInput, - getString(R.string.find_account_password_check_email_message_exist_fail), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnFindAccountPasswordCheckEmailNext - ) - } - } - } else { - SetTextInputLayout.setEditTextErrorThemeWithIcon( - requireContext(), - binding.layoutFindAccountPasswordCheckEmailUserInput, - binding.etFindAccountPasswordCheckEmailUserInput, - getString(R.string.signup_email_set_email_address_message_format_fail), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnFindAccountPasswordCheckEmailNext - ) - } - } - - private fun setLimitEditTextInputType() { - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - binding.etFindAccountPasswordCheckEmailUserInput.filters = arrayOf(filterInputCheck) - } - - private fun setBackClickListener() { - binding.btnFindAccountPasswordCheckEmailBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnFindAccountPasswordCheckEmailNext.setOnDebounceClickListener { - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.FindAccountPasswordCheckEmailFragment, - action = R.id.action_findAccountPasswordCheckEmailFragment_to_findAccountPasswordCheckEmailCertificateFragment, - args = FindAccountPasswordCheckEmailFragmentDirections.actionFindAccountPasswordCheckEmailFragmentToFindAccountPasswordCheckEmailCertificateFragment( - trimBlankText(binding.etFindAccountPasswordCheckEmailUserInput.text) - ).arguments - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordCompleteFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordCompleteFragment.kt deleted file mode 100644 index 75570d50..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordCompleteFragment.kt +++ /dev/null @@ -1,41 +0,0 @@ -package daily.dayo.presentation.fragment.account.findAccount - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.R -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFindAccountPasswordCompleteBinding - -class FindAccountPasswordCompleteFragment : Fragment() { - private var binding by autoCleared() - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFindAccountPasswordCompleteBinding.inflate(inflater, container, false) - setBackClickListener() - setNextClickListener() - return binding.root - } - - private fun setBackClickListener() { - binding.btnFindAccountPasswordCompleteBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnFindAccountPasswordCompleteNext.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.FindAccountPasswordCompleteFragment, - action = R.id.action_findAccountPasswordCompleteFragment_to_loginEmailFragment - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordNewPasswordConfirmationFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordNewPasswordConfirmationFragment.kt deleted file mode 100644 index afa2a7a0..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordNewPasswordConfirmationFragment.kt +++ /dev/null @@ -1,219 +0,0 @@ -package daily.dayo.presentation.fragment.account.findAccount - -import android.app.AlertDialog -import android.os.Bundle -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.SetTextInputLayout -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFindAccountPasswordNewPasswordConfirmationBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import java.util.regex.Pattern - -@AndroidEntryPoint -class FindAccountPasswordNewPasswordConfirmationFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val args by navArgs() - private val loginViewModel by activityViewModels() - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFindAccountPasswordNewPasswordConfirmationBinding.inflate( - inflater, - container, - false - ) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - setBackClickListener() - setNextClickListener() - initEditText() - setLimitEditTextInputType() - setTextEditorActionListener() - verifyPassword() - setUserPasswordEditText() - observeChangePasswordSuccess() - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.setOnTouchListener { _, _ -> - HideKeyBoardUtil.hide( - requireContext(), - binding.etFindAccountPasswordNewPasswordConfirmationUserInput - ) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordConfirmationUserInput, - binding.etFindAccountPasswordNewPasswordConfirmationUserInput, - binding.etFindAccountPasswordNewPasswordConfirmationUserInput.text.isNullOrEmpty() - ) - true - } - } - - private fun setTextEditorActionListener() { - binding.etFindAccountPasswordNewPasswordConfirmationUserInput.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide( - requireContext(), - binding.etFindAccountPasswordNewPasswordConfirmationUserInput - ) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordConfirmationUserInput, - binding.etFindAccountPasswordNewPasswordConfirmationUserInput, - binding.etFindAccountPasswordNewPasswordConfirmationUserInput.text.isNullOrEmpty() - ) - true - } - - else -> false - } - } - } - - private fun initEditText() { - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordConfirmationUserInput, - binding.etFindAccountPasswordNewPasswordConfirmationUserInput, - binding.etFindAccountPasswordNewPasswordConfirmationUserInput.text.isNullOrEmpty() - ) - binding.etFindAccountPasswordNewPasswordConfirmationUserInput.setOnFocusChangeListener { _, hasFocus -> - with(binding.layoutFindAccountPasswordNewPasswordConfirmationUserInput) { - if (hasFocus) { - hint = getString(R.string.password_confirmation) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordConfirmationUserInput, - binding.etFindAccountPasswordNewPasswordConfirmationUserInput, - false - ) - } else { - hint = - getString(R.string.signup_email_set_password_confirmation_edittext_hint) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordConfirmationUserInput, - binding.etFindAccountPasswordNewPasswordConfirmationUserInput, - true - ) - } - } - } - } - - private fun changeEditTextTitle() { - with(binding.layoutFindAccountPasswordNewPasswordConfirmationUserInput) { - if (binding.etFindAccountPasswordNewPasswordConfirmationUserInput.text.isNullOrEmpty()) { - hint = - getString(R.string.signup_email_set_password_confirmation_edittext_hint) - } else { - hint = getString(R.string.password_confirmation) - } - } - } - - private fun verifyPassword() { - binding.etFindAccountPasswordNewPasswordConfirmationUserInput.addTextChangedListener(object : - TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (args.password != trimBlankText(binding.etFindAccountPasswordNewPasswordConfirmationUserInput.text) - ) { // ๋™์ผ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์‚ฌ - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnFindAccountPasswordNewPasswordConfirmationNext - ) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordConfirmationUserInput, - binding.etFindAccountPasswordNewPasswordConfirmationUserInput, - getString(R.string.signup_email_set_password_confirmation_message_fail), - false - ) - } else { - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnFindAccountPasswordNewPasswordConfirmationNext - ) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordConfirmationUserInput, - binding.etFindAccountPasswordNewPasswordConfirmationUserInput, - null, - true - ) - } - } - }) - } - - private fun setLimitEditTextInputType() { - // InputFilter๋กœ ๋„์–ด์“ฐ๊ธฐ ์ž…๋ ฅ๋งŒ ๋ง‰๊ณ  ๋‚˜๋จธ์ง€๋Š” ์•ˆ๋‚ด๋ฉ”์‹œ์ง€๋กœ ๋„์›Œ์ง€๋„๋ก ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ๋„ - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - binding.etFindAccountPasswordNewPasswordConfirmationUserInput.filters = - arrayOf(filterInputCheck) - } - - private fun setUserPasswordEditText() { - binding.password = args.password - } - - private fun setBackClickListener() { - binding.btnFindAccountPasswordNewPasswordConfirmationBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnFindAccountPasswordNewPasswordConfirmationNext.setOnDebounceClickListener { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - loginViewModel.requestChangePassword(email = args.email, newPassword = args.password) - } - } - - private fun observeChangePasswordSuccess() { - loginViewModel.changePasswordSuccess.observe(viewLifecycleOwner) { isChangeSuccess -> - if (isChangeSuccess) { - findNavController().navigateSafe( - currentDestinationId = R.id.FindAccountPasswordNewPasswordConfirmationFragment, - action = R.id.action_findAccountPasswordNewPasswordConfirmationFragment_to_findAccountPasswordCompleteFragment - ) - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordNewPasswordFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordNewPasswordFragment.kt deleted file mode 100644 index adb45338..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/findAccount/FindAccountPasswordNewPasswordFragment.kt +++ /dev/null @@ -1,219 +0,0 @@ -package daily.dayo.presentation.fragment.account.findAccount - -import android.os.Bundle -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import androidx.fragment.app.Fragment -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.SetTextInputLayout -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFindAccountPasswordNewPasswordBinding -import java.util.regex.Pattern - -@AndroidEntryPoint -class FindAccountPasswordNewPasswordFragment : Fragment() { - private var binding by autoCleared() - private val args by navArgs() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFindAccountPasswordNewPasswordBinding.inflate(inflater, container, false) - setBackClickListener() - setNextClickListener() - initEditText() - setLimitEditTextInputType() - setTextEditorActionListener() - verifyPassword() - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.setOnTouchListener { _, _ -> - HideKeyBoardUtil.hide( - requireContext(), - binding.etFindAccountPasswordNewPasswordUserPassword - ) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword.text.isNullOrEmpty() - ) - true - } - } - - private fun setTextEditorActionListener() { - binding.etFindAccountPasswordNewPasswordUserPassword.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide( - requireContext(), - binding.etFindAccountPasswordNewPasswordUserPassword - ) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword.text.isNullOrEmpty() - ) - true - } - - else -> false - } - } - } - - private fun initEditText() { - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword.text.isNullOrEmpty() - ) - with(binding.etFindAccountPasswordNewPasswordUserPassword) { - setOnFocusChangeListener { _, hasFocus -> // EditText Title ์„ค์ • - with(binding.layoutFindAccountPasswordNewPasswordUserPassword) { - if (hasFocus) { - hint = getString(R.string.password) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword, - false - ) - } else { - hint = - getString(R.string.signup_email_set_password_message_length_fail_min) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword, - true - ) - } - } - } - } - } - - private fun changeEditTextTitle() { - with(binding.layoutFindAccountPasswordNewPasswordUserPassword) { - if (binding.etFindAccountPasswordNewPasswordUserPassword.text.isNullOrEmpty()) { - hint = getString(R.string.signup_email_set_password_message_length_fail_min) - } else { - hint = getString(R.string.password) - } - } - } - - private fun verifyPassword() { - binding.etFindAccountPasswordNewPasswordUserPassword.addTextChangedListener(object : - TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (trimBlankText(s).length < 8) { // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธธ์ด ๊ฒ€์‚ฌ 1์— - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword, - false - ) - // 8์ž ์ดํ•˜์ผ ๋•Œ์—๋Š” ์˜ค๋ฅ˜ ๊ฒ€์‚ฌ ์‹คํŒจ์‹œ, ์—๋Ÿฌ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚˜์˜ค์ง€ ์•Š๋„๋ก ๋””์ž์ธ ์ˆ˜์ • - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnFindAccountPasswordNewPasswordNext - ) - } else if (trimBlankText(s).length > 16) { // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธธ์ด ๊ฒ€์‚ฌ 2 - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword, - getString(R.string.signup_email_set_password_message_length_fail_max), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnFindAccountPasswordNewPasswordNext - ) - } else if (!Pattern.matches("^[a-z|0-9|]+\$", trimBlankText(s))) { // ๋น„๋ฐ€๋ฒˆํ˜ธ ์–‘์‹ ๊ฒ€์‚ฌ - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword, - getString(R.string.signup_email_set_password_message_format_fail), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnFindAccountPasswordNewPasswordNext - ) - } else { - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutFindAccountPasswordNewPasswordUserPassword, - binding.etFindAccountPasswordNewPasswordUserPassword, - null, - true - ) - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnFindAccountPasswordNewPasswordNext - ) - } - } - }) - } - - private fun setLimitEditTextInputType() { - // InputFilter๋กœ ๋„์–ด์“ฐ๊ธฐ ์ž…๋ ฅ๋งŒ ๋ง‰๊ณ  ๋‚˜๋จธ์ง€๋Š” ์•ˆ๋‚ด๋ฉ”์‹œ์ง€๋กœ ๋„์›Œ์ง€๋„๋ก ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ๋„ - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - binding.etFindAccountPasswordNewPasswordUserPassword.filters = arrayOf(filterInputCheck) - } - - private fun setBackClickListener() { - binding.btnFindAccountPasswordNewPasswordBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnFindAccountPasswordNewPasswordNext.setOnDebounceClickListener { - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.FindAccountPasswordNewPasswordFragment, - action = R.id.action_findAccountPasswordNewPasswordFragment_to_findAccountPasswordNewPasswordConfirmationFragment, - args = FindAccountPasswordNewPasswordFragmentDirections.actionFindAccountPasswordNewPasswordFragmentToFindAccountPasswordNewPasswordConfirmationFragment( - args.email, - trimBlankText(binding.etFindAccountPasswordNewPasswordUserPassword.text) - ).arguments - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signin/LoginEmailFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/signin/LoginEmailFragment.kt deleted file mode 100644 index d8ed042a..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signin/LoginEmailFragment.kt +++ /dev/null @@ -1,274 +0,0 @@ -package daily.dayo.presentation.fragment.account.signin - -import android.app.AlertDialog -import android.content.ContentValues -import android.content.Intent -import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.activity.MainActivity -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentLoginEmailBinding -import daily.dayo.presentation.viewmodel.AccountViewModel - -@AndroidEntryPoint -class LoginEmailFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog( - loadingAlertDialog - ) - } - private val loginViewModel by activityViewModels() - private var isLoginButtonClick = false // TODO : ์ฒซ ํ™”๋ฉด์—์„œ ๋กœ๊ทธ์ธ ์‹คํŒจ ๋ฉ”์‹œ์ง€ ๋“ฑ์žฅ์œผ๋กœ ์ธํ•œ ์ž„์‹œ ํ•ด๊ฒฐ - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?, - ): View? { - binding = FragmentLoginEmailBinding.inflate(inflater, container, false) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - setBackClickListener() - setNextClickListener() - loginSuccess() - initEditText() - setTextEditorActionListener() - activationNextButton() - setForgetAccountClickListener() - setSignupClickListener() - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.setOnTouchListener { _, _ -> - HideKeyBoardUtil.hide(requireContext(), binding.etLoginEmailAddress) - HideKeyBoardUtil.hide(requireContext(), binding.etLoginEmailPassword) - true - } - } - - private fun setTextEditorActionListener() { - binding.etLoginEmailAddress.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide(requireContext(), binding.etLoginEmailAddress) - HideKeyBoardUtil.hide(requireContext(), binding.etLoginEmailPassword) - true - } - - else -> false - } - } - } - - private fun setBackClickListener() { - binding.btnLoginEmailBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnLoginEmailNext.setOnDebounceClickListener { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - isLoginButtonClick = true - loginViewModel.requestLoginEmail( - email = trimBlankText(binding.etLoginEmailAddress.text), - password = trimBlankText(binding.etLoginEmailPassword.text), - ) - } - } - - private fun loginSuccess() { - loginViewModel.loginSuccess.observe(viewLifecycleOwner) { isSuccess -> - if (isSuccess.peekContent()) { - val intent = Intent(requireContext(), MainActivity::class.java) - intent.flags = - Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - startActivity(intent) - requireActivity().finish() - } else { - if (isLoginButtonClick) { - Log.e(ContentValues.TAG, "๋กœ๊ทธ์ธ ์‹คํŒจ") - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - if (loginViewModel.isLoginFailByUncorrected.value?.getContentIfNotHandled() == true) { - Toast.makeText( - requireContext(), - getString(R.string.login_email_alert_message_fail), - Toast.LENGTH_SHORT - ).show() - } - } - } - } - } - - private fun initEditText() { - with(binding.etLoginEmailAddress) { - setOnFocusChangeListener { _, hasFocus -> // - with(binding.layoutLoginEmailAddress) { - if (hasFocus) { - hint = "" - binding.etLoginEmailAddress.backgroundTintList = - resources.getColorStateList( - R.color.primary_green_23C882, - context?.theme - ) - } else if (!binding.etLoginEmailAddress.text.isNullOrEmpty()) { - hint = "" - binding.etLoginEmailAddress.backgroundTintList = null - } else { - hint = getString(R.string.hint_login_email_address) - binding.etLoginEmailAddress.backgroundTintList = null - } - } - } - addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int, - ) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - with(binding.layoutLoginEmailAddress) { - boxStrokeColor = - resources.getColor(R.color.primary_green_23C882, context?.theme) - } - } - }) - } - with(binding.etLoginEmailPassword) { - setOnFocusChangeListener { _, hasFocus -> // Title - with(binding.layoutLoginEmailPassword) { - if (hasFocus) { - hint = "" - binding.etLoginEmailPassword.backgroundTintList = - resources.getColorStateList( - R.color.primary_green_23C882, - context?.theme - ) - } else if (!binding.etLoginEmailPassword.text.isNullOrEmpty()) { - hint = "" - binding.etLoginEmailPassword.backgroundTintList = null - } else { - hint = getString(R.string.hint_login_email_password) - binding.etLoginEmailPassword.backgroundTintList = null - } - } - } - addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int, - ) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - with(binding.layoutLoginEmailPassword) { - boxStrokeColor = - resources.getColor(R.color.primary_green_23C882, context?.theme) - } - } - }) - } - } - - private fun activationNextButton() { - with(binding) { - etLoginEmailAddress.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int, - ) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (!s.toString() - .isNullOrEmpty() && !etLoginEmailPassword.text.isNullOrEmpty() - ) { - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnLoginEmailNext - ) - } else { - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnLoginEmailNext - ) - } - } - }) - etLoginEmailPassword.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int, - ) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (!s.toString() - .isNullOrEmpty() && !etLoginEmailAddress.text.isNullOrEmpty() - ) { - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnLoginEmailNext - ) - } else { - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnLoginEmailNext - ) - } - } - }) - } - } - - private fun setForgetAccountClickListener() { - binding.tvLoginEmailForgotPassword.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.LoginEmailFragment, - action = R.id.action_loginEmailFragment_to_findAccountPasswordCheckEmail - ) - } - } - - private fun setSignupClickListener() { - binding.tvLoginEmailSignup.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.LoginEmailFragment, - action = R.id.action_loginEmailFragment_to_signupEmailSetEmailAddressFragment - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signin/LoginFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/signin/LoginFragment.kt deleted file mode 100644 index edd4f098..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signin/LoginFragment.kt +++ /dev/null @@ -1,302 +0,0 @@ -package daily.dayo.presentation.fragment.account.signin - -import android.app.AlertDialog -import android.content.ContentValues -import android.content.Intent -import android.graphics.Typeface -import android.os.Bundle -import android.text.Spannable -import android.text.SpannableString -import android.text.method.LinkMovementMethod -import android.text.style.ClickableSpan -import android.text.style.ForegroundColorSpan -import android.text.style.StyleSpan -import android.text.style.UnderlineSpan -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.LinearLayout -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager2.widget.ViewPager2 -import com.kakao.sdk.auth.model.OAuthToken -import com.kakao.sdk.common.KakaoSdk -import com.kakao.sdk.common.model.AuthError -import com.kakao.sdk.common.model.AuthErrorCause -import com.kakao.sdk.common.model.ClientError -import com.kakao.sdk.common.model.ClientErrorCause -import com.kakao.sdk.user.UserApiClient -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.activity.MainActivity -import daily.dayo.presentation.adapter.OnBoardingPagerStateAdapter -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentLoginBinding -import daily.dayo.presentation.fragment.onboarding.OnBoardingFirstFragment -import daily.dayo.presentation.fragment.onboarding.OnBoardingFourthFragment -import daily.dayo.presentation.fragment.onboarding.OnBoardingSecondFragment -import daily.dayo.presentation.fragment.onboarding.OnBoardingThirdFragment -import daily.dayo.presentation.viewmodel.AccountViewModel - -@AndroidEntryPoint -class LoginFragment : Fragment() { - private var binding by autoCleared { - onDestroyBindingView() - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val loginViewModel by activityViewModels() - private lateinit var indicators: Array - private var pagerAdapter: OnBoardingPagerStateAdapter? = null - private lateinit var loadingAlertDialog: AlertDialog - private val pageChangeCallBack = object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - setCurrentIndicator(position) - } - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentLoginBinding.inflate(inflater, container, false) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - - loginSuccess() - setPolicy() - setKakaoLoginButtonClickListener() - setEmailLoginButtonClickListener() - setViewPager() - setViewPagerChangeEvent() - return binding.root - } - - private fun onDestroyBindingView() { - pagerAdapter = null - with(binding.vpLoginOnboarding) { - unregisterOnPageChangeCallback(pageChangeCallBack) - adapter = null - } - } - - private fun setKakaoLoginButtonClickListener() { - binding.btnLoginKakao.setOnDebounceClickListener { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - if (UserApiClient.instance.isKakaoTalkLoginAvailable(requireContext())) { - UserApiClient.instance.loginWithKakaoTalk(requireContext()) { token, error -> - if (error != null) { - Log.e("kakao login", "์นด์นด์˜คํ†ก์œผ๋กœ ๋กœ๊ทธ์ธ ์‹คํŒจ", error) - if (error is ClientError && error.reason == ClientErrorCause.Cancelled) { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - return@loginWithKakaoTalk - } - - UserApiClient.instance.loginWithKakaoAccount( - requireContext(), - callback = callback - ) - } else if (token != null) { - Log.i("kakao login", "์นด์นด์˜คํ†ก์œผ๋กœ ๋กœ๊ทธ์ธ ์„ฑ๊ณต ${token.accessToken}") - loginViewModel.requestLoginKakao(accessToken = token.accessToken) - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - } - } else { - UserApiClient.instance.loginWithKakaoAccount(requireContext(), callback = callback) - } - } - } - - val callback: (OAuthToken?, Throwable?) -> Unit = { token, error -> - if (error != null) { - when (error) { - is ClientError -> - when (error.reason) { - ClientErrorCause.Cancelled -> - Log.d("kakao login", "์ทจ์†Œ๋จ (back button)", error) - - ClientErrorCause.NotSupported -> - Log.e("kakao login", "์ง€์›๋˜์ง€ ์•Š์Œ (์นดํ†ก ๋ฏธ์„ค์น˜)", error) - - else -> - Log.e("kakao login", "๊ธฐํƒ€ ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ", error) - } - - is AuthError -> - when (error.reason) { - AuthErrorCause.AccessDenied -> - Log.d("kakao login", "์ทจ์†Œ๋จ (๋™์˜ ์ทจ์†Œ)", error) - - AuthErrorCause.Misconfigured -> - Log.e( - "kakao login", - "๊ฐœ๋ฐœ์ž์‚ฌ์ดํŠธ ์•ฑ ์„ค์ •์— ํ‚คํ•ด์‹œ๋ฅผ ๋“ฑ๋กํ•˜์„ธ์š”. ํ˜„์žฌ ๊ฐ’: ${KakaoSdk.keyHash}", - error - ) - - else -> - Log.e("kakao login", "๊ธฐํƒ€ ์ธ์ฆ ์—๋Ÿฌ", error) - } - - else -> - Log.e("kakao login", "๊ธฐํƒ€ ์—๋Ÿฌ (๋„คํŠธ์›Œํฌ ์žฅ์•  ๋“ฑ..)", error) - } - } else if (token != null) { - Log.i("kakao login", "๋กœ๊ทธ์ธ ์„ฑ๊ณต ${token.accessToken}") - loginViewModel.requestLoginKakao(accessToken = token.accessToken) - } - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - - private fun loginSuccess() { - loginViewModel.loginSuccess.observe(viewLifecycleOwner) { isSuccess -> - if (isSuccess.getContentIfNotHandled() == true) { - // ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์ฒซ ์‹œ๋„ ์‹œ ๋‹‰๋„ค์ž„ ์„ค์ • ํ™”๋ฉด์œผ๋กœ ์ด๋™ - if (loginViewModel.getCurrentUserInfo().nickname == "") { - findNavController().navigateSafe( - currentDestinationId = R.id.LoginFragment, - action = R.id.action_loginFragment_to_signupEmailSetProfileFragment, - args = LoginFragmentDirections.actionLoginFragmentToSignupEmailSetProfileFragment( - loginViewModel.getCurrentUserInfo().email.toString(), - null - ).arguments - ) - } else { - val intent = Intent(requireContext(), MainActivity::class.java) - intent.flags = - Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - startActivity(intent) - requireActivity().finish() - } - } else if (isSuccess.getContentIfNotHandled() == false) { - Log.e(ContentValues.TAG, "๋กœ๊ทธ์ธ ์‹คํŒจ") - } - } - } - - private fun setEmailLoginButtonClickListener() { - binding.btnLoginEmail.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.LoginFragment, - action = R.id.action_loginFragment_to_loginEmailFragment - ) - } - } - - private fun setViewPager() { - binding.vpLoginOnboarding.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER - pagerAdapter = - OnBoardingPagerStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) - pagerAdapter?.addFragment(OnBoardingFirstFragment()) - pagerAdapter?.addFragment(OnBoardingSecondFragment()) - pagerAdapter?.addFragment(OnBoardingThirdFragment()) - pagerAdapter?.addFragment(OnBoardingFourthFragment()) - binding.vpLoginOnboarding.adapter = pagerAdapter - setupIndicators(pagerAdapter?.itemCount ?: 0) - } - - private fun setViewPagerChangeEvent() { - binding.vpLoginOnboarding.registerOnPageChangeCallback(pageChangeCallBack) - } - - private fun setupIndicators(count: Int) { - indicators = arrayOfNulls(count) - val params = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - params.setMargins(16, 8, 16, 8) - for (i in indicators.indices) { - indicators[i] = ImageView(requireContext()) - indicators[i]!!.setImageDrawable( - ContextCompat.getDrawable(requireContext(), R.drawable.ic_indicator_inactive_gray) - ) - indicators[i]!!.layoutParams = params - binding.viewLoginOnboardingIndicators.addView(indicators[i]) - } - setCurrentIndicator(0) - } - - private fun setCurrentIndicator(position: Int) { - with(binding.viewLoginOnboardingIndicators) { - for (i in 0 until this.childCount) { - val imageView = this.getChildAt(i) as ImageView - if (i == position) { - imageView.setImageDrawable( - ContextCompat.getDrawable(requireContext(), R.drawable.ic_indicator_active) - ) - } else { - imageView.setImageDrawable( - ContextCompat.getDrawable( - requireContext(), - R.drawable.ic_indicator_inactive_gray - ) - ) - } - } - } - } - - private fun setPolicy() { - var span = SpannableString(getString(R.string.login_policy_guide_message)) - setPolicyMessage(span = span, type = "terms", text = getString(R.string.policy_terms)) - setPolicyMessage(span = span, type = "privacy", text = getString(R.string.policy_privacy)) - span.setSpan( - ForegroundColorSpan( - ContextCompat.getColor( - requireContext(), - R.color.gray_4_C5CAD2 - ) - ), - 0, - span.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - binding.tvLoginUseGuideMessage.movementMethod = LinkMovementMethod.getInstance() - binding.tvLoginUseGuideMessage.text = span - } - - private fun setPolicyMessage(span: SpannableString, type: String, text: String) { - span.setSpan( - object : ClickableSpan() { - override fun onClick(widget: View) { - with(findNavController()) { - if (currentDestination?.id == R.id.LoginFragment) { - currentDestination?.getAction(R.id.action_loginFragment_to_policyFragment) - ?.let { - navigateSafe( - currentDestinationId = R.id.LoginFragment, - action = R.id.action_loginFragment_to_policyFragment, - args = LoginFragmentDirections.actionLoginFragmentToPolicyFragment( - informationType = type - ).arguments - ) - } - } - } - } - }, - span.indexOf(text), span.indexOf(text) + text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - span.setSpan( - StyleSpan(Typeface.BOLD), - span.indexOf(text), - span.indexOf(text) + text.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - span.setSpan( - UnderlineSpan(), - span.indexOf(text), - span.indexOf(text) + text.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailCompleteFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailCompleteFragment.kt deleted file mode 100644 index 0fe1a117..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailCompleteFragment.kt +++ /dev/null @@ -1,52 +0,0 @@ -package daily.dayo.presentation.fragment.account.signup - -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSignupEmailCompleteBinding -import daily.dayo.presentation.activity.MainActivity -import daily.dayo.presentation.viewmodel.AccountViewModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class SignupEmailCompleteFragment : Fragment() { - private var binding by autoCleared() - private val loginViewModel by activityViewModels() - private val args by navArgs() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSignupEmailCompleteBinding.inflate(inflater, container, false) - setCloseClickListener() - setUserNickname() - return binding.root - } - - private fun setUserNickname() { - binding.userNickname = args.nickname - } - - private fun setCloseClickListener() { - binding.btnSignupEmailCompleteClose.setOnDebounceClickListener { - if (loginViewModel.loginSuccess.value?.peekContent() == true) { - val intent = Intent(activity, MainActivity::class.java) - intent.flags = - Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - startActivity(intent) - activity?.finish() - } else { - findNavController().navigateUp() - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetEmailAddressCertificateFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetEmailAddressCertificateFragment.kt deleted file mode 100644 index ba3a9a9e..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetEmailAddressCertificateFragment.kt +++ /dev/null @@ -1,240 +0,0 @@ -package daily.dayo.presentation.fragment.account.signup - -import android.os.Bundle -import android.os.CountDownTimer -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.SetTextInputLayout -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSignupEmailSetEmailAddressCertificateBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import java.util.regex.Pattern - -@AndroidEntryPoint -class SignupEmailSetEmailAddressCertificateFragment : Fragment() { - private var binding by autoCleared { - currentCountDownTimer?.cancel() - currentCountDownTimer = null - } - private val loginViewModel by activityViewModels() - private val args by navArgs() - private var currentCountDownTimer: CountDownTimer? = null - private var isCountDownDone: Boolean = false - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSignupEmailSetEmailAddressCertificateBinding.inflate(inflater, container, false) - setBackClickListener() - setNextClickListener() - initEditText() - setLimitEditTextInputType() - setTextEditorActionListener() - setEmailResendClickListener() - certificateEmail() - setUserAddressEditText() - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.setOnTouchListener { _, _ -> - HideKeyBoardUtil.hide(requireContext(), binding.etSignupEmailSetEmailAddressCertificateUserInput) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressCertificateUserInput, - binding.etSignupEmailSetEmailAddressCertificateUserInput, - binding.etSignupEmailSetEmailAddressCertificateUserInput.text.isNullOrEmpty() - ) - true - } - } - - private fun setTextEditorActionListener() { - binding.etSignupEmailSetEmailAddressCertificateUserInput.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide(requireContext(), binding.etSignupEmailSetEmailAddressCertificateUserInput) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressCertificateUserInput, - binding.etSignupEmailSetEmailAddressCertificateUserInput, - binding.etSignupEmailSetEmailAddressCertificateUserInput.text.isNullOrEmpty() - ) - true - } - - else -> false - } - } - } - - private fun initEditText() { - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressCertificateUserInput, - binding.etSignupEmailSetEmailAddressCertificateUserInput, - binding.etSignupEmailSetEmailAddressCertificateUserInput.text.isNullOrEmpty() - ) - binding.etSignupEmailSetEmailAddressCertificateUserInput.setOnFocusChangeListener { _, hasFocus -> - with(binding.layoutSignupEmailSetEmailAddressCertificateUserInput) { - if (hasFocus) { - hint = getString(R.string.email_address_certification) - SetTextInputLayout.setEditTextTheme(requireContext(), binding.layoutSignupEmailSetEmailAddressCertificateUserInput, binding.etSignupEmailSetEmailAddressCertificateUserInput, false) - } else { - hint = getString(R.string.email_address_certificate_title) - SetTextInputLayout.setEditTextTheme(requireContext(), binding.layoutSignupEmailSetEmailAddressCertificateUserInput, binding.etSignupEmailSetEmailAddressCertificateUserInput, true) - } - } - } - currentCountDownTimer = countDownTimer - currentCountDownTimer?.start() - } - - private fun changeEditTextTitle() { - with(binding.layoutSignupEmailSetEmailAddressCertificateUserInput) { - if (binding.etSignupEmailSetEmailAddressCertificateUserInput.text.isNullOrEmpty()) { - hint = getString(R.string.email_address_certificate_title) - } else { - hint = getString(R.string.email_address_certification) - } - } - } - - private fun certificateEmail() { - // ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ๋‹ด๊ธด ์ด๋ฉ”์ผ ์ตœ์ดˆ ๋ฐœ์†ก - loginViewModel.requestCertificateEmail(args.email) - - binding.etSignupEmailSetEmailAddressCertificateUserInput.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (!binding.etSignupEmailSetEmailAddressCertificateUserInput.text.isNullOrEmpty() && !isCountDownDone) { - ButtonActivation.setSignupButtonActive(requireContext(), binding.btnSignupEmailSetEmailAddressCertificateNext) - } else { - ButtonActivation.setSignupButtonInactive(requireContext(), binding.btnSignupEmailSetEmailAddressCertificateNext) - } - } - }) - binding.btnSignupEmailSetEmailAddressCertificateNext.setOnDebounceClickListener { - if (binding.etSignupEmailSetEmailAddressCertificateUserInput.text.toString() == loginViewModel.certificateEmailAuthCode.value) { - ButtonActivation.setSignupButtonActive(requireContext(), binding.btnSignupEmailSetEmailAddressCertificateNext) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressCertificateUserInput, - binding.etSignupEmailSetEmailAddressCertificateUserInput, - null, - true - ) - currentCountDownTimer?.cancel() - currentCountDownTimer = null - binding.tvSignupEmailSetEmailAddressCertificateResend.isEnabled = false - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.SignupEmailSetEmailAddressCertificateFragment, - action = R.id.action_signupEmailSetEmailAddressCertificateFragment_to_signupEmailSetPasswordFragment, - args = SignupEmailSetEmailAddressCertificateFragmentDirections.actionSignupEmailSetEmailAddressCertificateFragmentToSignupEmailSetPasswordFragment(args.email).arguments - ) - } else { - ButtonActivation.setSignupButtonInactive(requireContext(), binding.btnSignupEmailSetEmailAddressCertificateNext) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressCertificateUserInput, - binding.etSignupEmailSetEmailAddressCertificateUserInput, - getString(R.string.email_address_certificate_alert_message_match_fail), - false - ) - Toast.makeText(requireContext(), "์ธ์ฆ ์‹คํŒจ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.", Toast.LENGTH_SHORT).show() - } - } - } - - private fun setLimitEditTextInputType() { - // InputFilter๋กœ ๋„์–ด์“ฐ๊ธฐ ์ž…๋ ฅ๋งŒ ๋ง‰๊ณ  ๋‚˜๋จธ์ง€๋Š” ์•ˆ๋‚ด๋ฉ”์‹œ์ง€๋กœ ๋„์›Œ์ง€๋„๋ก ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ๋„ - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - binding.etSignupEmailSetEmailAddressCertificateUserInput.filters = arrayOf(filterInputCheck) - } - - private val countDownTimer = object : CountDownTimer((1000 * 60 * 3).toLong(), 1000) { - override fun onTick(millisUntilFinished: Long) { - isCountDownDone = false - var remainMinutes = (millisUntilFinished / 1000) / 60 - var remainSeconds = millisUntilFinished / 1000 % 60 - binding.tvSignupEmailSetEmailAddressCertificateTimer.text = "${"%02d".format(remainMinutes)}:${"%02d".format(remainSeconds)}" - } - - override fun onFinish() { - isCountDownDone = true - ButtonActivation.setSignupButtonInactive(requireContext(), binding.btnSignupEmailSetEmailAddressCertificateNext) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressCertificateUserInput, - binding.etSignupEmailSetEmailAddressCertificateUserInput, - getString(R.string.email_address_certificate_alert_message_time_fail), - false - ) - } - } - - private fun setEmailResendClickListener() { - binding.tvSignupEmailSetEmailAddressCertificateResend.setOnDebounceClickListener { - HideKeyBoardUtil.hide(requireContext(), binding.etSignupEmailSetEmailAddressCertificateUserInput) // 1. ํ‚ค๋ณด๋“œ๊ฐ€ ์˜ฌ๋ผ์™€ ์žˆ๋Š” ๊ฒฝ์šฐ ํ‚ค๋ณด๋“œ๊ฐ€ ๋‚ด๋ ค๊ฐ - binding.etSignupEmailSetEmailAddressCertificateUserInput.setText("") // 2. ์ธ์ฆ๋ฒˆํ˜ธ ์ž…๋ ฅ์ฐฝ ์ดˆ๊ธฐํ™” - currentCountDownTimer?.start() // 3. ์ œํ•œ์‹œ๊ฐ„ ์ดˆ๊ธฐํ™” - - changeEditTextTitle() - binding.layoutSignupEmailSetEmailAddressCertificateUserInput.isErrorEnabled = false - SetTextInputLayout.setEditTextTheme(requireContext(), binding.layoutSignupEmailSetEmailAddressCertificateUserInput, binding.etSignupEmailSetEmailAddressCertificateUserInput, true) - - loginViewModel.requestCertificateEmail(args.email) - Toast.makeText(requireContext(), R.string.email_address_certificate_alert_message_resend, Toast.LENGTH_SHORT).show() // 4. ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ๋ณด์—ฌ์ง - } - } - - private fun setUserAddressEditText() { - binding.email = args.email - } - - private fun setBackClickListener() { - binding.btnSignupEmailSetEmailAddressCertificateBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnSignupEmailSetEmailAddressCertificateNext.setOnDebounceClickListener { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.SignupEmailSetEmailAddressCertificateFragment, - action = R.id.action_signupEmailSetEmailAddressCertificateFragment_to_signupEmailSetPasswordFragment, - args = SignupEmailSetEmailAddressCertificateFragmentDirections.actionSignupEmailSetEmailAddressCertificateFragmentToSignupEmailSetPasswordFragment(args.email).arguments - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetEmailAddressFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetEmailAddressFragment.kt deleted file mode 100644 index 225dc115..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetEmailAddressFragment.kt +++ /dev/null @@ -1,228 +0,0 @@ -package daily.dayo.presentation.fragment.account.signup - -import android.os.Bundle -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.SetTextInputLayout -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSignupEmailSetEmailAddressBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import java.util.regex.Pattern - -@AndroidEntryPoint -class SignupEmailSetEmailAddressFragment : Fragment() { - private var binding by autoCleared() - private val loginViewModel by activityViewModels() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSignupEmailSetEmailAddressBinding.inflate(inflater, container, false) - setBackClickListener() - setNextClickListener() - initEditText() - setLimitEditTextInputType() - setTextEditorActionListener() - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.setOnTouchListener { _, _ -> - HideKeyBoardUtil.hide(requireContext(), binding.etSignupEmailSetEmailAddressUserInput) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput.text.isNullOrEmpty() - ) - verifyEmailAddress() - true - } - } - - private fun setTextEditorActionListener() { - binding.etSignupEmailSetEmailAddressUserInput.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide( - requireContext(), - binding.etSignupEmailSetEmailAddressUserInput - ) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput.text.isNullOrEmpty() - ) - verifyEmailAddress() - true - } - - else -> false - } - } - } - - private fun initEditText() { - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput.text.isNullOrEmpty() - ) - with(binding.etSignupEmailSetEmailAddressUserInput) { - setOnFocusChangeListener { _, hasFocus -> // Title - with(binding.layoutSignupEmailSetEmailAddressUserInput) { - if (hasFocus) { - hint = getString(R.string.email) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput, - false - ) - } else { - hint = getString(R.string.email_address_example) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput, - true - ) - } - } - } - addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - with(binding.layoutSignupEmailSetEmailAddressUserInput) { - isErrorEnabled = false - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput, - false - ) - } - } - }) - } - } - - private fun changeEditTextTitle() { - with(binding.layoutSignupEmailSetEmailAddressUserInput) { - if (binding.etSignupEmailSetEmailAddressUserInput.text.isNullOrEmpty()) { - hint = getString(R.string.email_address_example) - } else { - hint = getString(R.string.email) - } - } - } - - private fun verifyEmailAddress() { - if (android.util.Patterns.EMAIL_ADDRESS.matcher( - trimBlankText(binding.etSignupEmailSetEmailAddressUserInput.text) - ).matches() - ) { // ์ด๋ฉ”์ผ ์ฃผ์†Œ ์–‘์‹ ๊ฒ€์‚ฌ - loginViewModel.requestCheckEmailDuplicate(trimBlankText(binding.etSignupEmailSetEmailAddressUserInput.text)) - loginViewModel.isEmailDuplicate.observe(viewLifecycleOwner, Observer { isDuplicate -> - if (isDuplicate) { - SetTextInputLayout.setEditTextErrorThemeWithIcon( - requireContext(), - binding.layoutSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput, - null, - true - ) - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnSignupEmailSetEmailAddressNext - ) - } else { - SetTextInputLayout.setEditTextErrorThemeWithIcon( - requireContext(), - binding.layoutSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput, - getString(R.string.signup_email_set_email_address_message_duplicate_fail), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSignupEmailSetEmailAddressNext - ) - } - }) - } else { - SetTextInputLayout.setEditTextErrorThemeWithIcon( - requireContext(), - binding.layoutSignupEmailSetEmailAddressUserInput, - binding.etSignupEmailSetEmailAddressUserInput, - getString(R.string.signup_email_set_email_address_message_format_fail), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSignupEmailSetEmailAddressNext - ) - } - } - - private fun setLimitEditTextInputType() { - // InputFilter๋กœ ๋„์–ด์“ฐ๊ธฐ ์ž…๋ ฅ๋งŒ ๋ง‰๊ณ  ๋‚˜๋จธ์ง€๋Š” ์•ˆ๋‚ด๋ฉ”์‹œ์ง€๋กœ ๋„์›Œ์ง€๋„๋ก ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ๋„ - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - binding.etSignupEmailSetEmailAddressUserInput.filters = arrayOf(filterInputCheck) - } - - private fun setBackClickListener() { - binding.btnSignupEmailSetEmailAddressBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnSignupEmailSetEmailAddressNext.setOnDebounceClickListener { - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.SignupEmailSetEmailAddressFragment, - action = R.id.action_signupEmailSetEmailAddressFragment_to_signupEmailSetEmailAddressCertificateFragment, - args = SignupEmailSetEmailAddressFragmentDirections.actionSignupEmailSetEmailAddressFragmentToSignupEmailSetEmailAddressCertificateFragment( - trimBlankText(binding.etSignupEmailSetEmailAddressUserInput.text) - ).arguments - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetPasswordConfirmationFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetPasswordConfirmationFragment.kt deleted file mode 100644 index 5882414d..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetPasswordConfirmationFragment.kt +++ /dev/null @@ -1,171 +0,0 @@ -package daily.dayo.presentation.fragment.account.signup - -import android.os.Bundle -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import androidx.fragment.app.Fragment -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.SetTextInputLayout -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSignupEmailSetPasswordConfirmationBinding -import java.util.regex.Pattern - -@AndroidEntryPoint -class SignupEmailSetPasswordConfirmationFragment : Fragment() { - private var binding by autoCleared() - private val args by navArgs() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSignupEmailSetPasswordConfirmationBinding.inflate(inflater, container, false) - setBackClickListener() - setNextClickListener() - initEditText() - setLimitEditTextInputType() - setTextEditorActionListener() - verifyPassword() - setUserPasswordEditText() - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.setOnTouchListener { _, _ -> - HideKeyBoardUtil.hide(requireContext(), binding.etSignupEmailSetPasswordConfirmationUserInput) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordConfirmationUserInput, - binding.etSignupEmailSetPasswordConfirmationUserInput, - binding.etSignupEmailSetPasswordConfirmationUserInput.text.isNullOrEmpty() - ) - true - } - } - - private fun setTextEditorActionListener() { - binding.etSignupEmailSetPasswordConfirmationUserInput.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide(requireContext(), binding.etSignupEmailSetPasswordConfirmationUserInput) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordConfirmationUserInput, - binding.etSignupEmailSetPasswordConfirmationUserInput, - binding.etSignupEmailSetPasswordConfirmationUserInput.text.isNullOrEmpty() - ) - true - } - - else -> false - } - } - } - - private fun initEditText() { - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordConfirmationUserInput, - binding.etSignupEmailSetPasswordConfirmationUserInput, - binding.etSignupEmailSetPasswordConfirmationUserInput.text.isNullOrEmpty() - ) - binding.etSignupEmailSetPasswordConfirmationUserInput.setOnFocusChangeListener { _, hasFocus -> - with(binding.layoutSignupEmailSetPasswordConfirmationUserInput) { - if (hasFocus) { - hint = getString(R.string.password_confirmation) - SetTextInputLayout.setEditTextTheme(requireContext(), binding.layoutSignupEmailSetPasswordConfirmationUserInput, binding.etSignupEmailSetPasswordConfirmationUserInput, false) - } else { - hint = getString(R.string.signup_email_set_password_confirmation_edittext_hint) - SetTextInputLayout.setEditTextTheme(requireContext(), binding.layoutSignupEmailSetPasswordConfirmationUserInput, binding.etSignupEmailSetPasswordConfirmationUserInput, true) - } - } - } - } - - private fun changeEditTextTitle() { - with(binding.layoutSignupEmailSetPasswordConfirmationUserInput) { - if (binding.etSignupEmailSetPasswordConfirmationUserInput.text.isNullOrEmpty()) { - hint = getString(R.string.signup_email_set_password_confirmation_edittext_hint) - } else { - hint = getString(R.string.password_confirmation) - } - } - } - - private fun verifyPassword() { - binding.etSignupEmailSetPasswordConfirmationUserInput.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (args.password != trimBlankText(binding.etSignupEmailSetPasswordConfirmationUserInput.text)) { // ๋™์ผ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์‚ฌ - ButtonActivation.setSignupButtonInactive(requireContext(), binding.btnSignupEmailSetPasswordConfirmationNext) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordConfirmationUserInput, - binding.etSignupEmailSetPasswordConfirmationUserInput, - getString(R.string.signup_email_set_password_confirmation_message_fail), - false - ) - } else { - ButtonActivation.setSignupButtonActive(requireContext(), binding.btnSignupEmailSetPasswordConfirmationNext) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordConfirmationUserInput, - binding.etSignupEmailSetPasswordConfirmationUserInput, - null, - true - ) - } - } - }) - } - - private fun setLimitEditTextInputType() { - // InputFilter๋กœ ๋„์–ด์“ฐ๊ธฐ ์ž…๋ ฅ๋งŒ ๋ง‰๊ณ  ๋‚˜๋จธ์ง€๋Š” ์•ˆ๋‚ด๋ฉ”์‹œ์ง€๋กœ ๋„์›Œ์ง€๋„๋ก ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ๋„ - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - binding.etSignupEmailSetPasswordConfirmationUserInput.filters = arrayOf(filterInputCheck) - } - - private fun setUserPasswordEditText() { - binding.password = args.password - } - - private fun setBackClickListener() { - binding.btnSignupEmailSetPasswordConfirmationBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnSignupEmailSetPasswordConfirmationNext.setOnDebounceClickListener { - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.SignupEmailSetPasswordConfirmationFragment, - action = R.id.action_signupEmailSetPasswordConfirmationFragment_to_signupEmailSetProfileFragment, - args = SignupEmailSetPasswordConfirmationFragmentDirections.actionSignupEmailSetPasswordConfirmationFragmentToSignupEmailSetProfileFragment(args.email, args.password).arguments - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetPasswordFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetPasswordFragment.kt deleted file mode 100644 index ca0ca1a8..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetPasswordFragment.kt +++ /dev/null @@ -1,215 +0,0 @@ -package daily.dayo.presentation.fragment.account.signup - -import android.os.Bundle -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import androidx.fragment.app.Fragment -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.SetTextInputLayout -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSignupEmailSetPasswordBinding -import java.util.regex.Pattern - -@AndroidEntryPoint -class SignupEmailSetPasswordFragment : Fragment() { - private var binding by autoCleared() - private val args by navArgs() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSignupEmailSetPasswordBinding.inflate(inflater, container, false) - setBackClickListener() - setNextClickListener() - initEditText() - setLimitEditTextInputType() - setTextEditorActionListener() - verifyPassword() - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.setOnTouchListener { _, _ -> - HideKeyBoardUtil.hide(requireContext(), binding.etSignupEmailSetPasswordUserPassword) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword.text.isNullOrEmpty() - ) - true - } - } - - private fun setTextEditorActionListener() { - binding.etSignupEmailSetPasswordUserPassword.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide( - requireContext(), - binding.etSignupEmailSetPasswordUserPassword - ) - changeEditTextTitle() - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword.text.isNullOrEmpty() - ) - true - } - - else -> false - } - } - } - - private fun initEditText() { - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword.text.isNullOrEmpty() - ) - with(binding.etSignupEmailSetPasswordUserPassword) { - setOnFocusChangeListener { _, hasFocus -> // EditText Title ์„ค์ • - with(binding.layoutSignupEmailSetPasswordUserPassword) { - if (hasFocus) { - hint = getString(R.string.password) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword, - false - ) - } else { - hint = getString(R.string.signup_email_set_password_message_length_fail_min) - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword, - true - ) - } - } - } - } - } - - private fun changeEditTextTitle() { - with(binding.layoutSignupEmailSetPasswordUserPassword) { - if (binding.etSignupEmailSetPasswordUserPassword.text.isNullOrEmpty()) { - hint = getString(R.string.signup_email_set_password_message_length_fail_min) - } else { - hint = getString(R.string.password) - } - } - } - - private fun verifyPassword() { - binding.etSignupEmailSetPasswordUserPassword.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (trimBlankText(s).length < 8) { // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธธ์ด ๊ฒ€์‚ฌ 1์— - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword, - false - ) - // 8์ž ์ดํ•˜์ผ ๋•Œ์—๋Š” ์˜ค๋ฅ˜ ๊ฒ€์‚ฌ ์‹คํŒจ์‹œ, ์—๋Ÿฌ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚˜์˜ค์ง€ ์•Š๋„๋ก ๋””์ž์ธ ์ˆ˜์ • - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSignupEmailSetPasswordNext - ) - } else if (trimBlankText(s).length > 16) { // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธธ์ด ๊ฒ€์‚ฌ 2 - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword, - getString(R.string.signup_email_set_password_message_length_fail_max), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSignupEmailSetPasswordNext - ) - } else if (!Pattern.matches("^[a-z|0-9|]+\$", trimBlankText(s)) - ) { // ๋น„๋ฐ€๋ฒˆํ˜ธ ์–‘์‹ ๊ฒ€์‚ฌ - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword, - getString(R.string.signup_email_set_password_message_format_fail), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSignupEmailSetPasswordNext - ) - } else { - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSignupEmailSetPasswordUserPassword, - binding.etSignupEmailSetPasswordUserPassword, - null, - true - ) - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnSignupEmailSetPasswordNext - ) - } - } - }) - } - - private fun setLimitEditTextInputType() { - // InputFilter๋กœ ๋„์–ด์“ฐ๊ธฐ ์ž…๋ ฅ๋งŒ ๋ง‰๊ณ  ๋‚˜๋จธ์ง€๋Š” ์•ˆ๋‚ด๋ฉ”์‹œ์ง€๋กœ ๋„์›Œ์ง€๋„๋ก ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ๋„ - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - binding.etSignupEmailSetPasswordUserPassword.filters = arrayOf(filterInputCheck) - } - - private fun setBackClickListener() { - binding.btnSignupEmailSetPasswordBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnSignupEmailSetPasswordNext.setOnDebounceClickListener { - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.SignupEmailSetPasswordFragment, - action = R.id.action_signupEmailSetPasswordFragment_to_signupEmailSetPasswordConfirmationFragment, - args = SignupEmailSetPasswordFragmentDirections.actionSignupEmailSetPasswordFragmentToSignupEmailSetPasswordConfirmationFragment( - args.email, - trimBlankText(binding.etSignupEmailSetPasswordUserPassword.text) - ).arguments - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetProfileFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetProfileFragment.kt deleted file mode 100644 index b72efe36..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetProfileFragment.kt +++ /dev/null @@ -1,395 +0,0 @@ -package daily.dayo.presentation.fragment.account.signup - -import android.app.AlertDialog -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.ImageDecoder -import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.Toast -import androidx.core.net.toUri -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.fragment.app.viewModels -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.image.ImageResizeUtil -import daily.dayo.presentation.common.image.ImageResizeUtil.USER_PROFILE_THUMBNAIL_RESIZE_SIZE -import daily.dayo.presentation.common.image.ImageResizeUtil.cropCenterBitmap -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSignupEmailSetProfileBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import daily.dayo.presentation.viewmodel.ProfileSettingViewModel -import java.io.File -import java.io.FileOutputStream -import java.io.OutputStream -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.regex.Pattern - -@AndroidEntryPoint -class SignupEmailSetProfileFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - onDestroyBindingView() - } - private val loginViewModel by activityViewModels() - private val profileSettingViewModel by viewModels() - private val args by navArgs() - private var glideRequestManager: RequestManager? = null - private lateinit var userProfileImageString: String - private var imagePath: String? = null - private val imageFileTimeFormat = SimpleDateFormat("yyyy-MM-d-HH-mm-ss", Locale.KOREA) - private lateinit var userProfileImageExtension: String - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSignupEmailSetProfileBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initAlertDialog() - setBackClickListener() - setNextClickListener() - setLimitEditTextInputType() - setTextEditorActionListener() - verifyNickname() - setProfilePhotoClickListener() - observeNavigationMyProfileImageCallBack() - observeKakaoSignupCallback() - observeSignupCallback() - setHideKeyboard() - } - - private fun setHideKeyboard() { - requireView().setOnTouchListener { _, _ -> - HideKeyBoardUtil.hide(requireContext(), binding.etSignupEmailSetProfileNickname) - true - } - } - - private fun onDestroyBindingView() { - glideRequestManager = null - } - - private fun initAlertDialog() { - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - loadingAlertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - } - - private fun setTextEditorActionListener() { - binding.etSignupEmailSetProfileNickname.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide(requireContext(), binding.etSignupEmailSetProfileNickname) - true - } - - else -> false - } - } - } - - private fun verifyNickname() { - binding.etSignupEmailSetProfileNickname.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - with(binding) { - if (trimBlankText(s).length < 2) { // ๋‹‰๋„ค์ž„ ๊ธธ์ด ๊ฒ€์‚ฌ 1 - setEditTextTheme( - getString(R.string.my_profile_edit_nickname_message_length_fail_min), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSignupEmailSetProfileNext - ) - } else if (trimBlankText(s).length > 10) { // ๋‹‰๋„ค์ž„ ๊ธธ์ด ๊ฒ€์‚ฌ 2 - setEditTextTheme( - getString(R.string.my_profile_edit_nickname_message_length_fail_max), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSignupEmailSetProfileNext - ) - } else { - if (Pattern.matches("^[ใ„ฑ-ใ…Ž|ใ…-ใ…ฃ|๊ฐ€-ํžฃ|a-z|A-Z|0-9|]+\$", trimBlankText(s)) - ) { - loginViewModel.requestCheckNicknameDuplicate(trimBlankText(binding.etSignupEmailSetProfileNickname.text)) - loginViewModel.isNicknameDuplicate.observe(viewLifecycleOwner) { isDuplicate -> - if (isDuplicate) { - setEditTextTheme( - getString(R.string.my_profile_edit_nickname_message_success), - true - ) - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnSignupEmailSetProfileNext - ) - } else { - setEditTextTheme( - getString(R.string.my_profile_edit_nickname_message_duplicate_fail), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSignupEmailSetProfileNext - ) - } - } - } else { - setEditTextTheme( - getString(R.string.my_profile_edit_nickname_message_format_fail), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSignupEmailSetProfileNext - ) - } - } - } - } - }) - } - - private fun setEditTextTheme(checkMessage: String?, pass: Boolean) { - with(binding.tvSignupEmailSetProfileNicknameMessage) { - visibility = View.VISIBLE - if (pass) { - text = checkMessage - setTextColor(resources.getColor(R.color.primary_green_23C882, context?.theme)) - binding.etSignupEmailSetProfileNickname.backgroundTintList = - resources.getColorStateList(R.color.primary_green_23C882, context?.theme) - } else { - text = checkMessage - setTextColor(resources.getColor(R.color.red_FF4545, context?.theme)) - binding.etSignupEmailSetProfileNickname.backgroundTintList = - resources.getColorStateList(R.color.red_FF4545, context?.theme) - } - } - } - - private fun setLimitEditTextInputType() { - // InputFilter๋กœ ๋„์–ด์“ฐ๊ธฐ ์ž…๋ ฅ๋งŒ ๋ง‰๊ณ  ๋‚˜๋จธ์ง€๋Š” ์•ˆ๋‚ด๋ฉ”์‹œ์ง€๋กœ ๋„์›Œ์ง€๋„๋ก ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ๋„ - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - val lengthFilter = InputFilter.LengthFilter(10) - binding.etSignupEmailSetProfileNickname.filters = arrayOf(filterInputCheck, lengthFilter) - } - - private fun setProfilePhotoClickListener() { - binding.layoutSignupEmailSetProfileUserImg.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.SignupEmailSetProfileFragment, - action = R.id.action_signupEmailSetProfileFragment_to_signupEmailSetProfileImageOptionFragment - ) - } - } - - private fun observeNavigationMyProfileImageCallBack() { - findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData("userProfileImageString") - ?.observe(viewLifecycleOwner) { - userProfileImageString = it - if (this::userProfileImageString.isInitialized) { - if (userProfileImageString == "resetMyProfileImage") { - glideRequestManager?.load(R.drawable.ic_user_profile_image_empty) - ?.centerCrop()?.into(binding.imgSignupEmailSetProfileUserImage) - } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - glideRequestManager?.load(userProfileImageString.toUri())?.centerCrop() - ?.into(binding.imgSignupEmailSetProfileUserImage) - } - } - } - } - findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData("fileExtension") - ?.observe(viewLifecycleOwner) { - userProfileImageExtension = it - } - } - - private fun setUploadImagePath(fileExtension: String) { - // uri๋ฅผ ํ†ตํ•˜์—ฌ ๋ถˆ๋Ÿฌ์˜จ ์ด๋ฏธ์ง€๋ฅผ ์ž„์‹œ๋กœ ํŒŒ์ผ๋กœ ์ €์žฅํ•  ๊ฒฝ๋กœ๋กœ ์•ฑ ๋‚ด๋ถ€ ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์„ค์ •, - // ํŒŒ์ผ ์ด๋ฆ„์€ ๋ถˆ๋Ÿฌ์˜จ ์‹œ๊ฐ„ ์‚ฌ์šฉ - val fileName = imageFileTimeFormat.format(Date(System.currentTimeMillis())) - .toString() + "." + fileExtension - val cacheDir = requireContext().cacheDir.toString() - imagePath = "$cacheDir/$fileName" - } - - fun bitmapToFile(bitmap: Bitmap?, path: String?): File? { - if (bitmap == null || path == null) { - return null - } - var file = File(path) - var out: OutputStream? = null - try { - file.createNewFile() - out = FileOutputStream(file) - bitmap?.compress(Bitmap.CompressFormat.JPEG, 100, out) - } finally { - out?.close() - } - return file - } - - fun Uri.toBitmap(): Bitmap { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoder.decodeBitmap( - ImageDecoder.createSource( - requireContext().contentResolver, - this - ) - ) - } else { - MediaStore.Images.Media.getBitmap(requireContext().contentResolver, this) - } - } - - private fun vectorDrawableToBitmapDrawable(drawable: Drawable): Bitmap? { - try { - val bitmap: Bitmap = Bitmap.createBitmap( - drawable.intrinsicWidth, - drawable.intrinsicHeight, - Bitmap.Config.ARGB_8888 - ) - val canvas = Canvas(bitmap) - drawable.setBounds(0, 0, canvas.width, canvas.height) - drawable.draw(canvas) - return bitmap - } catch (e: OutOfMemoryError) { - // TODO: Handle the error - return null - } - } - - private fun setBackClickListener() { - binding.btnSignupEmailSetProfileBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnSignupEmailSetProfileNext.setOnDebounceClickListener { - showLoadingDialog() - var profileImgFile: File? = null - profileImgFile = if (this::userProfileImageString.isInitialized) { - setUploadImagePath(userProfileImageExtension) - val originalBitmap = userProfileImageString.toUri().toBitmap().cropCenterBitmap() - val resizedBitmap = ImageResizeUtil.resizeBitmap( - originalBitmap = originalBitmap, - resizedWidth = USER_PROFILE_THUMBNAIL_RESIZE_SIZE, - resizedHeight = USER_PROFILE_THUMBNAIL_RESIZE_SIZE - ) - bitmapToFile(resizedBitmap, imagePath) - } else { // ๊ธฐ๋ณธ ํ”„๋กœํ•„ ์‚ฌ์ง„์œผ๋กœ ์„ค์ • - null - } - if (args.password != null) { - loginViewModel.requestSignupEmail( - args.email, - trimBlankText(binding.etSignupEmailSetProfileNickname.text), - args.password!!, - profileImgFile - ) - } else { - // ์นด์นด์˜ค ๊ณ„์ • ํšŒ์›๊ฐ€์ž… ์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ null - profileSettingViewModel.requestUpdateMyProfile( - trimBlankText(binding.etSignupEmailSetProfileNickname.text), - profileImgFile, - profileImgFile == null - ) - } - Toast.makeText( - requireContext(), - R.string.signup_email_alert_message_loading, - Toast.LENGTH_SHORT - ).show() - } - } - - private fun observeSignupCallback() { - loginViewModel.signupSuccess.observe(viewLifecycleOwner) { isSuccess -> - if (isSuccess.getContentIfNotHandled() == true) { - Navigation.findNavController(requireView()).navigateSafe( - currentDestinationId = R.id.SignupEmailSetProfileFragment, - action = R.id.action_signupEmailSetProfileFragment_to_signupEmailCompleteFragment, - args = SignupEmailSetProfileFragmentDirections.actionSignupEmailSetProfileFragmentToSignupEmailCompleteFragment( - trimBlankText(binding.etSignupEmailSetProfileNickname.text) - ).arguments - ) - } else if (isSuccess.getContentIfNotHandled() == false) { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - Toast.makeText( - requireContext(), - R.string.signup_email_alert_message_fail_network, - Toast.LENGTH_SHORT - ).show() - } - } - } - - private fun observeKakaoSignupCallback() { - profileSettingViewModel.updateSuccess.observe(viewLifecycleOwner) { isSuccess -> - if (isSuccess.getContentIfNotHandled() == true) { - Navigation.findNavController(requireView()).navigateSafe( - currentDestinationId = R.id.SignupEmailSetProfileFragment, - action = R.id.action_signupEmailSetProfileFragment_to_signupEmailCompleteFragment, - args = SignupEmailSetProfileFragmentDirections.actionSignupEmailSetProfileFragmentToSignupEmailCompleteFragment( - trimBlankText(binding.etSignupEmailSetProfileNickname.text) - ).arguments - ) - } else if (isSuccess.getContentIfNotHandled() == false) { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - Toast.makeText( - requireContext(), - R.string.signup_email_alert_message_fail_network, - Toast.LENGTH_SHORT - ).show() - } - } - } - - private fun showLoadingDialog() { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - LoadingAlertDialog.resizeDialogFragment(requireContext(), loadingAlertDialog) - } -} diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetProfileImageOptionFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetProfileImageOptionFragment.kt deleted file mode 100644 index 33936ff5..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/account/signup/SignupEmailSetProfileImageOptionFragment.kt +++ /dev/null @@ -1,187 +0,0 @@ -package daily.dayo.presentation.fragment.account.signup - -import android.Manifest -import android.app.Activity -import android.content.Intent -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.view.* -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.RequiresApi -import androidx.core.content.FileProvider -import androidx.fragment.app.DialogFragment -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.BuildConfig -import daily.dayo.presentation.R -import daily.dayo.presentation.databinding.FragmentSignupEmailSetProfileImageOptionBinding -import daily.dayo.presentation.common.dialog.DefaultDialogConfigure -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.image.ImageUploadUtil.extension -import daily.dayo.presentation.common.image.ImageUploadUtil.isPermitExtension -import daily.dayo.presentation.common.setOnDebounceClickListener -import java.io.File -import java.io.IOException -import java.text.SimpleDateFormat -import java.util.* - -class SignupEmailSetProfileImageOptionFragment : DialogFragment() { - private var binding by autoCleared() - private lateinit var currentTakenPhotoPath: String - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - isCancelable = true - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSignupEmailSetProfileImageOptionBinding.inflate(inflater, container, false) - // DialogFragment Radius ์„ค์ • - dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - // Android Version 4.4 ์ดํ•˜์—์„œ Blue Line์ด ์ƒ๋‹จ์— ๋‚˜ํƒ€๋Š” ๊ฒƒ ๋ฐฉ์ง€ - dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE) - dialog?.window?.setGravity(Gravity.BOTTOM) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setImageSelectGalleryClickListener() - setImageTakePhotoClickListener() - } - - override fun onResume() { - super.onResume() - resizeImageOptionDialogFragment() - } - - private fun resizeImageOptionDialogFragment() { - val params: ViewGroup.LayoutParams? = dialog?.window?.attributes - val deviceWidth = DefaultDialogConfigure.getDeviceWidthSize(requireContext()) - params?.width = (deviceWidth * 0.9).toInt() - dialog?.window?.attributes = params as WindowManager.LayoutParams - } - - private fun setImageSelectGalleryClickListener(){ - binding.layoutSignupEmailSetProfileImageOptionSelectGallery.setOnDebounceClickListener { - requestOpenGallery.launch( - PERMISSIONS_GALLERY - ) - } - } - private val requestOpenGallery = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - permissions.entries.forEach { - if (it.value == false) { - return@registerForActivityResult - } - } - openGallery() - } - private fun openGallery() { - val intent = Intent(Intent.ACTION_PICK) - intent.type = MediaStore.Images.Media.CONTENT_TYPE - requestSelectGalleryActivity.launch(intent) - } - @RequiresApi(Build.VERSION_CODES.O) - val requestSelectGalleryActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> - if (activityResult.resultCode == Activity.RESULT_OK) { - val data: Intent? = activityResult.data - // ํ˜ธ์ถœ๋œ ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ์ด๋ฏธ์ง€ ์„ ํƒ์‹œ, data์˜ data์†์„ฑ์œผ๋กœ ํ•ด๋‹น ์ด๋ฏธ์ง€์˜ Uri ์ „๋‹ฌ - val uri = data?.data!! - // ์ด๋ฏธ์ง€ ํŒŒ์ผ๊ณผ ํ•จ๊ป˜, ํŒŒ์ผ ํ™•์žฅ์ž๋„ ๊ฐ™์ด ์ €์žฅ - if (uri.extension(requireActivity().applicationContext.contentResolver).isPermitExtension) { - setMyProfileImage(uri.toString(), uri.extension(requireActivity().applicationContext.contentResolver)) - } else { - Toast.makeText( - requireContext(), - getString(R.string.write_post_upload_alert_message_image_fail_file_extension), - Toast.LENGTH_SHORT - ).show() - findNavController().popBackStack() - } - } - } - - private fun setImageTakePhotoClickListener() { - binding.layoutSignupEmailSetProfileImageOptionTakePhoto.setOnDebounceClickListener { - requestOpenCamera.launch( - PERMISSIONS_CAMERA - ) - } - } - private val requestOpenCamera = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - permissions.entries.forEach { - if (it.value == false) { - return@registerForActivityResult - } - } - openCamera() - } - private fun openCamera() { - val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - if(intent.resolveActivity(requireContext().packageManager)!=null) { - var photoFile: File?= null - val tmpDir: File?= requireContext().cacheDir - val timeStamp: String = SimpleDateFormat("yyyy-MM-d-HH-mm-ss", Locale.KOREA).format(Date()) - val photoFileName = "Capture_${timeStamp}_" - try { - val tmpPhoto = File.createTempFile(photoFileName, ".jpg", tmpDir) - currentTakenPhotoPath = tmpPhoto.absolutePath - photoFile = tmpPhoto - } catch (e: IOException) { - e.printStackTrace() - } - if(photoFile!= null){ - val photoURI=FileProvider.getUriForFile(Objects.requireNonNull(requireContext().applicationContext), BuildConfig.LIBRARY_PACKAGE_NAME+".fileprovider", photoFile) - intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI) - requestTakePhotoActivity.launch(intent) - } - } - } - @RequiresApi(Build.VERSION_CODES.O) - val requestTakePhotoActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> - if (activityResult.resultCode == Activity.RESULT_OK) { - val photoFile = File(currentTakenPhotoPath) - setMyProfileImage(Uri.fromFile(photoFile).toString(), "jpg") - } - } - - private fun setMyProfileImage(ImageString : String, fileExtension : String) { - findNavController().previousBackStackEntry?.savedStateHandle?.set("userProfileImageString", ImageString) - findNavController().previousBackStackEntry?.savedStateHandle?.set("fileExtension", fileExtension) - findNavController().popBackStack() - } - - companion object { - val PERMISSIONS_CAMERA = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - arrayOf( - Manifest.permission.CAMERA, - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_VIDEO - ) - } else { - arrayOf( - Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - } - val PERMISSIONS_GALLERY = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - arrayOf( - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_VIDEO - ) - } else { - arrayOf( - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/feed/FeedFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/feed/FeedFragment.kt deleted file mode 100644 index 072d61e8..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/feed/FeedFragment.kt +++ /dev/null @@ -1,200 +0,0 @@ -package daily.dayo.presentation.fragment.feed - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageButton -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.paging.LoadState -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import com.google.android.material.chip.Chip -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.domain.model.Post -import daily.dayo.presentation.R -import daily.dayo.presentation.activity.MainActivity -import daily.dayo.presentation.adapter.FeedListAdapter -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFeedBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import daily.dayo.presentation.viewmodel.FeedViewModel -import daily.dayo.presentation.viewmodel.SearchViewModel - -@AndroidEntryPoint -class FeedFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val accountViewModel by activityViewModels() - private val feedViewModel by activityViewModels() - private val searchViewModel by activityViewModels() - private var feedListAdapter: FeedListAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (savedInstanceState == null) - getFeedPostList() - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFeedBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setRvFeedListAdapter() - setFeedPostList() - setAdapterLoadStateListener() - setFeedPostClickListener() - setFeedEmptyButtonClickListener() - setFeedRefreshListener() - setBottomNavigationIconClickListener() - } - - override fun onResume() { - super.onResume() - binding.swipeRefreshLayoutFeed.isEnabled = true - } - - override fun onPause() { - super.onPause() - binding.swipeRefreshLayoutFeed.isEnabled = false - } - - private fun onDestroyBindingView() { - binding.rvFeedPost.adapter = null - glideRequestManager = null - feedListAdapter = null - } - - private fun setFeedRefreshListener() { - binding.swipeRefreshLayoutFeed.setOnRefreshListener { - feedListAdapter?.refresh() - } - } - - private fun setRvFeedListAdapter() { - feedListAdapter = glideRequestManager?.let { requestManager -> - FeedListAdapter(requestManager = requestManager, currentUserInfo = accountViewModel.getCurrentUserInfo()) - } - feedListAdapter?.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY - binding.rvFeedPost.adapter = feedListAdapter - } - - private fun getFeedPostList() { - feedViewModel.requestFeedList() - } - - private fun setFeedPostList() { - feedViewModel.feedList.observe(viewLifecycleOwner) { - binding.swipeRefreshLayoutFeed.isRefreshing = false - feedListAdapter?.submitData(viewLifecycleOwner.lifecycle, it) - } - } - - private fun setAdapterLoadStateListener() { - feedListAdapter?.addLoadStateListener { loadState -> - if (loadState.refresh is LoadState.NotLoading) { - val isListEmpty = (feedListAdapter?.itemCount ?: 0 == 0) - binding.isEmpty = isListEmpty - } - } - } - - private fun setFeedEmptyButtonClickListener() { - binding.btnFeedEmpty.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.FeedFragment, - action = R.id.action_feedFragment_to_homeFragment - ) - } - } - - private fun setFeedPostClickListener() { - feedListAdapter?.setOnItemClickListener(object : FeedListAdapter.OnItemClickListener { - override fun likePostClick(button: ImageButton, post: Post, position: Int) { - setFeedPostLikeClickListener(button, post, position) - } - - override fun likeCountClick(postId: Int) { - setLikeCountClickListener(postId) - } - - override fun bookmarkPostClick(button: ImageButton, post: Post, position: Int) { - setFeedPostBookmarkClickListener(button, post, position) - } - - override fun tagPostClick(chip: Chip) { - searchViewModel.searchKeyword = trimBlankText(chip.text.toString().substringAfter("#")) - findNavController().navigateSafe( - currentDestinationId = R.id.FeedFragment, - action = R.id.action_feedFragment_to_searchResultFragment - ) - } - }) - } - - private fun setFeedPostLikeClickListener(button: ImageButton, post: Post, position: Int) { - with(post) { - try { - if (!heart) { - feedViewModel.requestLikePost(postId!!) - } else { - feedViewModel.requestUnlikePost(post.postId!!) - } - } catch (postIdNullException: NullPointerException) { - Log.e(this@FeedFragment.tag, "PostId Null Exception Occurred") - getFeedPostList() - } - } - } - - fun setLikeCountClickListener(postId: Int) { - findNavController().navigateSafe( - currentDestinationId = R.id.FeedFragment, - action = FeedFragmentDirections.actionFeedFragmentToPostLikeUsersFragment(postId = postId) - ) - } - - private fun setFeedPostBookmarkClickListener(button: ImageButton, post: Post, position: Int) { - with(post) { - try { - if (bookmark == false) { - feedViewModel.requestBookmarkPost(postId = post.postId!!) - } else { - feedViewModel.requestDeleteBookmarkPost(postId = post.postId!!) - } - } catch (postIdNullException: NullPointerException) { - Log.e(this@FeedFragment.tag, "PostId Null Exception Occurred") - getFeedPostList() - } - } - } - - private fun setBottomNavigationIconClickListener() { - (requireActivity() as MainActivity).setBottomNavigationIconClickListener(R.id.FeedFragment) { - binding.swipeRefreshLayoutFeed.isRefreshing = true - getFeedPostList() - setScrollToTop(isSmoothScroll = true) - } - } - - private fun setScrollToTop(isSmoothScroll: Boolean = false) { - with(binding.rvFeedPost) { - if (isSmoothScroll) this.smoothScrollToPosition(0) - else this.scrollToPosition(0) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeDayoPickPostListFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeDayoPickPostListFragment.kt deleted file mode 100644 index 21e7c496..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeDayoPickPostListFragment.kt +++ /dev/null @@ -1,289 +0,0 @@ -package daily.dayo.presentation.fragment.home - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.domain.model.Category -import daily.dayo.domain.model.Post -import daily.dayo.presentation.R -import daily.dayo.presentation.activity.MainActivity -import daily.dayo.presentation.adapter.HomeDayoPickAdapter -import daily.dayo.presentation.common.GlideLoadUtil.HOME_POST_THUMBNAIL_SIZE -import daily.dayo.presentation.common.GlideLoadUtil.HOME_USER_THUMBNAIL_SIZE -import daily.dayo.presentation.common.GlideLoadUtil.loadImageBackground -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.common.toByteArray -import daily.dayo.presentation.databinding.FragmentHomeDayoPickPostListBinding -import daily.dayo.presentation.viewmodel.HomeViewModel -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -@AndroidEntryPoint -class HomeDayoPickPostListFragment : Fragment() { - private var binding by autoCleared() { onDestroyBindingView() } - private val homeViewModel by activityViewModels() - private var homeDayoPickAdapter: HomeDayoPickAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (savedInstanceState == null) - loadPosts(homeViewModel.currentDayoPickCategory) - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentHomeDayoPickPostListBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - startLoadingView() - setInitialCategory() - setRvDayoPickPostAdapter() - setDayoPickPostListCollect() - setPostLikeClickListener() - setEmptyViewActionClickListener() - setDayoPickPostListRefreshListener() - } - - override fun onResume() { - binding.swipeRefreshLayoutDayoPickPost.isEnabled = true - setBottomNavigationIconClickListener() - super.onResume() - } - - override fun onPause() { - super.onPause() - binding.swipeRefreshLayoutDayoPickPost.isEnabled = false - } - - private fun onDestroyBindingView() { - glideRequestManager = null - homeDayoPickAdapter = null - binding.rvDayopickPost.adapter = null - } - - private fun setDayoPickPostListRefreshListener() { - binding.swipeRefreshLayoutDayoPickPost.setOnRefreshListener { - loadPosts(homeViewModel.currentDayoPickCategory) - } - } - - private fun setInitialCategory() { - with(binding) { - radiogroupDayopickPostCategory.check( - when (homeViewModel.currentDayoPickCategory) { - Category.ALL -> radiobuttonDayopickPostCategoryAll.id - Category.SCHEDULER -> radiobuttonDayopickPostCategoryScheduler.id - Category.STUDY_PLANNER -> radiobuttonDayopickPostCategoryStudyplanner.id - Category.GOOD_NOTE -> radiobuttonDayopickPostCategoryDigital.id - Category.POCKET_BOOK -> radiobuttonDayopickPostCategoryPocketbook.id - Category.SIX_DIARY -> radiobuttonDayopickPostCategory6holediary.id - Category.ETC -> radiobuttonDayopickPostCategoryEtc.id - } - ) - } - } - - private fun setRvDayoPickPostAdapter() { - homeDayoPickAdapter = glideRequestManager?.let { requestManager -> - HomeDayoPickAdapter( - rankingShowing = true, - requestManager = requestManager - ) - } - homeDayoPickAdapter?.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY - binding.rvDayopickPost.adapter = homeDayoPickAdapter - } - - private fun setDayoPickPostListCollect() { - viewLifecycleOwner.lifecycleScope.launch { - homeViewModel.dayoPickPostList.observe(viewLifecycleOwner) { - // TODO : ํ•˜๋‹จ BottomNavigation์„ ํ†ตํ•ด Fragment๋ฅผ ์ด๋™ํ•˜๊ณ  ๋‚˜์„œ ๋Œ์•„์˜ค๊ณ ๋‚˜์„œ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์„ ํƒํ•˜๋ฉด observe๊ฐ€ ์ค‘๋ณตํ•ด์„œ ์ƒ๊ฒจ๋‚œ๋‹ค - // ํ•ด๋‹น LiveData ๊ฐ์ฒด๊ฐ€ 1๊ฐœ ๋” ์ƒ์„ฑ๋˜๋Š”๋“ฏ ํ•˜๋‹ค - when (it.status) { - Status.SUCCESS -> { - it.data?.let { postList -> - binding.swipeRefreshLayoutDayoPickPost.isRefreshing = false - loadInitialPostThumbnail(postList) - binding.layoutDayopickPostEmpty.isVisible = postList.isEmpty() - } - } - Status.LOADING -> { - } - Status.ERROR -> { - - } - } - } - } - with(binding) { - radiobuttonDayopickPostCategoryAll.setOnDebounceClickListener { - loadPosts(Category.ALL) - } - radiobuttonDayopickPostCategoryScheduler.setOnDebounceClickListener { - loadPosts(Category.SCHEDULER) - } - radiobuttonDayopickPostCategoryStudyplanner.setOnDebounceClickListener { - loadPosts(Category.STUDY_PLANNER) - } - radiobuttonDayopickPostCategoryPocketbook.setOnDebounceClickListener { - loadPosts(Category.POCKET_BOOK) - } - radiobuttonDayopickPostCategory6holediary.setOnDebounceClickListener { - loadPosts(Category.SIX_DIARY) - } - radiobuttonDayopickPostCategoryDigital.setOnDebounceClickListener { - loadPosts(Category.GOOD_NOTE) - } - radiobuttonDayopickPostCategoryEtc.setOnDebounceClickListener { - loadPosts(Category.ETC) - } - } - } - - private fun loadPosts(selectCategory: Category, isSmoothScroll: Boolean = false) { - with(homeViewModel) { - currentDayoPickCategory = selectCategory - requestDayoPickPostList() - } - - if (this.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) { - if (isSmoothScroll) binding.rvDayopickPost.smoothScrollToPosition(0) - else binding.rvDayopickPost.scrollToPosition(0) - } - } - - private fun setPostLikeClickListener() { - homeDayoPickAdapter?.setOnItemClickListener(object : - HomeDayoPickAdapter.OnItemClickListener { - override fun likePostClick(post: Post) { - with(post) { - try { - if (!heart) { - homeViewModel.requestLikePost(postId!!, isDayoPickLike = true) - } else { - homeViewModel.requestUnlikePost(postId!!, isDayoPickLike = true) - } - } catch (postIdNullException: NullPointerException) { - Log.e( - this@HomeDayoPickPostListFragment.tag, - "PostId Null Exception Occurred" - ) - loadPosts(homeViewModel.currentDayoPickCategory) - } - } - } - }) - } - - private fun setEmptyViewActionClickListener() { - binding.btnDayopickPostEmptyAction.setOnDebounceClickListener { - val homeViewPager = - requireParentFragment().requireView().findViewById(R.id.pager_home_post) - val current = homeViewPager.currentItem - if (current == 0) { - homeViewPager.setCurrentItem(1, true) - } else { - homeViewPager.setCurrentItem(current - 1, true) - } - } - } - - private fun loadInitialPostThumbnail(postList: List) { - val thumbnailImgList = emptyList().toMutableList() - val userImgList = emptyList().toMutableList() - - viewLifecycleOwner.lifecycleScope.launch { - for (i in 0 until (if (postList.size >= 6) 6 else postList.size)) { - thumbnailImgList.add(withContext(Dispatchers.IO) { - loadImageBackground( - context = requireContext(), - height = HOME_POST_THUMBNAIL_SIZE, - width = HOME_POST_THUMBNAIL_SIZE, - imgName = postList[i].thumbnailImage ?: "" - ) - }.toByteArray) - userImgList.add(withContext(Dispatchers.IO) { - loadImageBackground( - context = requireContext(), - height = HOME_USER_THUMBNAIL_SIZE, - width = HOME_USER_THUMBNAIL_SIZE, - imgName = postList[i].userProfileImage ?: "" - ) - }.toByteArray) - } - }.invokeOnCompletion { throwable -> - when (throwable) { - is CancellationException -> Log.e("Image Loading", "CANCELLED") - null -> { - for (i in 0 until (if (postList.size >= 6) 6 else postList.size)) { - with(postList[i]) { - preLoadThumbnail = thumbnailImgList[i] - preLoadUserImg = userImgList[i] - } - } - homeDayoPickAdapter?.submitList(postList.toMutableList()) - stopLoadingView() - thumbnailImgList.clear() - userImgList.clear() - } - } - } - } - - private fun setBottomNavigationIconClickListener() { - val currentViewPagerPosition = - requireParentFragment().requireView() - .findViewById(R.id.pager_home_post) - .currentItem - - (requireActivity() as MainActivity).setBottomNavigationIconClickListener(reselectedIconId = R.id.HomeFragment) { - if (currentViewPagerPosition == HOME_DAYOPICK_PAGE_TAB_ID) { - binding.swipeRefreshLayoutDayoPickPost.isRefreshing = true - loadPosts(homeViewModel.currentDayoPickCategory, isSmoothScroll = true) - } - } - } - - private fun startLoadingView() { - with(binding) { - with(layoutDayopickPostShimmer) { - startShimmer() - visibility = View.VISIBLE - } - rvDayopickPost.visibility = View.INVISIBLE - } - } - - private fun stopLoadingView() { - with(binding) { - with(layoutDayopickPostShimmer) { - stopShimmer() - visibility = View.GONE - } - rvDayopickPost.visibility = View.VISIBLE - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeFragment.kt deleted file mode 100644 index 8e3e3eb5..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeFragment.kt +++ /dev/null @@ -1,99 +0,0 @@ -package daily.dayo.presentation.fragment.home - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import androidx.viewpager2.widget.ViewPager2 -import com.google.android.material.tabs.TabLayoutMediator -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.adapter.HomeFragmentPagerStateAdapter -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentHomeBinding - -const val HOME_DAYOPICK_PAGE_TAB_ID = 0 -const val HOME_NEW_PAGE_TAB_ID = 1 - -@AndroidEntryPoint -class HomeFragment : Fragment() { - private var binding by autoCleared(onDestroy = { - onDestroyBindingView() - }) - private var mediator: TabLayoutMediator? = null - private var pagerAdapter: HomeFragmentPagerStateAdapter? = null - private val pageChangeCallBack = object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - pagerAdapter?.refreshFragment(position, pagerAdapter!!.fragments[position]) - } - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentHomeBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initViewPager() - setSearchClickListener() - } - - private fun onDestroyBindingView() { - mediator?.detach() - mediator = null - pagerAdapter = null - with(binding.pagerHomePost) { - unregisterOnPageChangeCallback(pageChangeCallBack) - adapter = null - } - } - - private fun setSearchClickListener() { - binding.btnPostSearch.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.HomeFragment, - action = R.id.action_homeFragment_to_searchFragment - ) - } - } - - private fun initViewPager() { - pagerAdapter = - HomeFragmentPagerStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) - pagerAdapter?.addFragment(HomeDayoPickPostListFragment()) - pagerAdapter?.addFragment(HomeNewPostListFragment()) - for (i in 0 until pagerAdapter!!.itemCount) { - pagerAdapter?.refreshFragment(i, pagerAdapter!!.fragments[i]) - } - - with(binding.pagerHomePost) { - isUserInputEnabled = false // DISABLE SWIPE - adapter = pagerAdapter - registerOnPageChangeCallback(pageChangeCallBack) - } - - initTabLayout() - } - - private fun initTabLayout() { - mediator = TabLayoutMediator( - binding.tabsActionbarHomeCategory, - binding.pagerHomePost - ) { tab, position -> - when (position) { - HOME_DAYOPICK_PAGE_TAB_ID -> tab.text = "DAYO PICK" - HOME_NEW_PAGE_TAB_ID -> tab.text = "NEW" - } - } - mediator?.attach() - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeNewPostListFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeNewPostListFragment.kt deleted file mode 100644 index 57ddf248..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/home/HomeNewPostListFragment.kt +++ /dev/null @@ -1,284 +0,0 @@ -package daily.dayo.presentation.fragment.home - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.R -import daily.dayo.presentation.common.GlideLoadUtil -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.common.toByteArray -import daily.dayo.presentation.databinding.FragmentHomeNewPostListBinding -import daily.dayo.domain.model.Category -import daily.dayo.domain.model.Post -import daily.dayo.presentation.adapter.HomeNewAdapter -import daily.dayo.presentation.viewmodel.HomeViewModel -import com.google.android.material.bottomnavigation.BottomNavigationView -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.activity.MainActivity -import daily.dayo.presentation.common.GlideLoadUtil.HOME_POST_THUMBNAIL_SIZE -import daily.dayo.presentation.common.GlideLoadUtil.HOME_USER_THUMBNAIL_SIZE -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -@AndroidEntryPoint -class HomeNewPostListFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val homeViewModel by activityViewModels() - private var homeNewAdapter: HomeNewAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (savedInstanceState == null) - loadPosts(homeViewModel.currentNewCategory) - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentHomeNewPostListBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - startLoadingView() - setInitialCategory() - setRvNewPostAdapter() - setNewPostListCollect() - setPostLikeClickListener() - setEmptyViewActionClickListener() - setNewPostListRefreshListener() - } - - override fun onResume() { - binding.swipeRefreshLayoutNewPost.isEnabled = true - setBottomNavigationIconClickListener() - super.onResume() - } - - override fun onPause() { - super.onPause() - binding.swipeRefreshLayoutNewPost.isEnabled = false - } - - private fun onDestroyBindingView() { - glideRequestManager = null - homeNewAdapter = null - binding.rvNewPost.adapter = null - } - - private fun setNewPostListRefreshListener() { - binding.swipeRefreshLayoutNewPost.setOnRefreshListener { - loadPosts(homeViewModel.currentNewCategory) - } - } - - private fun setInitialCategory() { - with(binding) { - radiogroupNewPostCategory.check( - when (homeViewModel.currentNewCategory) { - Category.ALL -> radiobuttonNewPostCategoryAll.id - Category.SCHEDULER -> radiobuttonNewPostCategoryScheduler.id - Category.STUDY_PLANNER -> radiobuttonNewPostCategoryStudyplanner.id - Category.GOOD_NOTE -> radiobuttonNewPostCategoryDigital.id - Category.POCKET_BOOK -> radiobuttonNewPostCategoryPocketbook.id - Category.SIX_DIARY -> radiobuttonNewPostCategory6holediary.id - Category.ETC -> radiobuttonNewPostCategoryEtc.id - } - ) - } - } - - private fun setRvNewPostAdapter() { - homeNewAdapter = glideRequestManager?.let { requestManager -> - HomeNewAdapter( - rankingShowing = false, - requestManager = requestManager - ) - } - homeNewAdapter?.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY - binding.rvNewPost.adapter = homeNewAdapter - } - - private fun setNewPostListCollect() { - viewLifecycleOwner.lifecycleScope.launch { - homeViewModel.newPostList.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { postList -> - binding.swipeRefreshLayoutNewPost.isRefreshing = false - loadPostThumbnail(postList) - binding.layoutNewPostEmpty.isVisible = postList.isEmpty() - } - } - - Status.LOADING -> { - } - - Status.ERROR -> { - - } - } - } - } - with(binding) { - radiobuttonNewPostCategoryAll.setOnDebounceClickListener { - loadPosts(Category.ALL) - } - radiobuttonNewPostCategoryScheduler.setOnDebounceClickListener { - loadPosts(Category.SCHEDULER) - } - radiobuttonNewPostCategoryStudyplanner.setOnDebounceClickListener { - loadPosts(Category.STUDY_PLANNER) - } - radiobuttonNewPostCategoryPocketbook.setOnDebounceClickListener { - loadPosts(Category.POCKET_BOOK) - } - radiobuttonNewPostCategory6holediary.setOnDebounceClickListener { - loadPosts(Category.SIX_DIARY) - } - radiobuttonNewPostCategoryDigital.setOnDebounceClickListener { - loadPosts(Category.GOOD_NOTE) - } - radiobuttonNewPostCategoryEtc.setOnDebounceClickListener { - loadPosts(Category.ETC) - } - } - } - - private fun loadPosts(selectCategory: Category, isSmoothScroll: Boolean = false) { - with(homeViewModel) { - currentNewCategory = selectCategory - requestNewPostList() - } - - if (this.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) { - if (isSmoothScroll) binding.rvNewPost.smoothScrollToPosition(0) - else binding.rvNewPost.scrollToPosition(0) - } - } - - private fun setPostLikeClickListener() { - homeNewAdapter?.setOnItemClickListener(object : - HomeNewAdapter.OnItemClickListener { - override fun likePostClick(post: Post) { - with(post) { - try { - if (!heart) { - homeViewModel.requestLikePost(postId!!, false) - } else { - homeViewModel.requestUnlikePost(post.postId!!, false) - } - } catch (postIdNullException: NullPointerException) { - Log.e(this@HomeNewPostListFragment.tag, "PostId Null Exception Occurred") - loadPosts(homeViewModel.currentNewCategory) - } - } - } - }) - } - - private fun setEmptyViewActionClickListener() { - binding.btnNewPostEmptyAction.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.HomeFragment, - action = R.id.action_homeFragment_to_writeFragment - ) - } - } - - private fun loadPostThumbnail(postList: List) { - val thumbnailImgList = emptyList().toMutableList() - val userImgList = emptyList().toMutableList() - - viewLifecycleOwner.lifecycleScope.launch { - for (i in 0 until (if (postList.size >= 6) 6 else postList.size)) { - thumbnailImgList.add(withContext(Dispatchers.IO) { - GlideLoadUtil.loadImageBackground( - context = requireContext(), - height = HOME_POST_THUMBNAIL_SIZE, - width = HOME_POST_THUMBNAIL_SIZE, - imgName = postList[i].thumbnailImage ?: "" - ) - }.toByteArray) - userImgList.add(withContext(Dispatchers.IO) { - GlideLoadUtil.loadImageBackground( - context = requireContext(), - height = HOME_USER_THUMBNAIL_SIZE, - width = HOME_USER_THUMBNAIL_SIZE, - imgName = postList[i].userProfileImage ?: "" - ) - }.toByteArray) - } - }.invokeOnCompletion { throwable -> - when (throwable) { - is CancellationException -> Log.e("Image Loading", "CANCELLED") - null -> { - var loadedPostList = postList.toMutableList() - for (i in 0 until (if (postList.size >= 6) 6 else postList.size)) { - loadedPostList[i].preLoadThumbnail = thumbnailImgList[i] - loadedPostList[i].preLoadUserImg = userImgList[i] - } - homeNewAdapter?.submitList(postList.toMutableList()) - stopLoadingView() - thumbnailImgList.clear() - userImgList.clear() - } - } - } - } - - private fun setBottomNavigationIconClickListener() { - val currentViewPagerPosition = - requireParentFragment().requireView() - .findViewById(R.id.pager_home_post) - .currentItem - - (requireActivity() as MainActivity).setBottomNavigationIconClickListener(reselectedIconId = R.id.HomeFragment) { - if (currentViewPagerPosition == HOME_NEW_PAGE_TAB_ID) { - binding.swipeRefreshLayoutNewPost.isRefreshing = true - loadPosts(homeViewModel.currentNewCategory, isSmoothScroll = true) - } - } - } - - private fun startLoadingView() { - with(binding) { - with(layoutNewPostShimmer) { - startShimmer() - visibility = View.VISIBLE - } - rvNewPost.visibility = View.INVISIBLE - } - } - - private fun stopLoadingView() { - with(binding) { - with(layoutNewPostShimmer) { - stopShimmer() - visibility = View.GONE - } - rvNewPost.visibility = View.VISIBLE - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderEditFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderEditFragment.kt deleted file mode 100644 index 77851b36..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderEditFragment.kt +++ /dev/null @@ -1,293 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.folder - -import android.app.AlertDialog -import android.graphics.Bitmap -import android.graphics.ImageDecoder -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.text.Editable -import android.text.SpannableStringBuilder -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import androidx.core.net.toUri -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.domain.model.Privacy -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.GlideLoadUtil.loadImageView -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.TextLimitUtil -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFolderSettingAddBinding -import daily.dayo.presentation.viewmodel.FolderViewModel -import kotlinx.coroutines.launch -import java.io.File -import java.io.FileOutputStream -import java.io.OutputStream -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -class FolderEditFragment : Fragment() { - private var binding by autoCleared { - onDestroyBindingView() - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val folderViewModel by activityViewModels() - private val args by navArgs() - private var glideRequestManager: RequestManager? = null - private lateinit var imageUri: String - var thumbnailImgBitmap: Bitmap? = null - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFolderSettingAddBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - setKeyboardMode() - initializeLoadingDialog() - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setFolderInfoDescription() - setBackButtonClickListener() - setConfirmButtonClickListener() - setFolderSettingThumbnailOptionClickListener() - observeNavigationFolderSettingImageCallBack() - verifyFolderName() - confirmFolderSubheading() - } - - - private fun onDestroyBindingView() { - glideRequestManager = null - } - - private fun setKeyboardMode() { - requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) - } - - private fun initializeLoadingDialog() { - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - } - - private fun setFolderInfoDescription() { - val layoutParams = ViewGroup.MarginLayoutParams( - ViewGroup.MarginLayoutParams.MATCH_PARENT, - ViewGroup.MarginLayoutParams.MATCH_PARENT - ) - - folderViewModel.requestFolderInfo(args.folderId) - viewLifecycleOwner.lifecycleScope.launch { - folderViewModel.folderInfo.observe(viewLifecycleOwner) { it -> - when (it.status) { - Status.SUCCESS -> { - it.data?.let { folder -> - binding.tvFolderSettingAddTitle.text = - getString(R.string.folder_edit_title) - binding.etFolderSettingAddSetTitle.text = - SpannableStringBuilder(folder.name) - folder.subheading?.let { subheading -> - binding.etFolderSettingAddSetSubheading.text = - SpannableStringBuilder(subheading) - } - when (folder.privacy) { - Privacy.ALL -> binding.radiobuttonFolderSettingAddSetPrivateAll.isChecked = - true - - Privacy.ONLY_ME -> binding.radiobuttonFolderSettingAddSetPrivateOnlyMe.isChecked = - true - } - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - glideRequestManager?.let { requestManager -> - loadImageView( - requestManager = requestManager, - width = layoutParams.width, - height = 148, - imgName = folder.thumbnailImage, - imgView = binding.ivFolderSettingThumbnail - ) - } - } - } - } - } - - else -> {} - } - } - } - } - - private fun setBackButtonClickListener() { - binding.btnFolderSettingAddBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setConfirmButtonClickListener() { - binding.tvFolderSettingAddConfirm.setOnDebounceClickListener { it -> - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - val name: String = binding.etFolderSettingAddSetTitle.text.toString() - val subheading: String = binding.etFolderSettingAddSetSubheading.text.toString() - val privacy: Privacy = - when (binding.radiogroupFolderSettingAddSetPrivate.checkedRadioButtonId) { - binding.radiobuttonFolderSettingAddSetPrivateAll.id -> Privacy.ALL - binding.radiobuttonFolderSettingAddSetPrivateOnlyMe.id -> Privacy.ONLY_ME - else -> Privacy.ALL - } - - if (this::imageUri.isInitialized) { - // ํด๋” ์ปค๋ฒ„ ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ - val thumbnailImg = thumbnailImgBitmap?.let { bitmapToFile(it) } - folderViewModel.requestEditFolder( - args.folderId, - name, - privacy, - subheading, - true, - thumbnailImg - ) - } else { - // ํด๋” ์ปค๋ฒ„ ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์Œ - folderViewModel.requestEditFolder( - args.folderId, - name, - privacy, - subheading, - false, - null - ) - } - - folderViewModel.editSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - findNavController().navigateUp() - } else { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - } - } - } - - private fun setFolderSettingThumbnailOptionClickListener() { - binding.ivFolderSettingThumbnail.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.FolderEditFragment, - action = R.id.action_folderEditFragment_to_folderSettingAddImageOptionFragment - ) - } - } - - private fun observeNavigationFolderSettingImageCallBack() { - findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData("imageUri") - ?.observe(viewLifecycleOwner) { - imageUri = it - if (this::imageUri.isInitialized) { - if (imageUri == "") { - glideRequestManager?.load(R.drawable.ic_folder_thumbnail_empty) - ?.centerCrop() - ?.into(binding.ivFolderSettingThumbnail) - thumbnailImgBitmap = null - } else { - thumbnailImgBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoder.decodeBitmap( - ImageDecoder.createSource( - requireContext().contentResolver, - imageUri.toUri() - ) - ) - } else { - MediaStore.Images.Media.getBitmap( - requireContext().contentResolver, - imageUri.toUri() - ) - } - glideRequestManager?.load(imageUri)?.into(binding.ivFolderSettingThumbnail) - } - } - } - } - - private fun bitmapToFile(bitmap: Bitmap): File { - val imageFileTimeFormat = SimpleDateFormat("yyyy-MM-d-HH-mm-ss", Locale.KOREA) - val fileName = - imageFileTimeFormat.format(Date(System.currentTimeMillis())).toString() + ".jpg" - val cacheDir = requireContext().cacheDir.toString() - val file = File("$cacheDir/$fileName") - var out: OutputStream? = null - try { - file.createNewFile() - out = FileOutputStream(file) - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) - } finally { - out?.close() - } - return file - } - - private fun verifyFolderName() { - val maxLength = 15 - binding.etFolderSettingAddSetTitle.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - val text = s.toString() - val newText = TextLimitUtil.trimToMaxLength(text, maxLength) - if (text != newText) { - binding.etFolderSettingAddSetTitle.setText(newText) - binding.etFolderSettingAddSetTitle.setSelection(newText.length) - } - - if (trimBlankText(s).isEmpty()) { - ButtonActivation.setTextViewConfirmButtonInactive( - requireContext(), - binding.tvFolderSettingAddConfirm - ) - } else { - ButtonActivation.setTextViewConfirmButtonActive( - requireContext(), - binding.tvFolderSettingAddConfirm - ) - } - } - }) - } - - private fun confirmFolderSubheading() { - val subheadingMaxLength = 20 - binding.etFolderSettingAddSetSubheading.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - val text = s.toString() - val newText = TextLimitUtil.trimToMaxLength(text, subheadingMaxLength) - if (text != newText) { - binding.etFolderSettingAddSetSubheading.setText(newText) - binding.etFolderSettingAddSetSubheading.setSelection(newText.length) - } - } - }) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderFragment.kt deleted file mode 100644 index b90454f5..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderFragment.kt +++ /dev/null @@ -1,147 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.folder - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import androidx.paging.LoadState -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.R -import daily.dayo.presentation.adapter.FolderPostListAdapter -import daily.dayo.presentation.common.GlideLoadUtil.loadImageView -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFolderBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import daily.dayo.presentation.viewmodel.FolderViewModel - -class FolderFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val accountViewModel by activityViewModels() - private val folderViewModel by activityViewModels() - private val args by navArgs() - private var folderPostListAdapter: FolderPostListAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (savedInstanceState == null) - getFolderPostList() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFolderBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setBackButtonClickListener() - setFolderOptionClickListener() - setRvFolderPostListAdapter() - setFolderPostList() - setAdapterLoadStateListener() - setFolderDetail() - } - - private fun onDestroyBindingView() { - glideRequestManager = null - folderPostListAdapter = null - binding.rvFolderPost.adapter = null - } - - private fun setBackButtonClickListener() { - binding.btnFolderBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setFolderOptionClickListener() { - binding.btnFolderOption.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.FolderFragment, - action = R.id.action_folderFragment_to_folderOptionFragment, - args = FolderFragmentDirections.actionFolderFragmentToFolderOptionFragment( - args.folderId - ).arguments - ) - } - } - - private fun setFolderDetail() { - val layoutParams = ViewGroup.MarginLayoutParams( - ViewGroup.MarginLayoutParams.MATCH_PARENT, - ViewGroup.MarginLayoutParams.MATCH_PARENT - ) - - folderViewModel.requestFolderInfo(args.folderId) - folderViewModel.folderInfo.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { folder -> - binding.folder = folder - binding.isMine = folder.memberId == accountViewModel.getCurrentUserInfo().memberId - glideRequestManager?.let { requestManager -> - loadImageView( - requestManager = requestManager, - width = layoutParams.width, - height = 200, - imgName = folder.thumbnailImage, - imgView = binding.imgFolderThumbnail - ) - } - } - } - - else -> {} - } - } - } - - private fun setRvFolderPostListAdapter() { - folderPostListAdapter = glideRequestManager?.let { requestManager -> - FolderPostListAdapter(requestManager = requestManager) - } - binding.rvFolderPost.adapter = folderPostListAdapter - } - - private fun getFolderPostList() { - folderViewModel.requestFolderPostList(args.folderId) - } - - private fun setFolderPostList() { - folderViewModel.folderPostList.observe(viewLifecycleOwner) { - folderPostListAdapter?.submitData(viewLifecycleOwner.lifecycle, it) - } - } - - private fun setAdapterLoadStateListener() { - var isInitialLoad = false - folderPostListAdapter?.addLoadStateListener { loadState -> - if (loadState.refresh is LoadState.NotLoading && !isInitialLoad) { - if (loadState.append is LoadState.NotLoading) { - completeLoadPost() - isInitialLoad = true - } - } - } - } - - private fun completeLoadPost() { - binding.layoutFolderPostShimmer.stopShimmer() - binding.layoutFolderPostShimmer.visibility = View.GONE - binding.rvFolderPost.visibility = View.VISIBLE - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderOptionFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderOptionFragment.kt deleted file mode 100644 index 8837190f..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderOptionFragment.kt +++ /dev/null @@ -1,102 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.folder - -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.os.Bundle -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.Window -import android.view.WindowManager -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import daily.dayo.presentation.R -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.DefaultDialogConfigure -import daily.dayo.presentation.common.dialog.DefaultDialogExplanationConfirm -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFolderOptionBinding -import daily.dayo.presentation.viewmodel.FolderViewModel - -class FolderOptionFragment : DialogFragment() { - private var binding by autoCleared() - private val args by navArgs() - private val folderViewModel by activityViewModels() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFolderOptionBinding.inflate(inflater, container, false) - - dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE) - dialog?.window?.setGravity(Gravity.BOTTOM) - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setEditClickListenter() - setDeleteClickListener() - } - - override fun onResume() { - super.onResume() - resizePostOptionDialogFragment() - } - - private fun resizePostOptionDialogFragment() { - val params: ViewGroup.LayoutParams? = dialog?.window?.attributes - val deviceWidth = DefaultDialogConfigure.getDeviceWidthSize(requireContext()) - params?.width = (deviceWidth * 0.9).toInt() - dialog?.window?.attributes = params as WindowManager.LayoutParams - } - - private fun setEditClickListenter() { - binding.layoutFolderOptionEdit.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.FolderOptionFragment, - action = R.id.action_folderOptionFragment_to_folderEditFragment, - args = FolderOptionFragmentDirections.actionFolderOptionFragmentToFolderEditFragment(args.folderId).arguments - ) - } - } - - private fun setDeleteClickListener() { - binding.layoutFolderOptionDelete.setOnDebounceClickListener { - val mAlertDialog = DefaultDialogExplanationConfirm.createDialog(requireContext(), - R.string.folder_delete_description_message, - R.string.folder_delete_explanation_message, - true, - true, - R.string.confirm, - R.string.cancel, - { folderDelete() }, - { findNavController().popBackStack() }) - if (!mAlertDialog.isShowing) { - mAlertDialog.show() - DefaultDialogConfigure.dialogResize(requireContext(), mAlertDialog, 0.7f, 0.21f) - } - } - } - - private fun folderDelete() { - folderViewModel.requestDeleteFolder(args.folderId) - folderViewModel.deleteSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - findNavController().navigateSafe( - currentDestinationId = R.id.FolderOptionFragment, - action = R.id.action_folderOptionFragment_to_myPageFragment, - args = FolderOptionFragmentDirections.actionFolderOptionFragmentToMyPageFragment().arguments - ) - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderSettingAddFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderSettingAddFragment.kt deleted file mode 100644 index be74ebd8..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderSettingAddFragment.kt +++ /dev/null @@ -1,213 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.folder - -import android.app.AlertDialog -import android.graphics.Bitmap -import android.graphics.ImageDecoder -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.text.Editable -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import android.widget.Toast -import androidx.core.net.toUri -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.domain.model.Privacy -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.TextLimitUtil -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.image.ImageResizeUtil -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFolderSettingAddBinding -import daily.dayo.presentation.viewmodel.FolderViewModel -import java.io.File -import java.io.FileOutputStream -import java.io.OutputStream -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -class FolderSettingAddFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - onDestroyBindingView() - } - private val folderViewModel by activityViewModels() - private var glideRequestManager: RequestManager? = null - private lateinit var imageUri: String - private var thumbnailImgBitmap: Bitmap? = null - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFolderSettingAddBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - setBackButtonClickListener() - setConfirmButtonClickListener() - setFolderSettingThumbnailOptionClickListener() - observeNavigationFolderSettingImageCallBack() - confirmFolderSubheading() - verifyFolderName() - return binding.root - } - - private fun onDestroyBindingView() { - glideRequestManager = null - } - - private fun setBackButtonClickListener() { - binding.btnFolderSettingAddBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setConfirmButtonClickListener() { - binding.tvFolderSettingAddConfirm.setOnDebounceClickListener { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - val name: String = binding.etFolderSettingAddSetTitle.text.toString() - val subheading: String = binding.etFolderSettingAddSetSubheading.text.toString() - val privacy: Privacy = - when (binding.radiogroupFolderSettingAddSetPrivate.checkedRadioButtonId) { - binding.radiobuttonFolderSettingAddSetPrivateAll.id -> Privacy.ALL - binding.radiobuttonFolderSettingAddSetPrivateOnlyMe.id -> Privacy.ONLY_ME - else -> Privacy.ALL - } - val thumbnailImg = thumbnailImgBitmap?.let { - val resizedThumbnailImg = ImageResizeUtil.resizeBitmap( - originalBitmap = it, - resizedWidth = 480, - resizedHeight = 240 - ) - bitmapToFile(resizedThumbnailImg) - } - - folderViewModel.requestCreateFolder(name, privacy, subheading, thumbnailImg) - folderViewModel.folderAddSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - findNavController().popBackStack() - } else if (it.getContentIfNotHandled() == false) { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - Toast.makeText( - requireContext(), - R.string.folder_add_message_fail, - Toast.LENGTH_SHORT - ).show() - } - } - } - } - - private fun setFolderSettingThumbnailOptionClickListener() { - binding.ivFolderSettingThumbnail.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.FolderSettingAddFragment, - action = R.id.action_folderSettingAddFragment_to_folderSettingAddImageOptionFragment - ) - } - } - - private fun observeNavigationFolderSettingImageCallBack() { - findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData("imageUri") - ?.observe(viewLifecycleOwner) { - imageUri = it - if (this::imageUri.isInitialized) { - if (imageUri == "") { - glideRequestManager?.load(R.drawable.ic_folder_thumbnail_empty)?.centerCrop() - ?.into(binding.ivFolderSettingThumbnail) - thumbnailImgBitmap = null - } else { - thumbnailImgBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoder.decodeBitmap( - ImageDecoder.createSource( - requireContext().contentResolver, - imageUri.toUri() - ) - ) - } else { - MediaStore.Images.Media.getBitmap( - requireContext().contentResolver, - imageUri.toUri() - ) - } - glideRequestManager?.load(imageUri)?.into(binding.ivFolderSettingThumbnail) - } - } - } - } - - private fun bitmapToFile(bitmap: Bitmap): File { - val imageFileTimeFormat = SimpleDateFormat("yyyy-MM-d-HH-mm-ss", Locale.KOREA) - val fileName = - imageFileTimeFormat.format(Date(System.currentTimeMillis())).toString() + ".jpg" - val cacheDir = requireContext().cacheDir.toString() - val file = File("$cacheDir/$fileName") - var out: OutputStream? = null - try { - file.createNewFile() - out = FileOutputStream(file) - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) - } finally { - out?.close() - } - return file - } - - private fun verifyFolderName() { - val maxLength = 15 - binding.etFolderSettingAddSetTitle.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - val text = s.toString() - val newText = TextLimitUtil.trimToMaxLength(text, maxLength) - if (text != newText) { - binding.etFolderSettingAddSetTitle.setText(newText) - binding.etFolderSettingAddSetTitle.setSelection(newText.length) - } - - if (trimBlankText(s).isEmpty()) { - ButtonActivation.setTextViewConfirmButtonInactive( - requireContext(), - binding.tvFolderSettingAddConfirm - ) - } else { - ButtonActivation.setTextViewConfirmButtonActive( - requireContext(), - binding.tvFolderSettingAddConfirm - ) - } - } - }) - } - - private fun confirmFolderSubheading() { - val subheadingMaxLength = 20 - binding.etFolderSettingAddSetSubheading.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - val text = s.toString() - val newText = TextLimitUtil.trimToMaxLength(text, subheadingMaxLength) - if (text != newText) { - binding.etFolderSettingAddSetSubheading.setText(newText) - binding.etFolderSettingAddSetSubheading.setSelection(newText.length) - } - } - }) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderSettingEditImageOptionFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderSettingEditImageOptionFragment.kt deleted file mode 100644 index b3007546..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderSettingEditImageOptionFragment.kt +++ /dev/null @@ -1,171 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.folder - -import android.Manifest -import android.app.Activity -import android.content.Intent -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.view.* -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.RequiresApi -import androidx.core.content.FileProvider -import androidx.fragment.app.DialogFragment -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.BuildConfig -import daily.dayo.presentation.databinding.FragmentFolderSettingEditImageOptionBinding -import daily.dayo.presentation.common.dialog.DefaultDialogConfigure -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.setOnDebounceClickListener -import java.io.File -import java.io.IOException -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.Objects - -class FolderSettingEditImageOptionFragment : DialogFragment() { - private var binding by autoCleared() - private lateinit var currentTakenPhotoPath: String - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - isCancelable = true - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFolderSettingEditImageOptionBinding.inflate(inflater, container, false) - - dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE) - dialog?.window?.setGravity(Gravity.BOTTOM) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setImageSelectGalleryClickListener() - setImageTakePhotoClickListener() - setImageResetClickListener() - } - - override fun onResume() { - super.onResume() - resizeOptionDialogFragment() - } - - private fun resizeOptionDialogFragment() { - val params: ViewGroup.LayoutParams? = dialog?.window?.attributes - val deviceWidth = DefaultDialogConfigure.getDeviceWidthSize(requireContext()) - params?.width = (deviceWidth * 0.9).toInt() - dialog?.window?.attributes = params as WindowManager.LayoutParams - } - - private fun setImageSelectGalleryClickListener() { - binding.layoutFolderSettingEditImageOptionSelectGallery.setOnDebounceClickListener { - val intent = Intent(Intent.ACTION_PICK) - intent.data = MediaStore.Images.Media.EXTERNAL_CONTENT_URI - intent.type = "image/*" - startForResult.launch(intent) - } - } - - private fun setImageResetClickListener() { - binding.layoutFolderSettingEditImageOptionReset.setOnDebounceClickListener { - setFolderCoverImage("") - } - } - - private val startForResult = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - if (result.resultCode == Activity.RESULT_OK) { - val intent = result.data - val uri = intent?.data!! - setFolderCoverImage(uri.toString()) - } else { - findNavController().popBackStack() - } - } - - private fun setImageTakePhotoClickListener() { - binding.layoutFolderSettingEditImageOptionCamera.setOnDebounceClickListener { - requestOpenCamera.launch( - PERMISSIONS_CAMERA - ) - } - } - - private val requestOpenCamera = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - permissions.entries.forEach { - if (it.value == false) { - return@registerForActivityResult - } - } - openCamera() - } - - private fun openCamera() { - val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - if (intent.resolveActivity(requireContext().packageManager) != null) { - var photoFile: File? = null - val tmpDir: File? = requireContext().cacheDir - val timeStamp: String = - SimpleDateFormat("yyyy-MM-d-HH-mm-ss", Locale.KOREA).format(Date()) - val photoFileName = "Capture_${timeStamp}_" - try { - val tmpPhoto = File.createTempFile(photoFileName, ".jpg", tmpDir) - currentTakenPhotoPath = tmpPhoto.absolutePath - photoFile = tmpPhoto - } catch (e: IOException) { - e.printStackTrace() - } - if (photoFile != null) { - val photoURI = FileProvider.getUriForFile( - Objects.requireNonNull(requireContext().applicationContext), - BuildConfig.LIBRARY_PACKAGE_NAME + ".fileprovider", - photoFile - ) - intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI) - requestTakePhotoActivity.launch(intent) - } - } - } - - @RequiresApi(Build.VERSION_CODES.O) - val requestTakePhotoActivity = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> - if (activityResult.resultCode == Activity.RESULT_OK) { - val photoFile = File(currentTakenPhotoPath) - setFolderCoverImage(Uri.fromFile(photoFile).toString()) - } - } - - private fun setFolderCoverImage(coverImageUri: String) { - findNavController().previousBackStackEntry?.savedStateHandle?.set("imageUri", coverImageUri) - findNavController().popBackStack() - } - - companion object { - val PERMISSIONS_CAMERA = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - arrayOf( - Manifest.permission.CAMERA, - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_VIDEO - ) - } else { - arrayOf( - Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderSettingFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderSettingFragment.kt deleted file mode 100644 index 4afdf053..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/folder/FolderSettingFragment.kt +++ /dev/null @@ -1,175 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.folder - -import android.app.AlertDialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.Observer -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView -import daily.dayo.domain.model.FolderOrder -import daily.dayo.presentation.R -import daily.dayo.presentation.adapter.FolderSettingAdapter -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.ItemTouchHelperCallback -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFolderSettingBinding -import daily.dayo.presentation.viewmodel.FolderViewModel -import kotlinx.coroutines.launch - -class FolderSettingFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - onDestroyBindingView() - } - private val folderViewModel by activityViewModels() - private var folderSettingAdapter: FolderSettingAdapter? = null - private var folderOrderList: MutableList = mutableListOf() - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentFolderSettingBinding.inflate(inflater, container, false) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - - setBackButtonClickListener() - setFolderAddButtonClickListener() - setRvFolderSettingListAdapter() - setProfileFolderList() - setChangeOrderButtonClickListener() - setSaveButtonClickListener() - - return binding.root - } - - private fun onDestroyBindingView() { - folderSettingAdapter = null - binding.rvFolderSettingListSaved.adapter = null - } - - private fun setBackButtonClickListener() { - binding.btnFolderSettingBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setFolderAddButtonClickListener() { - binding.tvFolderSettingAdd.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.FolderSettingFragment, - action = R.id.action_folderSettingFragment_to_folderSettingAddFragment - ) - } - } - - private fun setRvFolderSettingListAdapter() { - folderSettingAdapter = FolderSettingAdapter(false) - binding.rvFolderSettingListSaved.adapter = folderSettingAdapter - } - - private fun setProfileFolderList() { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - folderViewModel.requestAllMyFolderList() - folderViewModel.folderList.observe(viewLifecycleOwner, Observer { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { folderList -> - folderSettingAdapter?.submitList(folderList) - if (folderList.size < 5) { - ButtonActivation.setTextViewButtonActive( - requireContext(), - binding.tvFolderSettingAdd - ) - } else { - ButtonActivation.setTextViewButtonInactive( - requireContext(), - binding.tvFolderSettingAdd - ) - } - } - } - - else -> {} - } - }) - } - } - } - - private fun setProfileOrderFolderList() { - folderViewModel.folderList.observe(viewLifecycleOwner, Observer { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { folderList -> - val size = folderList.size - folderOrderList = mutableListOf() - for (i in 0 until size) { - folderOrderList.add( - FolderOrder( - folderList[i].folderId!!, - i - ) - ) - } - folderSettingAdapter?.submitList(folderList) - } - } - - else -> {} - } - }) - } - - private fun setChangeOrderButtonClickListener() { - binding.btnFolderSettingChangeOrderOption.setOnDebounceClickListener { - binding.btnFolderSettingSave.visibility = View.VISIBLE - folderSettingAdapter = FolderSettingAdapter(true) - setProfileOrderFolderList() - changeOrder() - } - } - - private fun setSaveButtonClickListener() { - binding.btnFolderSettingSave.setOnDebounceClickListener { - //๋ณ€๊ฒฝ๋œ ์ˆœ์„œ ์ €์žฅ - folderViewModel.requestOrderFolder(folderOrderList) - - //์ˆœ์„œ ์ €์žฅ ์™„๋ฃŒ ํ›„ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋กœ ๋Œ์•„๊ฐ€๊ธฐ - setRvFolderSettingListAdapter() - binding.btnFolderSettingSave.visibility = View.GONE - folderViewModel.orderFolderSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - setProfileFolderList() - } - } - } - } - - private fun changeOrder() { - folderSettingAdapter?.folderOrderList = folderOrderList - val callback = folderSettingAdapter?.let { ItemTouchHelperCallback(it) } - val touchHelper = callback?.let { ItemTouchHelper(it) } - touchHelper?.attachToRecyclerView(binding.rvFolderSettingListSaved) - binding.rvFolderSettingListSaved.adapter = folderSettingAdapter - folderSettingAdapter?.startDrag(object : FolderSettingAdapter.OnStartDragListener { - override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) { - touchHelper?.startDrag(viewHolder) - } - }) - } - -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/follow/FollowFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/follow/FollowFragment.kt deleted file mode 100644 index c06e4faa..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/follow/FollowFragment.kt +++ /dev/null @@ -1,142 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.follow - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import daily.dayo.presentation.R -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentFollowBinding -import daily.dayo.presentation.adapter.FollowFragmentPagerStateAdapter -import daily.dayo.presentation.viewmodel.FollowViewModel -import com.google.android.material.tabs.TabLayout -import com.google.android.material.tabs.TabLayout.OnTabSelectedListener -import com.google.android.material.tabs.TabLayoutMediator - -class FollowFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val followViewModel by activityViewModels() - private var mediator: TabLayoutMediator? = null - private var pagerAdapter: FollowFragmentPagerStateAdapter? = null - private val args by navArgs() - private val tabSelectedListener = object : OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab) { - when (tab.position) { - 0 -> followViewModel.requestListAllFollower(args.memberId) - 1 -> followViewModel.requestListAllFollowing(args.memberId) - } - } - - override fun onTabUnselected(tab: TabLayout.Tab) {} - override fun onTabReselected(tab: TabLayout.Tab) {} - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - super.onCreate(savedInstanceState) - binding = FragmentFollowBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - setTabLayout() - setBackButtonClickListener() - setFollowFragmentDescription() - setOnTabSelectedListener() - } - - private fun onDestroyBindingView() { - binding.apply { - pagerFollow.adapter = null - tabsFollow.removeOnTabSelectedListener(tabSelectedListener) - } - mediator?.detach() - mediator = null - pagerAdapter = null - } - - private fun setTabLayout() { - pagerAdapter = - FollowFragmentPagerStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) - pagerAdapter?.addFragment(FollowerListFragment()) - pagerAdapter?.addFragment(FollowingListFragment()) - binding.pagerFollow.adapter = pagerAdapter - - mediator = TabLayoutMediator(binding.tabsFollow, binding.pagerFollow) { tab, position -> - tab.setCustomView(R.layout.tab_follow) - val tvFollow = tab.customView?.findViewById(R.id.tv_follow) - val tvFollowCount = tab.customView?.findViewById(R.id.tv_follow_count) - when (position) { - 0 -> { - tvFollow?.text = getText(R.string.follower) - followViewModel.requestListAllFollower(args.memberId) - followViewModel.followerCount.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { followerCount -> - tvFollowCount?.text = followerCount.toString() - } - } - Status.ERROR -> { - "0" - } - Status.ERROR -> { - tvFollowCount?.text = "0" - } - Status.LOADING -> {} - } - } - } - 1 -> { - tvFollow?.text = getText(R.string.following) - followViewModel.requestListAllFollowing(args.memberId) - followViewModel.followingCount.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { followingCount -> - tvFollowCount?.text = followingCount.toString() - } - } - Status.ERROR -> { - tvFollowCount?.text = "0" - } - Status.LOADING -> {} - } - } - } - } - } - mediator?.attach() - - binding.pagerFollow.post { - binding.pagerFollow.setCurrentItem(args.initPosition, false) - } - } - - private fun setOnTabSelectedListener() { - binding.tabsFollow.addOnTabSelectedListener(tabSelectedListener) - } - - private fun setBackButtonClickListener() { - binding.btnFollowBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setFollowFragmentDescription() { - binding.tvFollowUserNickname.text = args.nickname - followViewModel.memberId = args.memberId - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/follow/FollowerListFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/follow/FollowerListFragment.kt deleted file mode 100644 index bf2d1f29..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/follow/FollowerListFragment.kt +++ /dev/null @@ -1,132 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.follow - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.R -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.DefaultDialogConfigure -import daily.dayo.presentation.common.dialog.DefaultDialogConfirm -import daily.dayo.presentation.databinding.FragmentFollowerListBinding -import daily.dayo.domain.model.MyFollower -import daily.dayo.presentation.adapter.FollowListAdapter -import daily.dayo.presentation.viewmodel.AccountViewModel -import daily.dayo.presentation.viewmodel.FollowViewModel - -class FollowerListFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val accountViewModel by activityViewModels() - private val followViewModel by activityViewModels() - private var followerListAdapter: FollowListAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentFollowerListBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - setRvFollowerListAdapter() - return binding.root - } - - override fun onResume() { - super.onResume() - observeFollowerList() - observeFollowSuccess() - observeUnfollowSuccess() - } - - private fun onDestroyBindingView() { - glideRequestManager = null - followerListAdapter = null - binding.rvFollower.adapter = null - } - - private fun setRvFollowerListAdapter() { - followerListAdapter = glideRequestManager?.let { requestManager -> - FollowListAdapter(requestManager = requestManager, userInfo = accountViewModel.getCurrentUserInfo()) - } - binding.rvFollower.adapter = followerListAdapter - followerListAdapter?.setOnItemClickListener(object : FollowListAdapter.OnItemClickListener { - override fun onItemClick(button: Button, follow: MyFollower, position: Int) { - when (follow.isFollow) { - false -> { // ํด๋ฆญ ์‹œ ํŒ”๋กœ์šฐ - requestFollow(follow.memberId) - } - true -> { // ํด๋ฆญ ์‹œ ์–ธํŒ”๋กœ์šฐ - val mAlertDialog = DefaultDialogConfirm.createDialog(requireContext(), - R.string.follow_delete_description_message, - true, - true, - R.string.confirm, - R.string.cancel, - { requestUnfollow(follow.memberId) }, - { }) - if (!mAlertDialog.isShowing) { - mAlertDialog.show() - DefaultDialogConfigure.dialogResize( - requireContext(), - mAlertDialog, - 0.7f, - 0.23f - ) - } - mAlertDialog.setOnCancelListener { - mAlertDialog.dismiss() - } - } - } - } - }) - } - - private fun requestFollow(memberId: String) { - followViewModel.requestCreateFollow(followerId = memberId, isFollower = true) - } - - private fun requestUnfollow(memberId: String) { - followViewModel.requestDeleteFollow(followerId = memberId, isFollower = true) - } - - private fun requestFollowerList() { - followViewModel.requestListAllFollower(followViewModel.memberId) - } - - private fun observeFollowerList() { - followViewModel.followerList.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { followerList -> - binding.followerCount = followerList.size - followerListAdapter?.submitList(followerList) - } - } - else -> {} - } - } - } - - private fun observeFollowSuccess() { - followViewModel.followerFollowSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - requestFollowerList() - } - } - } - - private fun observeUnfollowSuccess() { - followViewModel.followerUnfollowSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - requestFollowerList() - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/follow/FollowingListFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/follow/FollowingListFragment.kt deleted file mode 100644 index 459cf797..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/follow/FollowingListFragment.kt +++ /dev/null @@ -1,133 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.follow - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.R -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.DefaultDialogConfigure -import daily.dayo.presentation.common.dialog.DefaultDialogConfirm -import daily.dayo.presentation.databinding.FragmentFollowingListBinding -import daily.dayo.domain.model.MyFollower -import daily.dayo.presentation.adapter.FollowListAdapter -import daily.dayo.presentation.viewmodel.AccountViewModel -import daily.dayo.presentation.viewmodel.FollowViewModel - -class FollowingListFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val accountViewModel by activityViewModels() - private val followViewModel by activityViewModels() - private var followingListAdapter: FollowListAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentFollowingListBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - setRvFollowingListAdapter() - return binding.root - } - - override fun onResume() { - super.onResume() - observeFollowingList() - observeFollowSuccess() - observeUnfollowSuccess() - } - - private fun onDestroyBindingView() { - glideRequestManager = null - followingListAdapter = null - binding.rvFollowing.adapter = null - } - - private fun setRvFollowingListAdapter() { - followingListAdapter = glideRequestManager?.let { requestManager -> - FollowListAdapter(requestManager = requestManager, userInfo = accountViewModel.getCurrentUserInfo()) - } - binding.rvFollowing.adapter = followingListAdapter - followingListAdapter?.setOnItemClickListener(object : - FollowListAdapter.OnItemClickListener { - override fun onItemClick(button: Button, follow: MyFollower, position: Int) { - when (follow.isFollow) { - false -> { // ํด๋ฆญ ์‹œ ํŒ”๋กœ์šฐ - requestFollow(follow.memberId) - } - true -> { // ํด๋ฆญ ์‹œ ์–ธํŒ”๋กœ์šฐ - val mAlertDialog = DefaultDialogConfirm.createDialog(requireContext(), - R.string.follow_delete_description_message, - true, - true, - R.string.confirm, - R.string.cancel, - { requestUnfollow(follow.memberId) }, - { }) - if (!mAlertDialog.isShowing) { - mAlertDialog.show() - DefaultDialogConfigure.dialogResize( - requireContext(), - mAlertDialog, - 0.7f, - 0.23f - ) - } - mAlertDialog.setOnCancelListener { - mAlertDialog.dismiss() - } - } - } - } - }) - } - - private fun requestFollowingList() { - followViewModel.requestListAllFollowing(followViewModel.memberId) - } - - private fun requestFollow(memberId: String) { - followViewModel.requestCreateFollow(followerId = memberId, isFollower = false) - } - - private fun requestUnfollow(memberId: String) { - followViewModel.requestDeleteFollow(followerId = memberId, isFollower = false) - } - - private fun observeFollowingList() { - followViewModel.followingList.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { followingList -> - binding.followingCount = followingList.size - followingListAdapter?.submitList(followingList) - } - } - else -> {} - } - } - } - - private fun observeFollowSuccess() { - followViewModel.followingFollowSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - requestFollowingList() - } - } - } - - private fun observeUnfollowSuccess() { - followViewModel.followingUnfollowSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - requestFollowingList() - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/MyPageFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/MyPageFragment.kt deleted file mode 100644 index 83cec367..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/MyPageFragment.kt +++ /dev/null @@ -1,185 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.profile - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.ViewCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import com.google.android.material.tabs.TabLayoutMediator -import daily.dayo.presentation.R -import daily.dayo.presentation.adapter.ProfileFragmentPagerStateAdapter -import daily.dayo.presentation.common.GlideLoadUtil.loadImageViewProfile -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentMyPageBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import daily.dayo.presentation.viewmodel.ProfileViewModel - -const val FOLDERS_TAB_ID = 0 -const val LIKES_TAB_ID = 1 -const val BOOKMARKS_TAB_ID = 2 - -class MyPageFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val accountViewModel by activityViewModels() - private val profileViewModel by activityViewModels() - private var glideRequestManager: RequestManager? = null - private var mediator: TabLayoutMediator? = null - private var pagerAdapter: ProfileFragmentPagerStateAdapter? = null - private val pageChangeCallBack = object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - pagerAdapter?.let { - it.refreshFragment(position, it.fragments[position]) - } - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentMyPageBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setScrollPosition() - setProfileDescription() - setViewPager() - setViewPagerChangeEvent() - setMyProfileOptionClickListener() - } - - private fun onDestroyBindingView() { - mediator?.detach() - mediator = null - glideRequestManager = null - pagerAdapter = null - with(binding.pagerMyPage) { - unregisterOnPageChangeCallback(pageChangeCallBack) - adapter = null - } - profileViewModel.cleanUpFolders() - } - - private fun setScrollPosition() { - with(binding) { - ViewCompat.requestApplyInsets(layoutMyPage) - ViewCompat.requestApplyInsets(layoutMyPageAppBar) - } - } - - fun resetAppBarScrollPosition() { - with(binding) { - layoutMyPageAppBar.setExpanded(true, true) - } - } - - private fun setProfileDescription() { - profileViewModel.requestMyProfile() - profileViewModel.profileInfo.observe(viewLifecycleOwner) { - it?.let { profile -> - binding.profile = profile.data - glideRequestManager?.let { requestManager -> - profile.data?.profileImg?.let { profileImg -> - loadImageViewProfile( - requestManager = requestManager, - width = binding.imgMyPageUserProfile.width, - height = binding.imgMyPageUserProfile.height, - imgName = profileImg, - imgView = binding.imgMyPageUserProfile - ) - } - } - - profile.data?.let { profile -> - profile.memberId?.let { memberId -> - setFollowerCountButtonClickListener( - memberId = memberId, - nickname = profile.nickname - ) - setFollowingCountButtonClickListener( - memberId = memberId, - nickname = profile.nickname - ) - } - } - } - } - } - - private fun setViewPagerChangeEvent() { - binding.pagerMyPage.registerOnPageChangeCallback(pageChangeCallBack) - } - - private fun setViewPager() { - pagerAdapter = - ProfileFragmentPagerStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) - pagerAdapter?.addFragment(ProfileFolderListFragment()) - pagerAdapter?.addFragment(ProfileLikePostListFragment()) - pagerAdapter?.addFragment(ProfileBookmarkPostListFragment()) - binding.pagerMyPage.adapter = pagerAdapter - - mediator = TabLayoutMediator(binding.tabsMyPage, binding.pagerMyPage) { tab, position -> - when (position) { - FOLDERS_TAB_ID -> tab.text = "์ž‘์„ฑํ•œ ๊ธ€" - LIKES_TAB_ID -> tab.text = "์ข‹์•„์š”" - BOOKMARKS_TAB_ID -> tab.text = "๋ถ๋งˆํฌ" - } - } - mediator?.attach() - } - - private fun setMyProfileOptionClickListener() { - binding.btnMyPageOption.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.MyPageFragment, - action = R.id.action_myPageFragment_to_profileOptionFragment, - args = MyPageFragmentDirections.actionMyPageFragmentToProfileOptionFragment( - isMine = true, - memberId = accountViewModel.getCurrentUserInfo().memberId!! - ).arguments - ) - } - } - - private fun setFollowerCountButtonClickListener(memberId: String, nickname: String) { - binding.layoutMyPageFollowerCount.setOnDebounceClickListener { v -> - Navigation.findNavController(v).navigateSafe( - currentDestinationId = R.id.MyPageFragment, - action = R.id.action_myPageFragment_to_followFragment, - args = MyPageFragmentDirections.actionMyPageFragmentToFollowFragment( - memberId = memberId, - nickname = nickname, - initPosition = 0 - ).arguments - ) - } - } - - private fun setFollowingCountButtonClickListener(memberId: String, nickname: String) { - binding.layoutMyPageFollowingCount.setOnDebounceClickListener { v -> - Navigation.findNavController(v).navigateSafe( - currentDestinationId = R.id.MyPageFragment, - action = R.id.action_myPageFragment_to_followFragment, - args = MyPageFragmentDirections.actionMyPageFragmentToFollowFragment( - memberId = memberId, - nickname = nickname, - initPosition = 1 - ).arguments - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileBookmarkPostListFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileBookmarkPostListFragment.kt deleted file mode 100644 index b6a310cc..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileBookmarkPostListFragment.kt +++ /dev/null @@ -1,148 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.profile - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.paging.LoadState -import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.domain.model.BookmarkPost -import daily.dayo.presentation.R -import daily.dayo.presentation.activity.MainActivity -import daily.dayo.presentation.adapter.ProfileBookmarkPostListAdapter -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.databinding.FragmentProfileBookmarkPostListBinding -import daily.dayo.presentation.viewmodel.ProfileViewModel - -class ProfileBookmarkPostListFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val profileViewModel by activityViewModels() - private var profileBookmarkPostListAdapter: ProfileBookmarkPostListAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (savedInstanceState == null) - getProfileBookmarkPostList() - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentProfileBookmarkPostListBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setRvProfileBookmarkPostListAdapter() - setProfileBookmarkPostList() - setAdapterLoadStateListener() - } - - override fun onResume() { - super.onResume() - setBottomNavigationIconClickListener() - } - - private fun onDestroyBindingView() { - glideRequestManager = null - profileBookmarkPostListAdapter = null - binding.rvProfileBookmarkPost.adapter = null - } - - private fun setRvProfileBookmarkPostListAdapter() { - profileBookmarkPostListAdapter = glideRequestManager?.let { requestManager -> - ProfileBookmarkPostListAdapter( - requestManager = requestManager - ) - } - binding.rvProfileBookmarkPost.adapter = profileBookmarkPostListAdapter - profileBookmarkPostListAdapter?.setOnItemClickListener(object : - ProfileBookmarkPostListAdapter.OnItemClickListener { - override fun onItemClick(v: View, bookmarkPost: BookmarkPost, pos: Int) { - when (requireParentFragment()) { - is MyPageFragment -> - findNavController().navigateSafe( - currentDestinationId = R.id.MyPageFragment, - action = R.id.action_myPageFragment_to_postFragment, - args = MyPageFragmentDirections.actionMyPageFragmentToPostFragment( - bookmarkPost.postId - ).arguments - ) - - is ProfileFragment -> - findNavController().navigateSafe( - currentDestinationId = R.id.ProfileFragment, - action = R.id.action_profileFragment_to_postFragment, - args = ProfileFragmentDirections.actionProfileFragmentToPostFragment(bookmarkPost.postId).arguments - ) - - else -> {} - } - } - }) - } - - private fun getProfileBookmarkPostList() { - profileViewModel.requestAllMyBookmarkPostList() - } - - private fun setProfileBookmarkPostList() { - profileViewModel.bookmarkPostList.observe(viewLifecycleOwner) { - profileBookmarkPostListAdapter?.submitData(viewLifecycleOwner.lifecycle, it) - } - } - - private fun setAdapterLoadStateListener() { - var isInitialLoad = false - profileBookmarkPostListAdapter?.let { - it.addLoadStateListener { loadState -> - if (loadState.refresh is LoadState.NotLoading && !isInitialLoad) { - val isListEmpty = it.itemCount == 0 - binding.isEmpty = isListEmpty - if (isListEmpty || loadState.append is LoadState.NotLoading) { - completeLoadPost() - isInitialLoad = true - } - } - } - } - } - - private fun completeLoadPost() { - binding.layoutProfileBookmarkPostShimmer.stopShimmer() - binding.layoutProfileBookmarkPostShimmer.visibility = View.GONE - binding.rvProfileBookmarkPost.visibility = View.VISIBLE - } - - private fun setBottomNavigationIconClickListener() { - (requireActivity() as MainActivity).setBottomNavigationIconClickListener(reselectedIconId = R.id.MyPageFragment) { - val currentViewPagerPosition = - requireParentFragment().requireView() - .findViewById(R.id.pager_my_page) - .currentItem - - if (currentViewPagerPosition == BOOKMARKS_TAB_ID) { - getProfileBookmarkPostList() - setScrollToTop(isSmoothScroll = true) - } - } - } - - private fun setScrollToTop(isSmoothScroll: Boolean = false) { - (requireParentFragment() as MyPageFragment).resetAppBarScrollPosition() - with(binding.rvProfileBookmarkPost) { - if (isSmoothScroll) this.smoothScrollToPosition(0) - else this.scrollToPosition(0) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileEditFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileEditFragment.kt deleted file mode 100644 index d2acc6c6..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileEditFragment.kt +++ /dev/null @@ -1,424 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.profile - -import android.app.AlertDialog -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.ImageDecoder -import android.graphics.drawable.Drawable -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import android.view.inputmethod.EditorInfo -import androidx.annotation.RequiresApi -import androidx.core.net.toUri -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.GlideLoadUtil -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.image.ImageResizeUtil -import daily.dayo.presentation.common.image.ImageResizeUtil.USER_PROFILE_THUMBNAIL_RESIZE_SIZE -import daily.dayo.presentation.common.image.ImageResizeUtil.cropCenterBitmap -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentProfileEditBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import daily.dayo.presentation.viewmodel.ProfileSettingViewModel -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.runBlocking -import java.io.File -import java.io.FileOutputStream -import java.io.OutputStream -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.regex.Pattern - -class ProfileEditFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - onDestroyBindingView() - } - private val accountViewModel by activityViewModels() - private val profileSettingViewModel by activityViewModels() - private var glideRequestManager: RequestManager? = null - private lateinit var userProfileImageString: String - private var imagePath: String? = null - private val imageFileTimeFormat = SimpleDateFormat("yyyy-MM-d-HH-mm-ss", Locale.KOREA) - private lateinit var userProfileImageExtension: String - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentProfileEditBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - setKeyboardMode() - initializeLoadingDialog() - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setBackButtonClickListener() - setLimitEditTextInputType() - setTextEditorActionListener() - verifyNickname() - initMyProfile() - setProfileImageOptionClickListener() - observeNavigationMyProfileImageCallBack() - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - setProfileUpdateClickListener() - } - setHideKeyboard() - } - - private fun onDestroyBindingView() { - glideRequestManager = null - } - - private fun setKeyboardMode() { - requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) - } - - private fun setHideKeyboard() { - requireView().setOnTouchListener { _, _ -> - HideKeyBoardUtil.hide(requireContext(), binding.etProfileEditNickname) - true - } - } - - private fun initializeLoadingDialog() { - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - } - - private fun setTextEditorActionListener() { - binding.etProfileEditNickname.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide(requireContext(), binding.etProfileEditNickname) - true - } - - else -> false - } - } - } - - private fun setBackButtonClickListener() { - binding.btnProfileEditBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun initMyProfile() { - profileSettingViewModel.requestProfile(memberId = accountViewModel.getCurrentUserInfo().memberId!!) - profileSettingViewModel.profileInfo.observe(viewLifecycleOwner) { - it?.let { profile -> - glideRequestManager?.let { requestManager -> - GlideLoadUtil.loadImageViewProfile( - requestManager = requestManager, - width = binding.imgProfileEditUserImage.width, - height = binding.imgProfileEditUserImage.height, - imgName = profile.profileImg, - imgView = binding.imgProfileEditUserImage - ) - } - binding.etProfileEditNickname.setText(profile.nickname) - } - } - } - - private fun setLimitEditTextInputType() { - // InputFilter๋กœ ๋„์–ด์“ฐ๊ธฐ ์ž…๋ ฅ๋งŒ ๋ง‰๊ณ  ๋‚˜๋จธ์ง€๋Š” ์•ˆ๋‚ด๋ฉ”์‹œ์ง€๋กœ ๋„์›Œ์ง€๋„๋ก ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ๋„ - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - val lengthFilter = InputFilter.LengthFilter(10) - binding.etProfileEditNickname.filters = arrayOf(filterInputCheck, lengthFilter) - } - - private fun verifyNickname() { - binding.etProfileEditNickname.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - with(binding) { - if (s.toString().length == 0) { - tvProfileEditNicknameCount.text = - "0${getString(R.string.my_profile_edit_nickname_edittext_count)}" - } else { - tvProfileEditNicknameCount.text = "${ - trimBlankText(s).length - }${getString(R.string.my_profile_edit_nickname_edittext_count)}" - } - - if (trimBlankText(s) == profileSettingViewModel.profileInfo.value?.nickname) { // ๊ธฐ์กด ๋‹‰๋„ค์ž„๊ณผ ๋™์ผํ•œ ๊ฒฝ์šฐ - setEditTextTheme( - "", - true - ) - ButtonActivation.setTextViewButtonActive( - requireContext(), - btnProfileEditComplete - ) - } else { - if (trimBlankText(s).length < 2) { // ๋‹‰๋„ค์ž„ ๊ธธ์ด ๊ฒ€์‚ฌ 1 - setEditTextTheme( - getString(R.string.my_profile_edit_nickname_message_length_fail_min), - false - ) - ButtonActivation.setTextViewButtonInactive( - requireContext(), - btnProfileEditComplete - ) - } else if (trimBlankText(s).length > 10) { // ๋‹‰๋„ค์ž„ ๊ธธ์ด ๊ฒ€์‚ฌ 2 - setEditTextTheme( - getString(R.string.my_profile_edit_nickname_message_length_fail_max), - false - ) - ButtonActivation.setTextViewButtonInactive( - requireContext(), - btnProfileEditComplete - ) - } else { - if (Pattern.matches( - "^[ใ„ฑ-ใ…Ž|ใ…-ใ…ฃ|๊ฐ€-ํžฃ|a-z|A-Z|0-9|]+\$", - trimBlankText(s) - ) - ) { - profileSettingViewModel.requestCheckNicknameDuplicate( - trimBlankText( - binding.etProfileEditNickname.text - ) - ) - profileSettingViewModel.isNicknameDuplicate.observe( - viewLifecycleOwner - ) { isDuplicate -> - if (isDuplicate) { - setEditTextTheme( - getString(R.string.my_profile_edit_nickname_message_success), - true - ) - ButtonActivation.setTextViewButtonActive( - requireContext(), - btnProfileEditComplete - ) - } else { - setEditTextTheme( - getString(R.string.my_profile_edit_nickname_message_duplicate_fail), - false - ) - ButtonActivation.setTextViewButtonInactive( - requireContext(), - btnProfileEditComplete - ) - } - } - } else { - setEditTextTheme( - getString(R.string.my_profile_edit_nickname_message_format_fail), - false - ) - ButtonActivation.setTextViewButtonInactive( - requireContext(), - btnProfileEditComplete - ) - } - } - } - } - } - }) - } - - private fun setEditTextTheme(checkMessage: String?, pass: Boolean) { - with(binding.tvProfileEditNicknameMessage) { - visibility = View.VISIBLE - if (pass) { - text = checkMessage - setTextColor(resources.getColor(R.color.primary_green_23C882, context?.theme)) - binding.tvProfileEditNicknameMessage.backgroundTintList = - resources.getColorStateList(R.color.primary_green_23C882, context?.theme) - } else { - text = checkMessage - setTextColor(resources.getColor(R.color.red_FF4545, context?.theme)) - binding.tvProfileEditNicknameMessage.backgroundTintList = - resources.getColorStateList(R.color.red_FF4545, context?.theme) - } - } - } - - private fun setProfileImageOptionClickListener() { - binding.layoutProfileEditUserImg.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.ProfileEditFragment, - action = R.id.action_profileEditFragment_to_profileEditImageOptionFragment - ) - } - } - - private fun observeNavigationMyProfileImageCallBack() { - findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData("userProfileImageString") - ?.observe(viewLifecycleOwner) { - userProfileImageString = it - if (this::userProfileImageString.isInitialized) { - if (userProfileImageString == "resetMyProfileImage") { - glideRequestManager?.load(R.drawable.ic_user_profile_image_empty) - ?.centerCrop()?.into(binding.imgProfileEditUserImage) - } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - glideRequestManager?.load(userProfileImageString.toUri())?.centerCrop() - ?.into(binding.imgProfileEditUserImage) - } - } - } - } - findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData("fileExtension") - ?.observe(viewLifecycleOwner) { - userProfileImageExtension = it - } - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun setProfileUpdateClickListener() { - binding.btnProfileEditComplete.setOnDebounceClickListener { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - val nickname: String? = trimBlankText(binding.etProfileEditNickname.text) - var profileImgFile: File? - - runBlocking { - if (this@ProfileEditFragment::userProfileImageString.isInitialized) { - // ํ”„๋กœํ•„ ์‚ฌ์ง„์„ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒฝ์šฐ - profileImgFile = changeMyProfileImage() - profileSettingViewModel.requestUpdateMyProfile( - nickname = nickname, - profileImg = profileImgFile, - isReset = profileImgFile == null - ) - .invokeOnCompletion { throwable -> - when (throwable) { - is CancellationException -> Log.e( - "My Profile Update", - "CANCELLED" - ) - - null -> { - profileSettingViewModel.requestProfile(memberId = accountViewModel.getCurrentUserInfo().memberId!!) - findNavController().navigateUp() - } - } - } - } else { - // ํ”„๋กœํ•„ ์‚ฌ์ง„์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ - profileSettingViewModel.requestUpdateMyProfile( - nickname = nickname, - profileImg = null, - isReset = false - ) - .invokeOnCompletion { throwable -> - when (throwable) { - is CancellationException -> Log.e("My Profile Update", "CANCELLED") - null -> { - profileSettingViewModel.requestProfile(memberId = accountViewModel.getCurrentUserInfo().memberId!!) - findNavController().navigateUp() - } - } - } - } - } - } - } - - private fun changeMyProfileImage(): File? { - return if (userProfileImageString == "resetMyProfileImage") { // 1. ํ”„๋กœํ•„ ์‚ฌ์ง„ ์ดˆ๊ธฐํ™”๋ฅผ ํ•œ ๊ฒฝ์šฐ - null - } else { // 2. ํ”„๋กœํ•„ ์‚ฌ์ง„์„ ๋‹ค๋ฅธ ์‚ฌ์ง„์œผ๋กœ ๋ณ€๊ฒฝ ํ•œ ๊ฒฝ์šฐ - setUploadImagePath(userProfileImageExtension) - val originalBitmap = userProfileImageString.toUri().toBitmap().cropCenterBitmap() - val resizedBitmap = ImageResizeUtil.resizeBitmap( - originalBitmap = originalBitmap, - resizedWidth = USER_PROFILE_THUMBNAIL_RESIZE_SIZE, - resizedHeight = USER_PROFILE_THUMBNAIL_RESIZE_SIZE - ) - bitmapToFile(resizedBitmap, imagePath) - } - } - - private fun setUploadImagePath(fileExtension: String) { - // uri๋ฅผ ํ†ตํ•˜์—ฌ ๋ถˆ๋Ÿฌ์˜จ ์ด๋ฏธ์ง€๋ฅผ ์ž„์‹œ๋กœ ํŒŒ์ผ๋กœ ์ €์žฅํ•  ๊ฒฝ๋กœ๋กœ ์•ฑ ๋‚ด๋ถ€ ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์„ค์ •, - // ํŒŒ์ผ ์ด๋ฆ„์€ ๋ถˆ๋Ÿฌ์˜จ ์‹œ๊ฐ„ ์‚ฌ์šฉ - val fileName = imageFileTimeFormat.format(Date(System.currentTimeMillis())) - .toString() + "." + fileExtension - val cacheDir = requireContext().cacheDir.toString() - imagePath = "$cacheDir/$fileName" - } - - private fun bitmapToFile(bitmap: Bitmap?, path: String?): File? { - if (bitmap == null || path == null) { - return null - } - var file = File(path) - var out: OutputStream? = null - try { - file.createNewFile() - out = FileOutputStream(file) - bitmap?.compress(Bitmap.CompressFormat.JPEG, 100, out) - } finally { - out?.close() - } - return file - } - - private fun Uri.toBitmap(): Bitmap { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoder.decodeBitmap( - ImageDecoder.createSource( - requireContext().contentResolver, - this - ) - ) - } else { - MediaStore.Images.Media.getBitmap(requireContext().contentResolver, this) - } - } - - private fun vectorDrawableToBitmapDrawable(drawable: Drawable): Bitmap? { - return try { - val bitmap: Bitmap = Bitmap.createBitmap( - drawable.intrinsicWidth, - drawable.intrinsicHeight, - Bitmap.Config.ARGB_8888 - ) - val canvas = Canvas(bitmap) - drawable.setBounds(0, 0, canvas.width, canvas.height) - drawable.draw(canvas) - bitmap - } catch (e: OutOfMemoryError) { - // TODO: Handle the error - null - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileFolderListFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileFolderListFragment.kt deleted file mode 100644 index 3776a315..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileFolderListFragment.kt +++ /dev/null @@ -1,130 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.profile - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.domain.model.Folder -import daily.dayo.presentation.R -import daily.dayo.presentation.activity.MainActivity -import daily.dayo.presentation.adapter.ProfileFolderListAdapter -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.databinding.FragmentProfileFolderListBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import daily.dayo.presentation.viewmodel.ProfileViewModel - -class ProfileFolderListFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val accountViewModel by activityViewModels() - private val profileViewModel by activityViewModels() - private var profileFolderListAdapter: ProfileFolderListAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentProfileFolderListBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - getProfileFolderList() - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setRvProfileFolderListAdapter() - setProfileFolderList() - } - - override fun onResume() { - super.onResume() - setBottomNavigationIconClickListener() - } - - private fun onDestroyBindingView() { - glideRequestManager = null - profileFolderListAdapter = null - binding.rvProfileFolder.adapter = null - } - - private fun setRvProfileFolderListAdapter() { - profileFolderListAdapter = glideRequestManager?.let { - ProfileFolderListAdapter( - requestManager = it - ) - } - binding.rvProfileFolder.adapter = profileFolderListAdapter - profileFolderListAdapter?.setOnItemClickListener(object : ProfileFolderListAdapter.OnItemClickListener { - override fun onItemClick(v: View, folder: Folder, pos: Int) { - when (requireParentFragment()) { - is MyPageFragment -> { - findNavController().navigateSafe( - currentDestinationId = R.id.MyPageFragment, - action = R.id.action_myPageFragment_to_folderFragment, - args = MyPageFragmentDirections.actionMyPageFragmentToFolderFragment(folderId = folder.folderId!!).arguments - ) - } - - is ProfileFragment -> { - findNavController().navigateSafe( - currentDestinationId = R.id.ProfileFragment, - action = R.id.action_profileFragment_to_folderFragment, - args = ProfileFragmentDirections.actionProfileFragmentToFolderFragment(folderId = folder.folderId!!).arguments - ) - } - - else -> {} - } - } - }) - } - - private fun getProfileFolderList() { - profileViewModel.requestFolderList(memberId = accountViewModel.getCurrentUserInfo().memberId!!, true) - } - - private fun setProfileFolderList() { - profileViewModel.folderList.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { folderList -> - binding.folderCount = folderList.size - profileFolderListAdapter?.submitList(folderList) - } - } - - else -> {} - } - } - } - - private fun setBottomNavigationIconClickListener() { - (requireActivity() as MainActivity).setBottomNavigationIconClickListener(reselectedIconId = R.id.MyPageFragment) { - val currentViewPagerPosition = - requireParentFragment().requireView() - .findViewById(R.id.pager_my_page) - .currentItem - - if (currentViewPagerPosition == FOLDERS_TAB_ID) { - getProfileFolderList() - setScrollToTop(isSmoothScroll = true) - } - } - } - - private fun setScrollToTop(isSmoothScroll: Boolean = false) { - (requireParentFragment() as MyPageFragment).resetAppBarScrollPosition() - with(binding.rvProfileFolder) { - if (isSmoothScroll) this.smoothScrollToPosition(0) - else this.scrollToPosition(0) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileFragment.kt deleted file mode 100644 index 3b1b476e..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileFragment.kt +++ /dev/null @@ -1,326 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.profile - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.ViewCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import com.google.android.material.tabs.TabLayoutMediator -import daily.dayo.domain.model.Folder -import daily.dayo.presentation.R -import daily.dayo.presentation.adapter.ProfileFolderListAdapter -import daily.dayo.presentation.adapter.ProfileFragmentPagerStateAdapter -import daily.dayo.presentation.common.GlideLoadUtil.loadImageViewProfile -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentProfileBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import daily.dayo.presentation.viewmodel.ProfileViewModel - -class ProfileFragment : Fragment() { - private var binding by autoCleared { - onDestroyBindingView() - } - private val accountViewModel by activityViewModels() - private val profileViewModel by activityViewModels() - private val args by navArgs() - private var profileFolderListAdapter: ProfileFolderListAdapter? = null - private var glideRequestManager: RequestManager? = null - private var pagerAdapter: ProfileFragmentPagerStateAdapter? = null - private var mediator: TabLayoutMediator? = null - private val pageChangeCallBack = object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - pagerAdapter?.let { - it.refreshFragment(position, it.fragments[position]) - } - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentProfileBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setScrollPosition() - } - - override fun onResume() { - super.onResume() - setProfile() - } - - private fun onDestroyBindingView() { - mediator?.detach() - mediator = null - profileFolderListAdapter = null - glideRequestManager = null - pagerAdapter = null - with(binding.pagerProfile) { - unregisterOnPageChangeCallback(pageChangeCallBack) - adapter = null - } - profileViewModel.cleanUpFolders() - } - - private fun setScrollPosition() { - with(binding) { - ViewCompat.requestApplyInsets(layoutProfile) - ViewCompat.requestApplyInsets(layoutProfileAppBar) - } - } - - private fun setProfile() { - if (args.memberId == accountViewModel.getCurrentUserInfo().memberId) { - profileViewModel.profileMemberId = args.memberId!! - binding.isMine = true - setMyProfile() - } else { - profileViewModel.profileMemberId = args.memberId!! - binding.isMine = false - setOtherProfile() - } - - setProfileDescription() - setBackButtonClickListener() - } - - private fun setMyProfile() { - setViewPager() - setViewPagerChangeEvent() - setMyProfileOptionClickListener() - getMyProfileDescription() - } - - private fun setOtherProfile() { - requireActivity().findViewById(R.id.layout_bottom_navigation_main).visibility = - View.GONE - setRvProfileFolderListAdapter() - getProfileFolderList() - setProfileFolderList() - setOtherProfileOptionClickListener() - getOtherProfileDescription() - } - - private fun getMyProfileDescription() { - profileViewModel.requestMyProfile() - } - - private fun getOtherProfileDescription() { - profileViewModel.requestOtherProfile(memberId = profileViewModel.profileMemberId) - } - - private fun setProfileDescription() { - profileViewModel.profileInfo.observe(viewLifecycleOwner) { - it?.let { profile -> - binding.profile = profile.data - glideRequestManager?.let { requestManager -> - profile.data?.let { profile -> - loadImageViewProfile( - requestManager = requestManager, - width = binding.imgProfileUserProfile.width, - height = binding.imgProfileUserProfile.height, - imgName = profile.profileImg, - imgView = binding.imgProfileUserProfile - ) - } - } - - setFollowButtonClickListener() - profile.data?.let { profile -> - profile.memberId?.let { memberId -> - setFollowerCountButtonClickListener( - memberId = memberId, - nickname = profile.nickname - ) - setFollowingCountButtonClickListener( - memberId = memberId, - nickname = profile.nickname - ) - } - } - } - } - } - - private fun setViewPagerChangeEvent() { - binding.pagerProfile.registerOnPageChangeCallback(pageChangeCallBack) - } - - private fun setBackButtonClickListener() { - binding.btnProfileBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setViewPager() { - pagerAdapter = - ProfileFragmentPagerStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) - pagerAdapter?.addFragment(ProfileFolderListFragment()) - pagerAdapter?.addFragment(ProfileLikePostListFragment()) - pagerAdapter?.addFragment(ProfileBookmarkPostListFragment()) - binding.pagerProfile.adapter = pagerAdapter - - mediator = TabLayoutMediator(binding.tabsProfile, binding.pagerProfile) { tab, position -> - when (position) { - 0 -> tab.text = "์ž‘์„ฑํ•œ ๊ธ€" - 1 -> tab.text = "์ข‹์•„์š”" - 2 -> tab.text = "๋ถ๋งˆํฌ" - } - } - mediator?.attach() - } - - private fun setFollowButtonClickListener() { - binding.btnProfileFollow.setOnDebounceClickListener { - when (profileViewModel.profileInfo.value?.data?.follow) { - false -> setFollow() - true -> setUnfollow() - else -> {} - } - } - } - - private fun setFollow() { - profileViewModel.requestCreateFollow(followerId = profileViewModel.profileMemberId) - profileViewModel.followSuccess.observe(viewLifecycleOwner) { - it.getContentIfNotHandled()?.let { success -> - if (success) { - getOtherProfileDescription() - } else { - Toast.makeText(requireContext(), "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ๊ฐ€ ๋ถˆ์•ˆ์ •ํ•ฉ๋‹ˆ๋‹ค", Toast.LENGTH_SHORT) - .show() - } - } - } - } - - private fun setUnfollow() { - profileViewModel.requestDeleteFollow(followerId = profileViewModel.profileMemberId) - profileViewModel.unfollowSuccess.observe(viewLifecycleOwner) { - it.getContentIfNotHandled()?.let { success -> - if (success) { - getOtherProfileDescription() - } else { - Toast.makeText(requireContext(), "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ๊ฐ€ ๋ถˆ์•ˆ์ •ํ•ฉ๋‹ˆ๋‹ค", Toast.LENGTH_SHORT) - .show() - } - } - } - } - - private fun setRvProfileFolderListAdapter() { - profileFolderListAdapter = glideRequestManager?.let { - ProfileFolderListAdapter( - requestManager = it - ) - } - binding.rvProfileFolder.adapter = profileFolderListAdapter - profileFolderListAdapter?.setOnItemClickListener(object : - ProfileFolderListAdapter.OnItemClickListener { - override fun onItemClick(v: View, folder: Folder, pos: Int) { - findNavController().navigateSafe( - currentDestinationId = R.id.ProfileFragment, - action = R.id.action_profileFragment_to_folderFragment, - args = ProfileFragmentDirections.actionProfileFragmentToFolderFragment( - folderId = folder.folderId!! - ).arguments - ) - } - }) - } - - private fun getProfileFolderList() { - profileViewModel.requestFolderList( - memberId = profileViewModel.profileMemberId, - isMine = false - ) - } - - private fun setProfileFolderList() { - profileViewModel.folderList.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { folderList -> - profileFolderListAdapter?.submitList(folderList) - } - } - - else -> {} - } - } - } - - private fun setMyProfileOptionClickListener() { - binding.btnProfileOption.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.ProfileFragment, - action = R.id.action_profileFragment_to_profileOptionFragment, - args = ProfileFragmentDirections.actionProfileFragmentToProfileOptionFragment( - isMine = true, - memberId = profileViewModel.profileMemberId - ).arguments - ) - } - } - - private fun setOtherProfileOptionClickListener() { - binding.btnProfileOption.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.ProfileFragment, - action = R.id.action_profileFragment_to_profileOptionFragment, - args = ProfileFragmentDirections.actionProfileFragmentToProfileOptionFragment( - isMine = false, - memberId = profileViewModel.profileMemberId - ).arguments - ) - } - } - - private fun setFollowerCountButtonClickListener(memberId: String, nickname: String) { - binding.layoutProfileFollowerCount.setOnDebounceClickListener { v -> - Navigation.findNavController(v).navigateSafe( - currentDestinationId = R.id.ProfileFragment, - action = R.id.action_profileFragment_to_followFragment, - args = ProfileFragmentDirections.actionProfileFragmentToFollowFragment( - memberId = memberId, - nickname = nickname, - initPosition = 0 - ).arguments - ) - } - } - - private fun setFollowingCountButtonClickListener(memberId: String, nickname: String) { - binding.layoutProfileFollowingCount.setOnDebounceClickListener { v -> - Navigation.findNavController(v).navigateSafe( - currentDestinationId = R.id.ProfileFragment, - action = R.id.action_profileFragment_to_followFragment, - args = ProfileFragmentDirections.actionProfileFragmentToFollowFragment( - memberId = memberId, - nickname = nickname, - initPosition = 1 - ).arguments - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileLikePostListFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileLikePostListFragment.kt deleted file mode 100644 index e3b133f7..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileLikePostListFragment.kt +++ /dev/null @@ -1,148 +0,0 @@ -package daily.dayo.presentation.fragment.mypage.profile - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.paging.LoadState -import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.domain.model.LikePost -import daily.dayo.presentation.R -import daily.dayo.presentation.activity.MainActivity -import daily.dayo.presentation.adapter.ProfileLikePostListAdapter -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.databinding.FragmentProfileLikePostListBinding -import daily.dayo.presentation.viewmodel.ProfileViewModel - -class ProfileLikePostListFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val profileViewModel by activityViewModels() - private var profileLikePostListAdapter: ProfileLikePostListAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (savedInstanceState == null) - getProfileLikePostList() - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentProfileLikePostListBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setRvProfileLikePostListAdapter() - setProfileLikePostList() - setAdapterLoadStateListener() - } - - override fun onResume() { - super.onResume() - setBottomNavigationIconClickListener() - } - - private fun onDestroyBindingView() { - glideRequestManager = null - profileLikePostListAdapter = null - binding.rvProfileLikePost.adapter = null - } - - private fun setRvProfileLikePostListAdapter() { - profileLikePostListAdapter = glideRequestManager?.let { requestManager -> - ProfileLikePostListAdapter( - requestManager = requestManager - ) - } - binding.rvProfileLikePost.adapter = profileLikePostListAdapter - profileLikePostListAdapter?.setOnItemClickListener(object : - ProfileLikePostListAdapter.OnItemClickListener { - override fun onItemClick(v: View, likePost: LikePost, pos: Int) { - when (requireParentFragment()) { - is MyPageFragment -> { - findNavController().navigateSafe( - currentDestinationId = R.id.MyPageFragment, - action = R.id.action_myPageFragment_to_postFragment, - args = MyPageFragmentDirections.actionMyPageFragmentToPostFragment(likePost.postId).arguments - ) - } - - is ProfileFragment -> { - findNavController().navigateSafe( - currentDestinationId = R.id.ProfileFragment, - action = R.id.action_profileFragment_to_postFragment, - args = ProfileFragmentDirections.actionProfileFragmentToPostFragment(likePost.postId).arguments - ) - } - - else -> {} - } - } - }) - } - - private fun getProfileLikePostList() { - profileViewModel.requestAllMyLikePostList() - } - - private fun setProfileLikePostList() { - profileViewModel.likePostList.observe(viewLifecycleOwner) { - profileLikePostListAdapter?.submitData(viewLifecycleOwner.lifecycle, it) - } - } - - private fun setAdapterLoadStateListener() { - var isInitialLoad = false - profileLikePostListAdapter?.let { - it.addLoadStateListener { loadState -> - if (loadState.refresh is LoadState.NotLoading && !isInitialLoad) { - val isListEmpty = it.itemCount == 0 - binding.isEmpty = isListEmpty - if (isListEmpty || loadState.append is LoadState.NotLoading) { - completeLoadPost() - isInitialLoad = true - } - } - } - } - } - - private fun completeLoadPost() { - binding.layoutProfileLikePostShimmer.stopShimmer() - binding.layoutProfileLikePostShimmer.visibility = View.GONE - binding.rvProfileLikePost.visibility = View.VISIBLE - } - - private fun setBottomNavigationIconClickListener() { - (requireActivity() as MainActivity).setBottomNavigationIconClickListener(reselectedIconId = R.id.MyPageFragment) { - val currentViewPagerPosition = - requireParentFragment().requireView() - .findViewById(R.id.pager_my_page) - .currentItem - - if (currentViewPagerPosition == LIKES_TAB_ID) { - getProfileLikePostList() - setScrollToTop(isSmoothScroll = true) - } - } - } - - private fun setScrollToTop(isSmoothScroll: Boolean = false) { - (requireParentFragment() as MyPageFragment).resetAppBarScrollPosition() - with(binding.rvProfileLikePost) { - if (isSmoothScroll) this.smoothScrollToPosition(0) - else this.scrollToPosition(0) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileOptionFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileOptionFragment.kt index 255ae38b..8d91c3e2 100644 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileOptionFragment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/fragment/mypage/profile/ProfileOptionFragment.kt @@ -72,10 +72,6 @@ class ProfileOptionFragment : DialogFragment() { private fun setOptionFolderSettingClickListener() { binding.layoutProfileOptionFolderSetting.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.ProfileOptionFragment, - action = R.id.action_profileOptionFragment_to_folderSettingFragment - ) } } @@ -90,10 +86,6 @@ class ProfileOptionFragment : DialogFragment() { private fun setOptionSettingClickListener() { binding.layoutProfileOptionSetting.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.ProfileOptionFragment, - action = R.id.action_profileOptionFragment_to_settingFragment - ) } } @@ -122,17 +114,6 @@ class ProfileOptionFragment : DialogFragment() { } private fun blockUser() { - profileViewModel.requestBlockMember(args.memberId) - profileViewModel.blockSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - Toast.makeText( - requireContext(), - R.string.other_profile_block_success_message, - Toast.LENGTH_SHORT - ).show() - findNavController().navigateUp() - } - } } private fun setOptionReportUserClickListener() { diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/notification/NotificationFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/notification/NotificationFragment.kt deleted file mode 100644 index 5162614b..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/notification/NotificationFragment.kt +++ /dev/null @@ -1,111 +0,0 @@ -package daily.dayo.presentation.fragment.notification - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.paging.LoadState -import androidx.recyclerview.widget.LinearLayoutManager -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.R -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.databinding.FragmentNotificationBinding -import daily.dayo.presentation.adapter.NotificationListAdapter -import daily.dayo.presentation.viewmodel.NotificationViewModel -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.activity.MainActivity - -@AndroidEntryPoint -class NotificationFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val notificationViewModel by viewModels() - private var notificationAdapter: NotificationListAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentNotificationBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setNotificationListAdapter() - setAdapterLoadStateListener() - setAlarmList() - setBottomNavigationIconClickListener() - } - - override fun onResume() { - super.onResume() - getAlarmList() - } - - private fun onDestroyBindingView() { - glideRequestManager = null - notificationAdapter = null - binding.rvNotificationList.adapter = null - } - - private fun setNotificationListAdapter() { - notificationAdapter = glideRequestManager?.let { - NotificationListAdapter(requestManager = it) - } - binding.rvNotificationList.layoutManager = LinearLayoutManager(requireContext()) - binding.rvNotificationList.adapter = notificationAdapter - notificationAdapter?.setOnItemClickListener(object : - NotificationListAdapter.OnItemClickListener { - override fun notificationItemClick(alarmId: Int, alarmCheck: Boolean, position: Int) { - if (!alarmCheck) { - notificationViewModel.requestIsCheckAlarm(alarmId = alarmId) - notificationViewModel.checkAlarmSuccess.observe(viewLifecycleOwner) { - if (it == true) notificationAdapter?.notifyItemChanged(position) - } - } - } - }) - } - - private fun getAlarmList() { - notificationViewModel.requestAllAlarmList() - } - - private fun setAlarmList() { - notificationViewModel.alarmList.observe(viewLifecycleOwner) { - notificationAdapter?.submitData(viewLifecycleOwner.lifecycle, it) - } - } - - private fun setAdapterLoadStateListener() { - var isInitialLoad = false - notificationAdapter?.addLoadStateListener { loadState -> - if (loadState.refresh is LoadState.NotLoading && !isInitialLoad) { - val isListEmpty = notificationAdapter?.itemCount == 0 - binding.isEmpty = isListEmpty - if (isListEmpty || loadState.append is LoadState.NotLoading) { - isInitialLoad = true - } - } - } - } - - private fun setBottomNavigationIconClickListener() { - (requireActivity() as MainActivity).setBottomNavigationIconClickListener(reselectedIconId = R.id.NotificationFragment) { - getAlarmList() - setScrollToTop(isSmoothScroll = true) - } - } - - private fun setScrollToTop(isSmoothScroll: Boolean = false) { - with(binding.rvNotificationList) { - if (isSmoothScroll) this.smoothScrollToPosition(0) - else this.scrollToPosition(0) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingFirstFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingFirstFragment.kt deleted file mode 100644 index 2c53b1c4..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingFirstFragment.kt +++ /dev/null @@ -1,30 +0,0 @@ -package daily.dayo.presentation.fragment.onboarding - -import android.graphics.Typeface -import android.os.Bundle -import android.text.Spannable -import android.text.style.StyleSpan -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import daily.dayo.presentation.databinding.FragmentOnBoardingFirstBinding -import daily.dayo.presentation.common.autoCleared - -class OnBoardingFirstFragment : Fragment() { - private var binding by autoCleared() - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentOnBoardingFirstBinding.inflate(inflater, container, false) - setMessageTextSpan() - return binding.root - } - - private fun setMessageTextSpan() { - val spannableText: Spannable = binding.tvOnboardingFirstMessage.text as Spannable - spannableText.setSpan(StyleSpan(Typeface.BOLD), 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingFourthFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingFourthFragment.kt deleted file mode 100644 index 38026021..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingFourthFragment.kt +++ /dev/null @@ -1,22 +0,0 @@ -package daily.dayo.presentation.fragment.onboarding - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.databinding.FragmentOnBoardingFourthBinding - -class OnBoardingFourthFragment : Fragment() { - private var binding by autoCleared() - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentOnBoardingFourthBinding.inflate(inflater, container, false) - - return binding.root - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingSecondFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingSecondFragment.kt deleted file mode 100644 index c9d0cf1b..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingSecondFragment.kt +++ /dev/null @@ -1,30 +0,0 @@ -package daily.dayo.presentation.fragment.onboarding - -import android.graphics.Typeface -import android.os.Bundle -import android.text.Spannable -import android.text.style.StyleSpan -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.databinding.FragmentOnBoardingSecondBinding - -class OnBoardingSecondFragment : Fragment() { - private var binding by autoCleared() - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentOnBoardingSecondBinding.inflate(inflater, container, false) - setMessageTextSpan() - return binding.root - } - - private fun setMessageTextSpan() { - val spannableText: Spannable = binding.tvOnboardingSecondMessage.text as Spannable - spannableText.setSpan(StyleSpan(Typeface.BOLD), 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingThirdFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingThirdFragment.kt deleted file mode 100644 index 17d2c5a8..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/onboarding/OnBoardingThirdFragment.kt +++ /dev/null @@ -1,30 +0,0 @@ -package daily.dayo.presentation.fragment.onboarding - -import android.graphics.Typeface -import android.os.Bundle -import android.text.Spannable -import android.text.style.StyleSpan -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.databinding.FragmentOnBoardingThirdBinding - -class OnBoardingThirdFragment : Fragment() { - private var binding by autoCleared() - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentOnBoardingThirdBinding.inflate(inflater, container, false) - setMessageTextSpan() - return binding.root - } - - private fun setMessageTextSpan() { - val spannableText: Spannable = binding.tvOnboardingThirdMessage.text as Spannable - spannableText.setSpan(StyleSpan(Typeface.BOLD), 7, 17, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/policy/PolicyFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/policy/PolicyFragment.kt deleted file mode 100644 index fdde426a..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/policy/PolicyFragment.kt +++ /dev/null @@ -1,72 +0,0 @@ -package daily.dayo.presentation.fragment.policy - -import android.os.Bundle -import android.view.KeyEvent -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.webkit.WebViewClient -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import daily.dayo.presentation.BuildConfig -import daily.dayo.presentation.R -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentPolicyBinding -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class PolicyFragment : Fragment() { - private var binding by autoCleared() - private val args by navArgs() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentPolicyBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initActionbar() - showPolicy(args.informationType) - setWebViewBackClickListener() - } - - private fun initActionbar() { - binding.btnPolicyActionBarBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - binding.tvPolicyActionBarTitle.text = - if (args.informationType == "privacy") getString(R.string.policy_privacy) - else getString(R.string.policy_terms) - } - - private fun showPolicy(informationType: String) { - with(binding.webviewPolicyContents) { - visibility = View.VISIBLE - apply { - webViewClient = WebViewClient() - settings.javaScriptEnabled = false - } - loadUrl("${BuildConfig.BASE_URL}/$informationType.html") - } - binding.webviewPolicyContents.visibility = View.VISIBLE - } - - private fun setWebViewBackClickListener() { - binding.webviewPolicyContents.setOnKeyListener( - View.OnKeyListener { v, keyCode, event -> - if (event.action !== KeyEvent.ACTION_DOWN) return@OnKeyListener true - if (keyCode == KeyEvent.KEYCODE_BACK) { - findNavController().navigateUp() - return@OnKeyListener true - } - false - } - ) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/post/PostFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/post/PostFragment.kt deleted file mode 100644 index d2c2a479..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/post/PostFragment.kt +++ /dev/null @@ -1,553 +0,0 @@ -package daily.dayo.presentation.fragment.post - -import android.app.AlertDialog -import android.content.res.ColorStateList -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.os.Bundle -import android.os.Handler -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback -import com.airbnb.lottie.LottieAnimationView -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import com.google.android.material.chip.Chip -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.domain.model.Comment -import daily.dayo.domain.model.categoryKR -import daily.dayo.presentation.R -import daily.dayo.presentation.adapter.PostCommentAdapter -import daily.dayo.presentation.adapter.PostImageSliderAdapter -import daily.dayo.presentation.common.GlideLoadUtil -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.KeyboardVisibilityUtils -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.TimeChangerUtil -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.DefaultDialogConfigure -import daily.dayo.presentation.common.dialog.DefaultDialogConfirm -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentPostBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import daily.dayo.presentation.viewmodel.HomeViewModel -import daily.dayo.presentation.viewmodel.PostViewModel -import daily.dayo.presentation.viewmodel.SearchViewModel -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.launch - -@AndroidEntryPoint -class PostFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - keyboardVisibilityUtils.detachKeyboardListeners() - postViewModel.cleanUpPostDetail() - onDestroyBindingView() - } - private val accountViewModel by activityViewModels() - private val homeViewModel by activityViewModels() - private val postViewModel by activityViewModels() - private val searchViewModel by activityViewModels() - private val args by navArgs() - private lateinit var mAlertDialog: AlertDialog - private var glideRequestManager: RequestManager? = null - private var postCommentAdapter: PostCommentAdapter? = null - private var postImageSliderAdapter: PostImageSliderAdapter? = null - private var indicators: Array? = null - private lateinit var keyboardVisibilityUtils: KeyboardVisibilityUtils - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentPostBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) - keyboardVisibilityUtils = KeyboardVisibilityUtils(requireActivity().window, - onShowKeyboard = { keyboardHeight -> - binding.layoutScrollPost.run { - scrollTo(0, binding.layoutScrollPost.bottom + keyboardHeight) - } - }) - - setBackButtonClickListener() - setCommentListAdapter() - setImageSlider() - setPostDetailCollect() - setPostCommentCollect() - setCreatePostComment() - setPostCommentClickListener() - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.postFragment = this - } - - private fun onDestroyBindingView() { - glideRequestManager = null - postImageSliderAdapter = null - postCommentAdapter = null - indicators = null - binding.rvPostCommentList.adapter = null - } - - private fun setBackButtonClickListener() { - binding.btnPostBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setOnUserProfileClickListener(memberId: String) { - binding.imgPostUserProfile.setOnDebounceClickListener { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.PostFragment, - action = R.id.action_postFragment_to_profileFragment, - args = PostFragmentDirections.actionPostFragmentToProfileFragment(memberId = memberId).arguments - ) - } - binding.tvPostUserNickname.setOnDebounceClickListener { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.PostFragment, - action = R.id.action_postFragment_to_profileFragment, - args = PostFragmentDirections.actionPostFragmentToProfileFragment(memberId = memberId).arguments - ) - } - } - - private fun setCommentListAdapter() { - postCommentAdapter = glideRequestManager?.let { requestManager -> - PostCommentAdapter(requestManager = requestManager, userInfo = accountViewModel.getCurrentUserInfo()) - } - binding.rvPostCommentList.layoutManager = LinearLayoutManager(requireContext()) - binding.rvPostCommentList.adapter = postCommentAdapter - } - - private fun setPostOptionClickListener(isMine: Boolean, memberId: String) { - binding.btnPostOption.setOnDebounceClickListener { - if (isMine) { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.PostFragment, - action = R.id.action_postFragment_to_postOptionMineFragment, - args = PostFragmentDirections.actionPostFragmentToPostOptionMineFragment( - postId = args.postId - ).arguments - ) - } else { - Navigation.findNavController(it) - .navigateSafe( - currentDestinationId = R.id.PostFragment, - action = R.id.action_postFragment_to_postOptionFragment, - args = PostFragmentDirections.actionPostFragmentToPostOptionFragment( - postId = args.postId, - memberId = memberId - ).arguments - ) - } - } - } - - private fun setPostDetailCollect() { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { - postViewModel.postDetail.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { post -> - updatePostStatus(post.heart, post.heartCount, null) - binding.post = post - binding.createDateTime = TimeChangerUtil.timeChange(context = binding.tvPostTime.context, time = post.createDateTime) - binding.categoryKR = post.category?.let { category -> categoryKR(category) } - binding.executePendingBindings() - postImageSliderAdapter?.submitList(post.images) - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - glideRequestManager?.let { requestManager -> - GlideLoadUtil.loadImageViewProfile( - requestManager = requestManager, - width = binding.imgPostUserProfile.width, - height = binding.imgPostUserProfile.height, - imgName = post.profileImg, - imgView = binding.imgPostUserProfile - ) - } - } - } - - val isMine = - (post.memberId == accountViewModel.getCurrentUserInfo().memberId) - binding.btnPostLike.isSelected = post.heart - setPostLikeClickListener(isChecked = post.heart) - setPostLikeDoubleTap(isChecked = post.heart) - post.memberId?.let { memberId -> - setOnUserProfileClickListener(memberId) - setPostOptionClickListener(isMine = isMine, memberId = memberId) - } - post.images?.let { it1 -> if (it1.size > 1) setupIndicators(it1.size) } - post.bookmark?.let { it1 -> setPostBookmarkClickListener(it1) } - post.hashtags?.let { it1 -> setTagList(it1) } - } - } - - Status.LOADING -> {} - Status.ERROR -> {} - } - } - postViewModel.requestPostDetail(args.postId) - } - } - } - - private fun setPostCommentCollect() { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - postViewModel.requestPostComment(args.postId) - postViewModel.postComment.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { postComment -> - postCommentAdapter?.submitList(postComment.toMutableList()) - updatePostStatus(null, null, postComment.size) - binding.commentCount = postComment.size - binding.commentCountStr = postComment.size.toString() - } - } - - Status.LOADING -> {} - Status.ERROR -> {} - } - } - } - } - } - - private fun setPostCommentClickListener() { - postCommentAdapter?.setOnItemClickListener(object : PostCommentAdapter.OnItemClickListener { - override fun onItemClick(v: View, comment: Comment, position: Int) { - // Item ์ž์ฒด๋ฅผ ํด๋ฆญํ•˜๋Š” ๊ฒฝ์šฐ ๋‚˜ํƒ€๋‚˜๋Š” Event ์ž‘์„ฑ - } - - override fun DeletePostCommentClick(comment: Comment, position: Int) { - val deleteComment = { - postViewModel.requestDeletePostComment(commentId = comment.commentId) - postViewModel.postCommentDeleteSuccess.observe(viewLifecycleOwner) { isSuccess -> - if (isSuccess.getContentIfNotHandled() == true) { - refreshPostComment() - Toast.makeText( - requireContext(), - R.string.post_comment_delete_alert_message_success, - Toast.LENGTH_SHORT - ).show() - } else if (isSuccess.getContentIfNotHandled() == false) { - Toast.makeText( - requireContext(), - R.string.post_comment_delete_alert_message_fail, - Toast.LENGTH_SHORT - ).show() - } - } - } - - mAlertDialog = DefaultDialogConfirm.createDialog( - requireContext(), - R.string.post_comment_delete_alert_message, - true, - true, - null, - null, - deleteComment - ) { this@PostFragment.mAlertDialog.dismiss() } - mAlertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - - if (!mAlertDialog.isShowing) { - mAlertDialog.show() - DefaultDialogConfigure.dialogResize(requireContext(), mAlertDialog, 0.7f, 0.19f) - } - } - }) - } - - private fun setTagList(tagList: List) { - binding.chipgroupPostTagList.removeAllViews() - if (!tagList.isNullOrEmpty()) { - (tagList.indices).mapNotNull { index -> - val chip = - LayoutInflater.from(context).inflate(R.layout.item_post_tag, null) as Chip - val layoutParams = ViewGroup.MarginLayoutParams( - ViewGroup.MarginLayoutParams.WRAP_CONTENT, - ViewGroup.MarginLayoutParams.WRAP_CONTENT - ) - with(chip) { - chipBackgroundColor = - ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_pressed), - intArrayOf(android.R.attr.state_pressed) - ), - intArrayOf( - resources.getColor(R.color.gray_6_F0F1F3, context?.theme), - resources.getColor(R.color.primary_green_23C882, context?.theme) - ) - ) - - setTextColor( - ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_pressed), - intArrayOf(android.R.attr.state_pressed) - ), - intArrayOf( - resources.getColor(R.color.gray_1_313131, context?.theme), - resources.getColor(R.color.white_FFFFFF, context?.theme) - ) - ) - ) - ensureAccessibleTouchTarget(42.toPx()) - text = "# ${trimBlankText(tagList[index])}" - setOnDebounceClickListener { - searchViewModel.searchKeyword = trimBlankText(tagList[index]) - Navigation.findNavController(it).navigateSafe( - currentDestinationId = R.id.PostFragment, - action = R.id.action_postFragment_to_searchResultFragment - ) - } - } - binding.chipgroupPostTagList.addView(chip, layoutParams) - } - } - } - - private fun setImageSlider() { - postImageSliderAdapter = glideRequestManager?.let { requestManager -> - PostImageSliderAdapter( - requestManager = requestManager - ) - } - with(binding.vpPostImage) { - adapter = postImageSliderAdapter - setPageTransformer { page, position -> - // To Disable image changing animation - } - offscreenPageLimit = 1 - registerOnPageChangeCallback(object : OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - setCurrentIndicator(position) - } - }) - } - } - - // TODO: Indicator Setup ์ฝ”๋“œ ๊ฐœ์„  ํ•„์š” - private fun setupIndicators(count: Int) { - indicators = arrayOfNulls(count) - val params = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT - ) - params.setMargins(16, 8, 16, 8) - - indicators?.let { indicators -> - for (i in indicators.indices) { - indicators[i] = ImageView(requireContext()) - indicators[i]!!.setImageDrawable( - ContextCompat.getDrawable(requireContext(), R.drawable.ic_indicator_inactive) - ) - indicators[i]!!.layoutParams = params - binding.viewPostImageIndicators.addView(indicators[i]) - } - - // TODO: addView๋กœ ์ธํ•˜์—ฌ Indicator๊ฐ€ ๋น„์ •์ƒ์ ์œผ๋กœ ๋งŽ์•„์ง€๋Š” ํ˜„์ƒ ์ž„์‹œ ํ•ด๊ฒฐ - if (indicators.size < binding.viewPostImageIndicators.childCount) { - for (i in binding.viewPostImageIndicators.childCount - 1 downTo (indicators.size)) { - binding.viewPostImageIndicators.removeViewAt(i) - } - } - } - setCurrentIndicator(0) - } - - private fun setCurrentIndicator(position: Int) { - val childCount: Int = binding.viewPostImageIndicators.childCount - for (i in 0 until childCount) { - val imageView = binding.viewPostImageIndicators.getChildAt(i) as ImageView - if (i == position) { - imageView.setImageDrawable( - ContextCompat.getDrawable(requireContext(), R.drawable.ic_indicator_active) - ) - } else { - imageView.setImageDrawable( - ContextCompat.getDrawable(requireContext(), R.drawable.ic_indicator_inactive) - ) - } - } - } - - private fun setPostLikeClickListener(isChecked: Boolean) { - with(binding.btnPostLike) { - setOnDebounceClickListener { - setPostLike(isChecked) - } - } - } - - private fun setPostLikeDoubleTap(isChecked: Boolean) { - postImageSliderAdapter?.setOnItemClickListener(object : - PostImageSliderAdapter.OnItemClickListener { - override fun postImageDoubleTap(lottieAnimationView: LottieAnimationView) { - setPostLike(isChecked, lottieAnimationView) - } - }) - } - - private fun setPostLike(isChecked: Boolean, lottieAnimationView: LottieAnimationView? = null) { - if (!isChecked) { - binding.btnPostLike.isSelected = true - postViewModel.requestLikePost(postId = args.postId) - if (lottieAnimationView != null) { - lottieAnimationView.visibility = View.VISIBLE - lottieAnimationView.playAnimation() - } - } else { - if (lottieAnimationView == null) { - binding.btnPostLike.isSelected = false - postViewModel.requestUnlikePost(postId = args.postId) - } - } - } - - private fun setPostBookmarkClickListener(isChecked: Boolean) { - with(binding.btnPostBookmark) { - setOnDebounceClickListener { - if (!isChecked) { - postViewModel.requestBookmarkPost(postId = args.postId) - } else { - postViewModel.requestDeleteBookmarkPost(postId = args.postId) - }.let { - it.invokeOnCompletion { throwable -> - when (throwable) { - is CancellationException -> Log.e("Post Bookmark Click", "CANCELLED") - null -> { - postViewModel.requestPostDetail(postId = args.postId) - } - } - } - } - } - } - } - - private fun setCreatePostComment() { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - glideRequestManager?.let { requestManager -> - GlideLoadUtil.loadImageViewProfile( - requestManager = requestManager, - width = binding.imgPostCommentMyProfile.width, - height = binding.imgPostCommentMyProfile.height, - imgName = accountViewModel.getCurrentUserInfo().profileImg ?: "", - imgView = binding.imgPostCommentMyProfile - ) - } - } - } - - binding.tvPostCommentUpload.setOnDebounceClickListener { - val currentCommentEditText = trimBlankText(binding.etPostCommentDescription.text) - if (currentCommentEditText.isNotEmpty()) { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - postViewModel.requestCreatePostComment( - contents = currentCommentEditText, - postId = args.postId - ) - with(binding.etPostCommentDescription) { - setText("") - clearFocus() - HideKeyBoardUtil.hide(requireContext(), this) - } - postViewModel.postCommentCreateSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - refreshPostComment() - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - } - } - } - } - - fun setLikeCountClickListener() { - findNavController().navigateSafe( - currentDestinationId = R.id.PostFragment, - action = PostFragmentDirections.actionPostFragmentToPostLikeUsersFragment(postId = args.postId) - ) - } - - private fun refreshPostComment() { - // TODO : Handler ์ˆ˜์ • ํ•„์š” - Handler().postDelayed( - { - binding.rvPostCommentList.adapter?.let { - with(it as PostCommentAdapter) { - postViewModel.requestPostComment(args.postId) - submitList( - postViewModel.postComment.value?.data?.subList( - 0, - it.itemCount - )?.toMutableList() - ) - } - } - }, 60 - ) - Handler().postDelayed({ afterCreatedScroll() }, 200) - - } - - private fun afterCreatedScroll() { - binding.layoutScrollPost.fullScroll(View.FOCUS_DOWN) - } - - fun Int.toPx(): Int { - val density = resources.displayMetrics.density - return (this * density).toInt() - } - - // Home์—์„œ๋„ ์—…๋ฐ์ดํŠธํ•œ ๋‚ด์šฉ์„ ๋ฐ˜์˜ํ•  ์ˆ˜ ์žˆ๋„๋ก Status ์—…๋ฐ์ดํŠธ - private fun updatePostStatus( - heart: Boolean? = null, - heartCount: Int? = null, - commentCount: Int? = null - ) { - homeViewModel.setPostStatus( - args.postId, - heart, - heartCount, - commentCount - ) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/post/PostLikeUsersFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/post/PostLikeUsersFragment.kt deleted file mode 100644 index 27fc7665..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/post/PostLikeUsersFragment.kt +++ /dev/null @@ -1,342 +0,0 @@ -package daily.dayo.presentation.fragment.post - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.indication -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CenterAlignedTopAppBar -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import androidx.paging.LoadState -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import daily.dayo.presentation.BuildConfig -import daily.dayo.presentation.R -import daily.dayo.presentation.common.Event -import daily.dayo.presentation.common.extension.clickableSingle -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.toSp -import daily.dayo.presentation.viewmodel.FollowViewModel -import daily.dayo.presentation.viewmodel.PostViewModel -import com.skydoves.landscapist.ImageOptions -import com.skydoves.landscapist.glide.GlideImage -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.domain.model.LikeUser -import daily.dayo.presentation.viewmodel.AccountViewModel - -@AndroidEntryPoint -class PostLikeUsersFragment : Fragment() { - private val args by navArgs() - private val accountViewModel by activityViewModels() - private val postViewModel by activityViewModels() - private val followViewModel by activityViewModels() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - requestPostLikeUsers() - return ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - MaterialTheme { - Scaffold(topBar = { SetPostLikeUsersActionbar() }) { contentPadding -> - Box(modifier = Modifier.padding(contentPadding)) { - SetPostLikeUsers() - } - } - } - } - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - private fun SetPostLikeUsersActionbar() { - val interactionSource = remember { MutableInteractionSource() } - CenterAlignedTopAppBar( - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = colorResource(id = R.color.white_FFFFFF) - ), - title = { - Text( - text = stringResource(id = R.string.post_like_users_title), - style = TextStyle( - fontSize = 16.dp.toSp(), - lineHeight = 24.dp.toSp(), -// TODO fontFamily = FontFamily(Font(R.font.pretendard)), - fontWeight = FontWeight(600), - color = colorResource(id = R.color.gray_1_313131), - textAlign = TextAlign.Center - ) - ) - }, - navigationIcon = { - IconButton( - onClick = { findNavController().navigateUp() }, - modifier = Modifier - .indication(interactionSource = interactionSource, indication = null) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_back_sign), - contentDescription = "back sign", - ) - } - } - ) - } - - @Composable - private fun SetPostLikeUsers() { - val likeUsers = postViewModel.postLikeUsers.collectAsLazyPagingItems() - when (likeUsers.loadState.refresh) { - is LoadState.Loading -> {} - is LoadState.Error -> {} - is LoadState.NotLoading -> { - SetPostLikeUsersLayout(likeUsers = likeUsers) - } - } - } - - @Composable - private fun SetPostLikeUsersLayout(likeUsers: LazyPagingItems) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .background(color = colorResource(id = R.color.white_FFFFFF)) - ) { - items(likeUsers.itemCount) { index -> - val item = likeUsers[index] - LikeUserLayout(likeUser = item!!) - } - } - } - - @Composable - private fun LikeUserLayout(likeUser: LikeUser) { - Surface( - color = colorResource(id = R.color.white_FFFFFF), - modifier = Modifier.fillMaxWidth() - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickableSingle { navigateUserProfile(likeUser.memberId) } - .padding(horizontal = 18.dp, vertical = 8.dp) - ) { - LikeUserImageLayout(likeUser = likeUser) - LikeUserNicknameLayout(userNickname = likeUser.nickname) - Spacer(modifier = Modifier.weight(1f)) - if (likeUser.memberId != accountViewModel.getCurrentUserInfo().memberId) { - LikeUserFollowLayout(likeUser = likeUser) - } - } - } - } - - @Composable - private fun LikeUserImageLayout(likeUser: LikeUser) { - val imageInteractionSource = remember { MutableInteractionSource() } - - GlideImage( - imageModel = { "${BuildConfig.BASE_URL}/images/${likeUser.profileImg}" }, - imageOptions = ImageOptions( - contentDescription = "image description", - contentScale = ContentScale.Crop, - ), - modifier = Modifier - .padding(1.dp) - .height(USER_THUMBNAIL_SIZE.dp) - .aspectRatio(1f) - .clip(CircleShape) - .clickableSingle( - interactionSource = imageInteractionSource, - indication = null, - onClick = { navigateUserProfile(likeUser.memberId) } - ) - ) - } - - @Composable - private fun LikeUserNicknameLayout(userNickname: String) { - Text( - text = userNickname, - style = TextStyle( - fontSize = 13.dp.toSp(), -// TODO fontFamily = FontFamily(Font(R.font.sandoll gothicneo1)), - fontWeight = FontWeight(400), - color = colorResource(id = R.color.gray_1_313131), - ), - modifier = Modifier - .fillMaxHeight() - .padding(start = 11.dp) - ) - } - - @Composable - private fun LikeUserFollowLayout(likeUser: LikeUser) { - val followInteractionSource = remember { MutableInteractionSource() } - val followIsPressed by followInteractionSource.collectIsPressedAsState() - - var followState by rememberSaveable { mutableStateOf(likeUser.follow) } - val followSuccess by followViewModel.followingFollowSuccess.observeAsState(Event(true)) - val unFollowSuccess by followViewModel.followerUnfollowSuccess.observeAsState(Event(true)) - - TextButton( - onClick = { - if (followState) { - followViewModel.requestDeleteFollow( - followerId = likeUser.memberId, - isFollower = true - ) - if (unFollowSuccess.getContentIfNotHandled() == true) { - followState = false - } - } else { - followViewModel.requestCreateFollow( - followerId = likeUser.memberId, - isFollower = false - ) - if (followSuccess.getContentIfNotHandled() == true) { - followState = true - } - } - }, - interactionSource = followInteractionSource, - colors = ButtonDefaults.buttonColors( - if (followState or (!followState and followIsPressed)) colorResource(id = R.color.white_FFFFFF) - else colorResource(id = R.color.primary_green_23C882) - ), - contentPadding = PaddingValues(0.dp), - modifier = Modifier - .defaultMinSize(1.dp) - .border( - width = 1.dp, - color = - if (followState or (!followState and followIsPressed)) colorResource(id = R.color.gray_3_9FA5AE) - else colorResource(id = R.color.primary_green_23C882), - shape = RoundedCornerShape(size = FOLLOW_BUTTON_RADIUS_SIZE.dp), - ) - .background( - color = - if (followState or (!followState and followIsPressed)) colorResource(id = R.color.white_FFFFFF) - else colorResource(id = R.color.primary_green_23C882), - shape = RoundedCornerShape(size = FOLLOW_BUTTON_RADIUS_SIZE.dp) - ) - .width(FOLLOW_BUTTON_WIDTH.dp) - .height(FOLLOW_BUTTON_HEIGHT.dp) - ) { - Text( - text = if (followState) stringResource(id = R.string.follow_already) - else stringResource(id = R.string.follow_yet), - style = TextStyle( - fontSize = 14.dp.toSp(), - lineHeight = 21.dp.toSp(), -// TODO fontFamily = FontFamily(Font(R.font.pretendard)), - fontWeight = FontWeight(600), - color = if (followState or (!followState and followIsPressed)) colorResource(id = R.color.gray_3_9FA5AE) - else colorResource(id = R.color.white_FFFFFF), - textAlign = TextAlign.Center, - ), - modifier = Modifier - ) - } - } - - @Composable - @Preview - private fun PreviewLikeUserLayout() { - MaterialTheme { - LikeUserLayout( - likeUser = LikeUser( - false, - "sample-member-id", - "sample-Nickname", - "sampleImgUrl.jpg" - ), - ) - } - } - - @Composable - @Preview - private fun PreviewActionbarLayout() { - MaterialTheme { - SetPostLikeUsersActionbar() - } - } - - private fun requestPostLikeUsers() { - postViewModel.requestPostLikeUsers(postId = args.postId) - } - - private fun navigateUserProfile(memberId: String) { - findNavController().navigateSafe( - currentDestinationId = R.id.PostLikeUsersFragment, - action = PostLikeUsersFragmentDirections.actionPostLikeUsersFragmentToProfileFragment( - memberId = memberId - ) - ) - } - - companion object { - const val USER_THUMBNAIL_SIZE = 45 - const val FOLLOW_BUTTON_HEIGHT = 30 - const val FOLLOW_BUTTON_WIDTH = 85 - const val FOLLOW_BUTTON_RADIUS_SIZE = 31 - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/post/PostOptionMineFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/post/PostOptionMineFragment.kt deleted file mode 100644 index 6d155f9e..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/post/PostOptionMineFragment.kt +++ /dev/null @@ -1,115 +0,0 @@ -package daily.dayo.presentation.fragment.post - -import android.app.AlertDialog -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.os.Bundle -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.Window -import android.view.WindowManager -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.DefaultDialogConfigure -import daily.dayo.presentation.common.dialog.DefaultDialogConfirm -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentPostOptionMineBinding -import daily.dayo.presentation.viewmodel.PostViewModel - -@AndroidEntryPoint -class PostOptionMineFragment : DialogFragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val postViewModel by activityViewModels() - private val args by navArgs() - private lateinit var mAlertDialog: AlertDialog - private lateinit var loadingAlertDialog: AlertDialog - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - isCancelable = true - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentPostOptionMineBinding.inflate(inflater, container, false) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE) - dialog?.window?.setGravity(Gravity.BOTTOM) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setDeletePostClickListener() - setModifyPostClickListener() - } - - override fun onResume() { - super.onResume() - resizePostOptionMineDialogFragment() - } - - private fun resizePostOptionMineDialogFragment() { - val params: ViewGroup.LayoutParams? = dialog?.window?.attributes - val deviceWidth = DefaultDialogConfigure.getDeviceWidthSize(requireContext()) - params?.width = (deviceWidth * 0.9).toInt() - dialog?.window?.attributes = params as WindowManager.LayoutParams - } - - private fun setDeletePostClickListener() { - val deletePost = { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - postViewModel.requestDeletePost(postId = args.postId) - findNavController().navigateSafe( - currentDestinationId = R.id.PostOptionMineFragment, - action = R.id.action_postOptionMineFragment_to_HomeFragment - ) - } - - mAlertDialog = DefaultDialogConfirm.createDialog( - requireContext(), - R.string.post_option_mine_delete_alert_message, - true, - true, - null, - null, - { deletePost() } - ) { - binding.layoutPostOptionMine.visibility = View.VISIBLE - this.mAlertDialog.dismiss() - } - mAlertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - - binding.layoutPostOptionMineDelete.setOnDebounceClickListener { - if (!mAlertDialog.isShowing) { - binding.layoutPostOptionMine.visibility = View.INVISIBLE - mAlertDialog.show() - DefaultDialogConfigure.dialogResize(requireContext(), mAlertDialog, 0.7f, 0.19f) - } - } - } - - private fun setModifyPostClickListener() { - binding.layoutPostOptionMineModify.setOnDebounceClickListener { - val navigateWithDataPassAction = - PostOptionMineFragmentDirections.actionPostOptionMineFragmentToWriteFragment(postId = args.postId) - findNavController().navigateSafe( - currentDestinationId = R.id.PostOptionMineFragment, - action = navigateWithDataPassAction - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/report/ReportPostFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/report/ReportPostFragment.kt deleted file mode 100644 index 713f0d33..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/report/ReportPostFragment.kt +++ /dev/null @@ -1,112 +0,0 @@ -package daily.dayo.presentation.fragment.report - -import android.app.AlertDialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import daily.dayo.presentation.R -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentReportPostBinding -import daily.dayo.presentation.viewmodel.ReportViewModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class ReportPostFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val reportViewModel by activityViewModels() - private val args by navArgs() - private lateinit var reportPostOptionMap: Map - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentReportPostBinding.inflate(inflater, container, false) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - - initReportPostOption() - setOnClickReportPostOtherReason() - setBackButtonClickListener() - setReportPostButtonActivation() - setReportPostButtonClickListener() - return binding.root - } - - private fun initReportPostOption() { - reportPostOptionMap = mapOf( - binding.radiobuttonReportPostReason1.id to binding.radiobuttonReportPostReason1.text.toString(), - binding.radiobuttonReportPostReason2.id to binding.radiobuttonReportPostReason2.text.toString(), - binding.radiobuttonReportPostReason3.id to binding.radiobuttonReportPostReason3.text.toString(), - binding.radiobuttonReportPostReason4.id to binding.radiobuttonReportPostReason4.text.toString(), - binding.radiobuttonReportPostReason5.id to binding.radiobuttonReportPostReason5.text.toString(), - binding.radiobuttonReportPostReason6.id to binding.radiobuttonReportPostReason6.text.toString(), - binding.radiobuttonReportPostReason7.id to binding.radiobuttonReportPostReason7.text.toString() - ) - } - - private fun setBackButtonClickListener() { - binding.btnReportPostBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setOnClickReportPostOtherReason() { - binding.radiogroupReportPostReason.setOnCheckedChangeListener { _, id -> - if (binding.radiogroupReportPostReason.checkedRadioButtonId != -1) binding.btnReportPost.isSelected = - true - when (id) { - binding.radiobuttonReportPostReasonOther.id -> binding.etReportPostReasonOther.visibility = - View.VISIBLE - else -> binding.etReportPostReasonOther.visibility = View.INVISIBLE - } - } - } - - private fun setReportPostButtonActivation() { - if (binding.radiogroupReportPostReason.checkedRadioButtonId == -1) binding.btnReportPost.isSelected = - false - } - - private fun setReportPostButtonClickListener() { - binding.btnReportPost.setOnDebounceClickListener { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - if (isOtherOption()) { - reportViewModel.requestSavePostReport( - comment = binding.etReportPostReasonOther.text.toString(), - postId = args.postId - ) - } else { - reportPostOptionMap[binding.radiogroupReportPostReason.checkedRadioButtonId]?.let { - reportViewModel.requestSavePostReport( - comment = it, - postId = args.postId - ) - } - } - reportViewModel.reportPostSuccess.observe(viewLifecycleOwner) { - if (it) { - Toast.makeText( - requireContext(), - getString(R.string.report_post_alert_message), - Toast.LENGTH_SHORT - ).show() - findNavController().navigateUp() - } - } - } - } - - private fun isOtherOption() = - binding.radiogroupReportPostReason.checkedRadioButtonId == binding.radiobuttonReportPostReasonOther.id -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/report/ReportUserFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/report/ReportUserFragment.kt deleted file mode 100644 index c12fe187..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/report/ReportUserFragment.kt +++ /dev/null @@ -1,113 +0,0 @@ -package daily.dayo.presentation.fragment.report - -import android.app.AlertDialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import daily.dayo.presentation.R -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentReportUserBinding -import daily.dayo.presentation.viewmodel.ReportViewModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class ReportUserFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val reportViewModel by activityViewModels() - private val args by navArgs() - private lateinit var reportUserOptionMap: Map - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentReportUserBinding.inflate(inflater, container, false) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - - initReportUserOption() - setOnClickReportUserOtherReason() - setBackButtonClickListener() - setReportUserButtonActivation() - setReportUserButtonClickListener() - return binding.root - } - - private fun initReportUserOption() { - reportUserOptionMap = mapOf( - binding.radiobuttonReportUserReason1.id to binding.radiobuttonReportUserReason1.text.toString(), - binding.radiobuttonReportUserReason2.id to binding.radiobuttonReportUserReason2.text.toString(), - binding.radiobuttonReportUserReason3.id to binding.radiobuttonReportUserReason3.text.toString(), - binding.radiobuttonReportUserReason4.id to binding.radiobuttonReportUserReason4.text.toString(), - binding.radiobuttonReportUserReason5.id to binding.radiobuttonReportUserReason5.text.toString(), - binding.radiobuttonReportUserReason6.id to binding.radiobuttonReportUserReason6.text.toString(), - binding.radiobuttonReportUserReason7.id to binding.radiobuttonReportUserReason7.text.toString() - ) - } - - private fun setBackButtonClickListener() { - binding.btnReportUserBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setOnClickReportUserOtherReason() { - binding.radiogroupReportUserReason.setOnCheckedChangeListener { _, id -> - if (binding.radiogroupReportUserReason.checkedRadioButtonId != -1) binding.btnReportUser.isSelected = - true - when (id) { - binding.radiobuttonReportUserReasonOther.id -> binding.etReportUserReasonOther.visibility = - View.VISIBLE - else -> binding.etReportUserReasonOther.visibility = View.INVISIBLE - } - } - } - - private fun setReportUserButtonActivation() { - if (binding.radiogroupReportUserReason.checkedRadioButtonId == -1) binding.btnReportUser.isSelected = - false - } - - private fun setReportUserButtonClickListener() { - binding.btnReportUser.setOnDebounceClickListener { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - if (isOtherOption()) { - reportViewModel.requestSaveMemberReport( - comment = binding.etReportUserReasonOther.text.toString(), - memberId = args.memberId - ) - } else { - reportUserOptionMap[binding.radiogroupReportUserReason.checkedRadioButtonId]?.let { - reportViewModel.requestSaveMemberReport( - comment = it, - memberId = args.memberId - ) - } - } - reportViewModel.reportMemberSuccess.observe(viewLifecycleOwner) { - if (it) { - Toast.makeText( - requireContext(), - getString(R.string.report_post_alert_message), - Toast.LENGTH_SHORT - ).show() - findNavController().navigateUp() - } - } - } - } - - private fun isOtherOption() = - binding.radiogroupReportUserReason.checkedRadioButtonId == binding.radiobuttonReportUserReasonOther.id - -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/search/SearchFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/search/SearchFragment.kt index 3306c0ff..3e53c0ab 100644 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/search/SearchFragment.kt +++ b/presentation/src/main/java/daily/dayo/presentation/fragment/search/SearchFragment.kt @@ -104,7 +104,7 @@ class SearchFragment : Fragment() { } private fun initSearchKeywordRecentList() { - searchKeywordRecentList = searchViewModel.getSearchKeywordRecent() +// searchKeywordRecentList = searchViewModel.getSearchKeywordRecent() searchKeywordRecentAdapter?.submitList(searchKeywordRecentList) } @@ -116,15 +116,17 @@ class SearchFragment : Fragment() { } override fun deleteSearchKeywordRecentClick(keyword: String, pos: Int) { - searchViewModel.deleteSearchKeywordRecent(keyword) - searchKeywordRecentList = searchViewModel.getSearchKeywordRecent() +// searchViewModel.searchHistory.value?.data?.get(pos)?.let { +// searchViewModel.deleteSearchKeywordRecent(it.history, it.searchHistoryType) +// } +// searchKeywordRecentList = searchViewModel.getSearchKeywordRecent() searchKeywordRecentAdapter?.submitList(searchKeywordRecentList) } }) binding.tvSearchAllDelete.setOnDebounceClickListener { - searchViewModel.clearSearchKeywordRecent() - searchKeywordRecentList = searchViewModel.getSearchKeywordRecent() +// searchViewModel.clearSearchKeywordRecent() +// searchKeywordRecentList = searchViewModel.getSearchKeywordRecent() searchKeywordRecentAdapter?.submitList(searchKeywordRecentList) } } diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/search/SearchResultFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/search/SearchResultFragment.kt deleted file mode 100644 index 0ca9731a..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/search/SearchResultFragment.kt +++ /dev/null @@ -1,192 +0,0 @@ -package daily.dayo.presentation.fragment.search - -import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.paging.LoadState -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.domain.model.Search -import daily.dayo.presentation.R -import daily.dayo.presentation.adapter.SearchTagResultPostAdapter -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSearchResultBinding -import daily.dayo.presentation.viewmodel.SearchViewModel - -@AndroidEntryPoint -class SearchResultFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val searchViewModel by activityViewModels() - private var searchTagResultPostAdapter: SearchTagResultPostAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (savedInstanceState == null) - getSearchTagList(searchViewModel.searchKeyword) - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?, - ): View? { - binding = FragmentSearchResultBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - setSearchResult() - observeSearchTagList() - observeSearchTagCount() - setBackButtonClickListener() - setSearchEditTextListener() - setSearchTagResultPostAdapter() - setAdapterLoadStateListener() - setSearchTagResultPostClickListener() - setSearchKeywordInputDone() - setSearchKeywordInputRemoveClickListener() - HideKeyBoardUtil.hideTouchDisplay(requireActivity(), view) - } - - private fun onDestroyBindingView() { - glideRequestManager = null - searchTagResultPostAdapter = null - binding.rvSearchResultContentsPostList.adapter = null - } - - private fun setBackButtonClickListener() { - binding.btnSearchResultBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setSearchResult() { - loadingPost() - binding.tvSearchResultKeywordInput.setText(searchViewModel.searchKeyword) - } - - private fun setSearchEditTextListener() { - binding.tvSearchResultKeywordInput.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - with(binding.btnSearchResultRemoveEtInput) { - visibility = if (!s.isNullOrBlank()) { - View.VISIBLE - } else { - View.GONE - } - } - } - }) - } - - private fun setSearchTagResultPostAdapter() { - searchTagResultPostAdapter = - glideRequestManager?.let { SearchTagResultPostAdapter(requestManager = it) } - binding.rvSearchResultContentsPostList.adapter = searchTagResultPostAdapter - } - - private fun setSearchTagResultPostClickListener() { - searchTagResultPostAdapter?.setOnItemClickListener(object : - SearchTagResultPostAdapter.OnItemClickListener { - override fun onItemClick(v: View, search: Search, position: Int) { - findNavController().navigateSafe( - currentDestinationId = R.id.SearchResultFragment, - action = R.id.action_searchResultFragment_to_postFragment, - args = SearchResultFragmentDirections.actionSearchResultFragmentToPostFragment( - search.postId - ).arguments - ) - } - }) - } - - private fun setSearchKeywordInputDone() { - binding.tvSearchResultKeywordInput.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_SEARCH -> { - HideKeyBoardUtil.hide(requireContext(), binding.tvSearchResultKeywordInput) - binding.tvSearchResultKeywordInput.setText(ReplaceUnicode.replaceBlankText(binding.tvSearchResultKeywordInput.text.toString())) - if (ReplaceUnicode.replaceBlankText(binding.tvSearchResultKeywordInput.text.toString()).isNotBlank()) { - searchViewModel.searchKeyword = ReplaceUnicode.replaceBlankText(binding.tvSearchResultKeywordInput.text.toString()) - getSearchTagList(searchViewModel.searchKeyword) - } - true - } - - else -> false - } - } - } - - private fun setSearchKeywordInputRemoveClickListener() { - binding.btnSearchResultRemoveEtInput.setOnDebounceClickListener { - binding.tvSearchResultKeywordInput.setText("") - } - } - - private fun getSearchTagList(keyword: String) { - searchViewModel.searchKeyword(keyword) - } - - private fun observeSearchTagList() { - searchViewModel.searchTagList.observe(viewLifecycleOwner) { - searchTagResultPostAdapter?.submitData(viewLifecycleOwner.lifecycle, it) - } - } - - private fun observeSearchTagCount() { - searchViewModel.searchTotalCount.observe(viewLifecycleOwner) { - binding.resultCount = it - } - } - - private fun setAdapterLoadStateListener() { - searchTagResultPostAdapter?.let { - it.addLoadStateListener { loadState -> - if (loadState.refresh is LoadState.NotLoading) { - val isListEmpty = it.itemCount == 0 - if (isListEmpty) { - binding.layoutSearchResultContents.visibility = View.INVISIBLE - binding.layoutSearchResultEmpty.visibility = View.VISIBLE - } else { - binding.layoutSearchResultContents.visibility = View.VISIBLE - binding.layoutSearchResultEmpty.visibility = View.INVISIBLE - } - - if (isListEmpty || loadState.append is LoadState.NotLoading) { - completeLoadPost() - } - } - } - } - } - - private fun loadingPost() { - binding.layoutSearchResultPostShimmer.startShimmer() - binding.layoutSearchResultPostShimmer.visibility = View.VISIBLE - binding.rvSearchResultContentsPostList.visibility = View.INVISIBLE - } - - private fun completeLoadPost() { - binding.layoutSearchResultPostShimmer.stopShimmer() - binding.layoutSearchResultPostShimmer.visibility = View.GONE - binding.rvSearchResultContentsPostList.visibility = View.VISIBLE - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/SettingFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/SettingFragment.kt deleted file mode 100644 index 290cf822..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/SettingFragment.kt +++ /dev/null @@ -1,204 +0,0 @@ -package daily.dayo.presentation.fragment.setting - -import android.app.AlertDialog -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.R -import daily.dayo.presentation.activity.LoginActivity -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.DefaultDialogConfigure -import daily.dayo.presentation.common.dialog.DefaultDialogConfirm -import daily.dayo.presentation.common.dialog.DefaultDialogExplanationConfirm -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSettingBinding -import daily.dayo.presentation.viewmodel.AccountViewModel - -class SettingFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val accountViewModel by activityViewModels() - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSettingBinding.inflate(inflater, container, false) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setChangePasswordClickListener() - setBackButtonClickListener() - setLogoutButtonClickListener() - setContactButtonClickListener() - setWithdrawButtonClickListener() - setNotificationButtonClickListener() - setBlockButtonClickListener() - setInformationButtonClickListener() - setNoticeButtonClickListener() - } - - private fun setChangePasswordClickListener() { - binding.layoutSettingPasswordChange.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.SettingFragment, - action = R.id.action_settingFragment_to_settingChangePasswordCurrentFragment - ) - } - } - - private fun setBackButtonClickListener() { - binding.btnSettingBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNotificationButtonClickListener() { - binding.layoutSettingNotification.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.SettingFragment, - action = R.id.action_settingFragment_to_settingNotificationFragment - ) - } - } - - private fun setBlockButtonClickListener() { - binding.layoutSettingBlock.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.SettingFragment, - action = R.id.action_settingFragment_to_settingBlockFragment - ) - } - } - - private fun setInformationButtonClickListener() { - binding.layoutSettingInformation.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.SettingFragment, - action = R.id.action_settingFragment_to_informationFragment - ) - } - } - - private fun setNoticeButtonClickListener() { - binding.layoutSettingNotice.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.SettingFragment, - action = R.id.action_settingFragment_to_noticeFragment - ) - } - } - - private fun setContactButtonClickListener() { - binding.layoutSettingContact.setOnDebounceClickListener { - val contactAlertDialog = DefaultDialogExplanationConfirm.createDialog(requireContext(), - R.string.setting_contact_message, - R.string.setting_contact_explanation_message, - true, - false, - R.string.confirm, - R.string.cancel, - { doContact() }, - {}) - if (!contactAlertDialog.isShowing) { - contactAlertDialog.show() - DefaultDialogConfigure.dialogResize( - requireContext(), - contactAlertDialog, - 0.7f, - 0.23f - ) - } - } - } - - private fun doContact() { - val clipboard = - requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip: ClipData = ClipData.newPlainText("Contact E-Mail", "gemij.dev@gmail.com") - clipboard.setPrimaryClip(clip) - } - - private fun setLogoutButtonClickListener() { - binding.layoutSettingLogout.setOnDebounceClickListener { - val logoutAlertDialog = - DefaultDialogConfirm.createDialog(requireContext(), R.string.setting_logout_message, - true, true, R.string.confirm, R.string.cancel, { doLogout() }, {}) - if (!logoutAlertDialog.isShowing) { - logoutAlertDialog.show() - DefaultDialogConfigure.dialogResize( - requireContext(), - logoutAlertDialog, - 0.7f, - 0.23f - ) - } - logoutAlertDialog.setOnCancelListener { - logoutAlertDialog.dismiss() - } - } - } - - private fun setWithdrawButtonClickListener() { - binding.layoutSettingWithdraw.setOnDebounceClickListener { - val withdrawAlertDialog = DefaultDialogExplanationConfirm.createDialog(requireContext(), - R.string.setting_withdraw_message, - R.string.setting_withdraw_explanation_message, - true, - true, - R.string.confirm, - R.string.cancel, - { doWithdraw() }, - {}) - if (!withdrawAlertDialog.isShowing) { - withdrawAlertDialog.show() - DefaultDialogConfigure.dialogResize( - requireContext(), - withdrawAlertDialog, - 0.7f, - 0.23f - ) - } - withdrawAlertDialog.setOnCancelListener { - withdrawAlertDialog.dismiss() - } - } - } - - private fun doWithdraw() { - findNavController().navigateSafe( - currentDestinationId = R.id.SettingFragment, - action = R.id.action_settingFragment_to_withdrawFragment - ) - } - - private fun doLogout() { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - accountViewModel.requestLogout() - accountViewModel.logoutSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - accountViewModel.clearCurrentUser() - val intent = Intent(this.activity, LoginActivity::class.java) - startActivity(intent) - this.requireActivity().finish() - } - } - } -} - diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/block/SettingBlockFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/block/SettingBlockFragment.kt deleted file mode 100644 index 2b762a30..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/block/SettingBlockFragment.kt +++ /dev/null @@ -1,98 +0,0 @@ -package daily.dayo.presentation.fragment.setting.block - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.CheckBox -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSettingBlockBinding -import daily.dayo.presentation.adapter.BlockListAdapter -import daily.dayo.presentation.viewmodel.ProfileSettingViewModel -import daily.dayo.presentation.viewmodel.ProfileViewModel -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.domain.model.UserBlocked - -@AndroidEntryPoint -class SettingBlockFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val profileViewModel by viewModels() - private val profileSettingViewModel by viewModels() - private var blockListAdapter: BlockListAdapter? = null - private var glideRequestManager: RequestManager? = null - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSettingBlockBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setBackButtonClickListener() - setBlockListAdapter() - } - - override fun onResume() { - super.onResume() - setBlockList() - } - - private fun onDestroyBindingView() { - glideRequestManager = null - blockListAdapter = null - binding.rvBlock.adapter = null - } - - private fun setBlockListAdapter() { - blockListAdapter = glideRequestManager?.let { requestManager -> - BlockListAdapter(requestManager = requestManager) - } - binding.rvBlock.adapter = blockListAdapter - blockListAdapter?.setOnItemClickListener(object : - BlockListAdapter.OnItemClickListener { - override fun onItemClick(checkbox: CheckBox, blockUser: UserBlocked, position: Int) { - unblockUser(blockUser.memberId, position) - } - }) - } - - private fun setBlockList() { - profileSettingViewModel.requestBlockList() - profileSettingViewModel.blockList.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> it.data?.let { blockList -> - blockListAdapter?.submitList(blockList) - } - else -> {} - } - } - } - - private fun unblockUser(memberId: String, position: Int) { - profileViewModel.requestUnblockMember(memberId = memberId) - profileViewModel.unblockSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - profileSettingViewModel.requestBlockList() - } - } - } - - private fun setBackButtonClickListener() { - binding.btnSettingBlockBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/changePassword/SettingChangePasswordCurrentFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/changePassword/SettingChangePasswordCurrentFragment.kt deleted file mode 100644 index 6ce917a4..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/changePassword/SettingChangePasswordCurrentFragment.kt +++ /dev/null @@ -1,173 +0,0 @@ -package daily.dayo.presentation.fragment.setting.changePassword - -import android.os.Bundle -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.SetTextInputLayout -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSettingChangePasswordCurrentBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import java.util.regex.Pattern - -@AndroidEntryPoint -class SettingChangePasswordCurrentFragment : Fragment() { - private var binding by autoCleared() - private val accountViewModel by activityViewModels() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSettingChangePasswordCurrentBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initEditText() - setBackClickListener() - setTextChangeListener() - setNextClickListener() - setLimitEditTextInputType() - setTextEditorActionListener() - verifyPassword() - } - - override fun onResume() { - if (binding.etSettingChangePasswordCurrentUserInput.text.isNullOrEmpty()) - ButtonActivation.setSignupButtonInactive(requireContext(), binding.btnSettingChangePasswordCurrentNext) - super.onResume() - } - - private fun setBackClickListener() { - binding.btnSettingChangePasswordCurrentBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNextClickListener() { - binding.btnSettingChangePasswordCurrentNext.setOnDebounceClickListener { - checkPassword() - } - } - - private fun initEditText() { - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSettingChangePasswordCurrentUserInput, - binding.etSettingChangePasswordCurrentUserInput, - binding.etSettingChangePasswordCurrentUserInput.text.isNullOrEmpty() - ) - } - - private fun setTextChangeListener() { - with(binding.etSettingChangePasswordCurrentUserInput) { - addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - // Edit Text ์ƒํƒœ - with(binding.layoutSettingChangePasswordCurrentUserInput) { - isErrorEnabled = false - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSettingChangePasswordCurrentUserInput, - binding.etSettingChangePasswordCurrentUserInput, - false - ) - } - - // ๋ฒ„ํŠผ ํ™œ์„ฑ ์ƒํƒœ - if (s.toString().isNotEmpty()) { - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnSettingChangePasswordCurrentNext - ) - } else { - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSettingChangePasswordCurrentNext - ) - } - } - }) - } - } - - private fun checkPassword() { - accountViewModel.requestCheckCurrentPassword( - inputPassword = trimBlankText(binding.etSettingChangePasswordCurrentUserInput.text) - ) - } - - private fun verifyPassword() { - accountViewModel.checkCurrentPasswordSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ๋‹ค๋ฆ„ ํ™”๋ฉด์œผ๋กœ ์ด๋™ - findNavController().navigateSafe( - currentDestinationId = R.id.SettingChangePasswordCurrentFragment, - action = R.id.action_settingChangePasswordCurrentFragment_to_settingChangePasswordNewFragment - ) - } else if (it.getContentIfNotHandled() == false) { - // ์‹คํŒจ ์‹œ - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSettingChangePasswordCurrentUserInput, - binding.etSettingChangePasswordCurrentUserInput, - getString(R.string.setting_change_password_current_fail_message), - false - ) - } - } - } - - private fun setLimitEditTextInputType() { - // InputFilter๋กœ ๋„์–ด์“ฐ๊ธฐ ์ž…๋ ฅ๋งŒ ๋ง‰๊ณ  ๋‚˜๋จธ์ง€๋Š” ์•ˆ๋‚ด๋ฉ”์‹œ์ง€๋กœ ๋„์›Œ์ง€๋„๋ก ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ๋„ - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - binding.etSettingChangePasswordCurrentUserInput.filters = arrayOf(filterInputCheck) - } - - private fun setTextEditorActionListener() { - binding.etSettingChangePasswordCurrentUserInput.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide( - requireContext(), - binding.etSettingChangePasswordCurrentUserInput - ) - true - } - - else -> false - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/changePassword/SettingChangePasswordNewFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/changePassword/SettingChangePasswordNewFragment.kt deleted file mode 100644 index 22cff847..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/changePassword/SettingChangePasswordNewFragment.kt +++ /dev/null @@ -1,342 +0,0 @@ -package daily.dayo.presentation.fragment.setting.changePassword - -import android.app.AlertDialog -import android.os.Bundle -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.MutableLiveData -import androidx.navigation.fragment.findNavController -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.SetTextInputLayout -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSettingChangePasswordNewBinding -import daily.dayo.presentation.viewmodel.AccountViewModel -import java.util.regex.Pattern - -@AndroidEntryPoint -class SettingChangePasswordNewFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val accountViewModel by activityViewModels() - private var isNewConfirmation = MutableLiveData(false) - private var verifyPasswordTextWatcher: TextWatcher? = null - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSettingChangePasswordNewBinding.inflate(inflater, container, false) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setBackClickListener() - setLimitEditTextInputType(binding.etSettingChangePasswordNewUserInput) - setLimitEditTextInputType(binding.etSettingChangePasswordNewConfirmation) - setTextEditorActionListener( - binding.layoutSettingChangePasswordNewUserInput, - binding.etSettingChangePasswordNewUserInput, - getString(R.string.password), - getString(R.string.signup_email_set_password_message_length_fail_min) - ) - setTextEditorActionListener( - binding.layoutSettingChangePasswordNewConfirmation, - binding.etSettingChangePasswordNewConfirmation, - getString(R.string.password_confirmation), - getString(R.string.signup_email_set_password_confirmation_edittext_hint) - ) - initEditText( - binding.layoutSettingChangePasswordNewUserInput, - binding.etSettingChangePasswordNewUserInput, - getString(R.string.password), - getString(R.string.signup_email_set_password_message_length_fail_min) - ) - initEditText( - binding.layoutSettingChangePasswordNewConfirmation, - binding.etSettingChangePasswordNewConfirmation, - getString(R.string.password_confirmation), - getString(R.string.signup_email_set_password_confirmation_edittext_hint) - ) - setViewClickListener( - binding.layoutSettingChangePasswordNewUserInput, - binding.etSettingChangePasswordNewUserInput, - getString(R.string.password), - getString(R.string.signup_email_set_password_message_length_fail_min) - ) - setViewClickListener( - binding.layoutSettingChangePasswordNewConfirmation, - binding.etSettingChangePasswordNewConfirmation, - getString(R.string.password_confirmation), - getString(R.string.signup_email_set_password_confirmation_edittext_hint) - ) - observePasswordChange() - } - - private fun setBackClickListener() { - binding.btnSettingChangePasswordNewBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun observePasswordChange() { - isNewConfirmation.observe(viewLifecycleOwner) { - if (it) { - binding.layoutSettingChangePasswordNewUserInput.hint = getString(R.string.password) - checkNewPassword() - with(binding.btnSettingChangePasswordNewNext) { - text = getString(R.string.change_password) - setOnDebounceClickListener { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - accountViewModel.requestChangePassword( - newPassword = trimBlankText(binding.etSettingChangePasswordNewUserInput.text) - ) - accountViewModel.changePasswordSuccess.observe(viewLifecycleOwner) { - Toast.makeText( - requireContext(), - getString(R.string.change_password_success_message), - Toast.LENGTH_SHORT - ).show() - findNavController().navigateSafe( - currentDestinationId = R.id.SettingChangePasswordNewFragment, - action = R.id.action_settingChangePasswordNewFragment_to_settingFragment - ) - } - } - } - } else { - verifyPassword() - with((binding.btnSettingChangePasswordNewNext)) { - text = getString(R.string.next) - setOnDebounceClickListener { - binding.etSettingChangePasswordNewUserInput.removeTextChangedListener( - verifyPasswordTextWatcher - ) - binding.etSettingChangePasswordNewUserInput.backgroundTintList = - ContextCompat.getColorStateList(requireContext(), R.color.gray_6_F0F1F3) - binding.etSettingChangePasswordNewUserInput.isEnabled = false - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSettingChangePasswordNewNext - ) - binding.layoutSettingChangePasswordNewConfirmation.visibility = View.VISIBLE - isNewConfirmation.postValue(true) - } - } - } - } - } - - private fun initEditText( - layout: TextInputLayout, - editText: TextInputEditText, - titleText: String, - hintText: String - ) { - with(editText) { - setOnFocusChangeListener { _, hasFocus -> // Title - with(layout) { - if (hasFocus) { - hint = titleText - SetTextInputLayout.setEditTextTheme( - requireContext(), layout, editText, false - ) - } else { - hint = hintText - SetTextInputLayout.setEditTextTheme( - requireContext(), layout, editText, true - ) - } - } - } - } - } - - private fun changeEditTextTitle( - layout: TextInputLayout, - editText: TextInputEditText, - titleText: String, - hintText: String - ) { - with(layout) { - if (editText.text.isNullOrEmpty()) { - hint = hintText - } else { - hint = titleText - } - } - } - - private fun checkNewPassword() { - binding.etSettingChangePasswordNewConfirmation.addTextChangedListener(object : - TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (trimBlankText(binding.etSettingChangePasswordNewUserInput.text) - != trimBlankText(binding.etSettingChangePasswordNewConfirmation.text) - ) { // ๋™์ผ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์‚ฌ - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSettingChangePasswordNewNext - ) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSettingChangePasswordNewConfirmation, - binding.etSettingChangePasswordNewConfirmation, - getString(R.string.signup_email_set_password_confirmation_message_fail), - false - ) - } else { - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnSettingChangePasswordNewNext - ) - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSettingChangePasswordNewConfirmation, - binding.etSettingChangePasswordNewConfirmation, - null, - true - ) - } - } - }) - } - - private fun verifyPassword() { - verifyPasswordTextWatcher = object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (trimBlankText(s).length < 8) { // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธธ์ด ๊ฒ€์‚ฌ 1์— - SetTextInputLayout.setEditTextTheme( - requireContext(), - binding.layoutSettingChangePasswordNewUserInput, - binding.etSettingChangePasswordNewUserInput, - false - ) - // 8์ž ์ดํ•˜์ผ ๋•Œ์—๋Š” ์˜ค๋ฅ˜ ๊ฒ€์‚ฌ ์‹คํŒจ์‹œ, ์—๋Ÿฌ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚˜์˜ค์ง€ ์•Š๋„๋ก ๋””์ž์ธ ์ˆ˜์ • - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSettingChangePasswordNewNext - ) - } else if (trimBlankText(s).length > 16) { // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธธ์ด ๊ฒ€์‚ฌ 2 - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSettingChangePasswordNewUserInput, - binding.etSettingChangePasswordNewUserInput, - getString(R.string.signup_email_set_password_message_length_fail_max), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSettingChangePasswordNewNext - ) - } else if (!Pattern.matches("^[a-z|0-9|]+\$", trimBlankText(s))) { // ๋น„๋ฐ€๋ฒˆํ˜ธ ์–‘์‹ ๊ฒ€์‚ฌ - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSettingChangePasswordNewUserInput, - binding.etSettingChangePasswordNewUserInput, - getString(R.string.signup_email_set_password_message_format_fail), - false - ) - ButtonActivation.setSignupButtonInactive( - requireContext(), - binding.btnSettingChangePasswordNewNext - ) - } else { - SetTextInputLayout.setEditTextErrorTheme( - requireContext(), - binding.layoutSettingChangePasswordNewUserInput, - binding.etSettingChangePasswordNewUserInput, - null, - true - ) - ButtonActivation.setSignupButtonActive( - requireContext(), - binding.btnSettingChangePasswordNewNext - ) - } - } - } - binding.etSettingChangePasswordNewUserInput.addTextChangedListener(verifyPasswordTextWatcher) - } - - private fun setLimitEditTextInputType(editText: TextInputEditText) { - // InputFilter๋กœ ๋„์–ด์“ฐ๊ธฐ ์ž…๋ ฅ๋งŒ ๋ง‰๊ณ  ๋‚˜๋จธ์ง€๋Š” ์•ˆ๋‚ด๋ฉ”์‹œ์ง€๋กœ ๋„์›Œ์ง€๋„๋ก ์‚ฌ์šฉ์ž์—๊ฒŒ ์œ ๋„ - val filterInputCheck = InputFilter { source, start, end, dest, dstart, dend -> - val ps = Pattern.compile("^[ ]+\$") - if (ps.matcher(source).matches()) { - return@InputFilter "" - } - null - } - editText.filters = arrayOf(filterInputCheck) - } - - private fun setViewClickListener( - layout: TextInputLayout, - editText: TextInputEditText, - titleText: String, - hintText: String - ) { - requireView().setOnTouchListener { _, _ -> - HideKeyBoardUtil.hide(requireContext(), editText) - changeEditTextTitle(layout, editText, titleText, hintText) - SetTextInputLayout.setEditTextTheme( - requireContext(), - layout, - editText, - editText.text.isNullOrEmpty() - ) - true - } - } - - private fun setTextEditorActionListener( - layout: TextInputLayout, - editText: TextInputEditText, - titleText: String, - hintText: String - ) { - editText.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - HideKeyBoardUtil.hide(requireContext(), editText) - changeEditTextTitle(layout, editText, titleText, hintText) - SetTextInputLayout.setEditTextTheme( - requireContext(), - layout, - editText, - editText.text.isNullOrEmpty() - ) - true - } - - else -> false - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/information/InformationFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/information/InformationFragment.kt deleted file mode 100644 index 0e53565f..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/information/InformationFragment.kt +++ /dev/null @@ -1,70 +0,0 @@ -package daily.dayo.presentation.fragment.setting.information - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.BuildConfig -import daily.dayo.presentation.R -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentInformationBinding -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class InformationFragment : Fragment() { - private var binding by autoCleared() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentInformationBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setBackButtonClickListener() - setTermsClickListener() - setPrivacyClickListener() - setAppVersion() - } - - private fun setBackButtonClickListener() { - binding.btnInformationBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setTermsClickListener() { - binding.layoutInformationContentsPolicyTerms.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.InformationFragment, - action = R.id.action_informationFragment_to_policyFragment, - args = InformationFragmentDirections.actionInformationFragmentToPolicyFragment( - informationType = "terms" - ).arguments - ) - } - } - - private fun setPrivacyClickListener() { - binding.layoutInformationContentsPolicyPrivacy.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.InformationFragment, - action = R.id.action_informationFragment_to_policyFragment, - args = InformationFragmentDirections.actionInformationFragmentToPolicyFragment( - informationType = "privacy" - ).arguments - ) - } - } - - private fun setAppVersion() { - binding.versionName = requireActivity().packageManager.getPackageInfo(requireActivity().packageName, 0).versionName - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/notice/NoticeDetailFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/notice/NoticeDetailFragment.kt deleted file mode 100644 index 2635af5b..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/notice/NoticeDetailFragment.kt +++ /dev/null @@ -1,62 +0,0 @@ -package daily.dayo.presentation.fragment.setting.notice - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.webkit.WebViewClient -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentNoticeDetailBinding -import daily.dayo.presentation.viewmodel.NoticeViewModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class NoticeDetailFragment : Fragment() { - private var binding by autoCleared() - private val args by navArgs() - private val noticeViewModel by activityViewModels() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - binding = FragmentNoticeDetailBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setBackButtonClickListener() - setNoticeDescription() - observeNoticeDetail() - getNoticeDetail() - } - - private fun setBackButtonClickListener() { - binding.btnNoticeDetailBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setNoticeDescription() { - binding.notice = args.notice - } - - private fun getNoticeDetail() { - noticeViewModel.requestDetailNotice(args.notice.noticeId) - } - - private fun observeNoticeDetail() { - noticeViewModel.detailNotice.observe(viewLifecycleOwner) { - with(binding.webviewNoticeDetailContents) { - apply { - webViewClient = WebViewClient() - settings.javaScriptEnabled = false - } - loadDataWithBaseURL(null, it, "text/html", "UTF-8", null) - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/notice/NoticeListFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/notice/NoticeListFragment.kt deleted file mode 100644 index 18ef378f..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/notice/NoticeListFragment.kt +++ /dev/null @@ -1,87 +0,0 @@ -package daily.dayo.presentation.fragment.setting.notice - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.R -import daily.dayo.presentation.common.CustomDividerDecoration -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dp -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentNoticeListBinding -import daily.dayo.presentation.adapter.NoticeListAdapter -import daily.dayo.presentation.viewmodel.NoticeViewModel -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -@AndroidEntryPoint -class NoticeListFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val noticeViewModel by activityViewModels() - private var noticeListAdapter: NoticeListAdapter? = null - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentNoticeListBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setBackButtonClickListener() - getNoticeList() - initNoticeListAdapter() - } - - private fun onDestroyBindingView() { - noticeListAdapter = null - binding.rvNoticePost.adapter = null - } - - private fun setBackButtonClickListener() { - binding.btnNoticeBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun initNoticeListAdapter() { - noticeListAdapter = NoticeListAdapter() - with(binding.rvNoticePost) { - adapter = noticeListAdapter - addItemDecoration( - CustomDividerDecoration( - 1.dp.toFloat(), - 18.dp.toFloat(), - ContextCompat.getColor(requireContext(), R.color.gray_6_F0F1F3) - ) - ) - } - - observeNoticeList() - } - - private fun getNoticeList() { - noticeViewModel.requestAllNoticeList() - } - - private fun observeNoticeList() { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - noticeViewModel.noticeList.collectLatest { noticeList -> - noticeListAdapter?.submitData(noticeList) - } - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/notification/SettingNotificationFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/notification/SettingNotificationFragment.kt deleted file mode 100644 index 348b4562..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/notification/SettingNotificationFragment.kt +++ /dev/null @@ -1,227 +0,0 @@ -package daily.dayo.presentation.fragment.setting.notification - -import android.Manifest -import android.content.ContentValues.TAG -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build -import android.os.Bundle -import android.provider.Settings -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.app.ActivityCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.R -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentSettingNotificationBinding -import daily.dayo.presentation.activity.MainActivity -import daily.dayo.presentation.viewmodel.SettingNotificationViewModel -import com.google.firebase.ktx.Firebase -import com.google.firebase.messaging.ktx.messaging -import daily.dayo.presentation.viewmodel.AccountViewModel - -class SettingNotificationFragment : Fragment() { - private var binding by autoCleared() - private val accountViewModel by activityViewModels() - private val settingNotificationViewModel by activityViewModels() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSettingNotificationBinding.inflate(inflater, container, false) - checkCurrentNotification() - setInitSwitchState() - setBackButtonClickListener() - setDeviceNotification() - setNoticeNotification() - setReactionNotification() - return binding.root - } - - private fun checkCurrentNotification() { - if (!NotificationManagerCompat.from(requireContext()).areNotificationsEnabled()) { - Toast.makeText( - requireContext(), - getString(R.string.permission_fail_message_notification), - Toast.LENGTH_SHORT - ).show() - } - } - - private fun setInitSwitchState() { - if (accountViewModel.getCurrentUserNotiDevicePermit()) { - binding.switchSettingNotificationDevice.isChecked = true - binding.switchSettingNotificationNotice.isEnabled = true - binding.switchSettingNotificationReaction.isEnabled = true - settingNotificationViewModel.requestReceiveAlarm() - settingNotificationViewModel.notiReactionPermit.observe(viewLifecycleOwner) { reactionPermit -> - binding.notiReactionPermit = reactionPermit - } - binding.switchSettingNotificationNotice.isChecked = - accountViewModel.getCurrentUserNotiNoticePermit() - } else { - binding.switchSettingNotificationDevice.isChecked = false - binding.switchSettingNotificationNotice.isEnabled = false - binding.switchSettingNotificationReaction.isEnabled = false - } - } - - private fun setDeviceNotification() { - binding.switchSettingNotificationDevice.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { // ๋””๋ฐ”์ด์Šค ์•Œ๋ฆผ ์ผœ๊ธฐ - setNotificationOnState() // ๋””๋ฐ”์ด์Šค ์•Œ๋ฆผ ์ผฐ์„ ๋•Œ์˜ ๊ณต์ง€, ๋ฐ˜์‘ ์•Œ๋ฆผ ์ƒํƒœ์— ๋”ฐ๋ผ ์„ค์ • - } else { // ๋””๋ฐ”์ด์Šค ์•Œ๋ฆผ ๋„๊ธฐ - binding.switchSettingNotificationNotice.isEnabled = false - binding.switchSettingNotificationReaction.isEnabled = false - setNotificationOffState() // ๋””๋ฐ”์ด์Šค ์•Œ๋ฆผ ๊ป์„ ๋•Œ์˜ ๊ณต์ง€, ๋ฐ˜์‘ ์•Œ๋ฆผ ์„ค์ • ๋ชจ๋‘ ๋„๊ธฐ - } - } - } - - private fun setNoticeNotification() { - binding.switchSettingNotificationNotice.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { // ๊ณต์ง€ ์•Œ๋ฆผ ์ผœ๊ธฐ - Firebase.messaging.subscribeToTopic(getString(R.string.notification_topic_notice)) - .addOnCompleteListener { task -> - if (task.isSuccessful) Log.d(TAG, "NOTICE ์ˆ˜์‹ ") - } - accountViewModel.requestCurrentUserNotiNoticePermit(true) - } else { // ๊ณต์ง€ ์•Œ๋ฆผ ๋„๊ธฐ - Firebase.messaging.unsubscribeFromTopic("NOTICE") - .addOnCompleteListener { task -> - if (task.isSuccessful) Log.d(TAG, "NOTICE ์ˆ˜์‹  ๊ฑฐ๋ถ€") - } - accountViewModel.requestCurrentUserNotiNoticePermit(false) - } - } - } - - private fun setReactionNotification() { - binding.switchSettingNotificationReaction.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { // ๋ฐ˜์‘ ์•Œ๋ฆผ ์ผœ๊ธฐ - settingNotificationViewModel.requestReceiveChangeReceiveAlarm(onReceiveAlarm = true) - } else { // ๋ฐ˜์‘ ์•Œ๋ฆผ ๋„๊ธฐ - settingNotificationViewModel.requestReceiveChangeReceiveAlarm(onReceiveAlarm = false) - } - } - } - - private fun setNotificationOnState() { // ๋””๋ฐ”์ด์Šค ์•Œ๋ฆผ ์ผฐ์„ ๋•Œ์˜ ๊ณต์ง€, ๋ฐ˜์‘ ์•Œ๋ฆผ ์ƒํƒœ์— ๋”ฐ๋ผ ์„ค์ • - // This is only necessary for API level >= 33 (TIRAMISU) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (ContextCompat.checkSelfPermission( - requireContext(), - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - ) { - // FCM SDK (and your app) can post notifications. - binding.switchSettingNotificationReaction.isEnabled = true - binding.switchSettingNotificationNotice.isEnabled = true - - setFCM() - accountViewModel.requestCurrentUserNotiDevicePermit(true) - settingNotificationViewModel.requestReceiveAlarm() - settingNotificationViewModel.notiReactionPermit.observe(viewLifecycleOwner) { reactionPermit -> - binding.notiReactionPermit = reactionPermit - } - binding.switchSettingNotificationNotice.isChecked = - accountViewModel.getCurrentUserNotiNoticePermit() - } else if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) { - // TODO: display an educational UI explaining to the user the features that will be enabled - // by them granting the POST_NOTIFICATION permission. This UI should provide the user - // "OK" and "No thanks" buttons. If the user selects "OK," directly request the permission. - // If the user selects "No thanks," allow the user to continue without notifications. - } else { - // Directly ask for the permission - permissionLauncherNotification.launch(MainActivity.notificationPermission) - } - } - } - - private fun setNotificationOffState() { // ๋””๋ฐ”์ด์Šค ์•Œ๋ฆผ ๊ป์„ ๋•Œ์˜ ๊ณต์ง€, ๋ฐ˜์‘ ์•Œ๋ฆผ ์„ค์ • ๋ชจ๋‘ ๋„๊ธฐ - binding.switchSettingNotificationNotice.isChecked = false - binding.switchSettingNotificationReaction.isChecked = false - accountViewModel.requestCurrentUserNotiDevicePermit(false) - accountViewModel.requestCurrentUserNotiNoticePermit(false) - settingNotificationViewModel.unregisterFcmToken() - } - - private fun setBackButtonClickListener() { - binding.btnSettingNotificationBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setFCM() { - settingNotificationViewModel.registerDeviceToken() - } - - private val permissionLauncherNotification = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - val deniedList: List = permissions.filter { - !it.value - }.map { - it.key - } - - when { - deniedList.isNotEmpty() -> { - accountViewModel.requestCurrentUserNotiDevicePermit(false) - accountViewModel.requestCurrentUserNotiNoticePermit(false) - val map = deniedList.groupBy { permission -> - if (shouldShowRequestPermissionRationale(permission)) getString(R.string.permission_fail_second) else getString( - R.string.permission_fail_final - ) - } - map[getString(R.string.permission_fail_second)]?.let { - // request denied , request again - Toast.makeText( - requireContext(), - getString(R.string.permission_fail_message_notification), - Toast.LENGTH_SHORT - ).show() - ActivityCompat.requestPermissions( - requireActivity(), - MainActivity.notificationPermission, - 1000 - ) - } - map[getString(R.string.permission_fail_final)]?.let { - Intent().apply { - action = Settings.ACTION_APP_NOTIFICATION_SETTINGS - putExtra(Settings.EXTRA_APP_PACKAGE, requireActivity().packageName) - } - Toast.makeText( - requireContext(), - getString(R.string.permission_fail_final_message_notification), - Toast.LENGTH_SHORT - ).show() - } - - binding.switchSettingNotificationDevice.isChecked = false - binding.switchSettingNotificationReaction.isEnabled = false - binding.switchSettingNotificationNotice.isEnabled = false - } - else -> { - //All request are permitted - // ์•Œ๋ฆผ ์ตœ์ดˆ ํ—ˆ์šฉ์‹œ์— ๋ชจ๋“  ์•Œ๋ฆผ ํ—ˆ์šฉ์ฒ˜๋ฆฌ - accountViewModel.requestCurrentUserNotiDevicePermit(true) - accountViewModel.requestCurrentUserNotiNoticePermit(true) - settingNotificationViewModel.registerDeviceToken() - settingNotificationViewModel.requestReceiveAlarm() - setInitSwitchState() - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/withdraw/WithdrawFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/setting/withdraw/WithdrawFragment.kt deleted file mode 100644 index b86a7223..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/setting/withdraw/WithdrawFragment.kt +++ /dev/null @@ -1,108 +0,0 @@ -package daily.dayo.presentation.fragment.setting.withdraw - -import android.app.AlertDialog -import android.content.Intent -import android.os.Bundle -import android.text.Spannable -import android.text.style.ForegroundColorSpan -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.R -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentWithdrawBinding -import daily.dayo.presentation.activity.LoginActivity -import daily.dayo.presentation.viewmodel.AccountViewModel - -class WithdrawFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val accountViewModel by activityViewModels() - private lateinit var loadingAlertDialog: AlertDialog - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentWithdrawBinding.inflate(inflater, container, false) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - setFinalMessageTextSpan() - setOnClickWithdrawOtherReason() - setBackButtonClickListener() - setWithdrawButtonActivation() - setWithdrawButtonClickListener() - return binding.root - } - - private fun setBackButtonClickListener() { - binding.btnWithdrawBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setOnClickWithdrawOtherReason() { - binding.radiogroupWithdrawReason.setOnCheckedChangeListener { _, id -> - if (binding.radiogroupWithdrawReason.checkedRadioButtonId != -1) binding.btnWithdraw.isEnabled = - true - when (id) { - binding.radiobuttonWithdrawReasonOther.id -> binding.etWithdrawReasonOther.visibility = - View.VISIBLE - else -> binding.etWithdrawReasonOther.visibility = View.GONE - } - } - } - - private fun setWithdrawButtonActivation() { - if (binding.radiogroupWithdrawReason.checkedRadioButtonId == -1) { - binding.btnWithdraw.isEnabled = false - } - } - - private fun setWithdrawButtonClickListener() { - binding.btnWithdraw.setOnDebounceClickListener { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - val reason = when (binding.radiogroupWithdrawReason.checkedRadioButtonId) { - R.id.radiobutton_withdraw_reason_1 -> binding.radiobuttonWithdrawReason1.text.toString() - R.id.radiobutton_withdraw_reason_2 -> binding.radiobuttonWithdrawReason2.text.toString() - R.id.radiobutton_withdraw_reason_3 -> binding.radiobuttonWithdrawReason3.text.toString() - R.id.radiobutton_withdraw_reason_4 -> binding.radiobuttonWithdrawReason4.text.toString() - R.id.radiobutton_withdraw_reason_5 -> binding.radiobuttonWithdrawReason5.text.toString() - else -> binding.radiobuttonWithdrawReasonOther.text.toString() - } - accountViewModel.requestWithdraw(content = reason) - accountViewModel.withdrawSuccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - accountViewModel.clearCurrentUser() - val intent = Intent(this.activity, LoginActivity::class.java) - startActivity(intent) - this.requireActivity().finish() - } - } - } - } - - private fun setFinalMessageTextSpan() { - val spannableText: Spannable = binding.tvWithdrawFinalMessage.text as Spannable - spannableText.setSpan( - ForegroundColorSpan( - ContextCompat.getColor( - requireContext(), - R.color.primary_green_23C882 - ) - ), 16, 35, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteFolderAddFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteFolderAddFragment.kt deleted file mode 100644 index be0ec854..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteFolderAddFragment.kt +++ /dev/null @@ -1,107 +0,0 @@ -package daily.dayo.presentation.fragment.write - -import android.app.AlertDialog -import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.R -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.TextLimitUtil -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentWriteFolderAddBinding -import daily.dayo.domain.model.Privacy -import daily.dayo.presentation.viewmodel.WriteViewModel - -class WriteFolderAddFragment : Fragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val writeViewModel by activityViewModels() - private lateinit var loadingAlertDialog: AlertDialog - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentWriteFolderAddBinding.inflate(inflater, container, false) - loadingAlertDialog = LoadingAlertDialog.createLoadingDialog(requireContext()) - setBackButtonClickListener() - setConfirmButtonClickListener() - verifyFolderName() - - return binding.root - } - - private fun setBackButtonClickListener() { - binding.btnPostFolderAddBack.setOnDebounceClickListener { - findNavController().navigateUp() - } - } - - private fun setConfirmButtonClickListener() { - binding.tvPostFolderAddConfirm.setOnDebounceClickListener { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - createFolder() - writeViewModel.folderAddAccess.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - findNavController().popBackStack() - } else if (it.getContentIfNotHandled() == false) { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - Toast.makeText( - requireContext(), - R.string.folder_add_message_fail, - Toast.LENGTH_SHORT - ).show() - } - } - } - } - - private fun createFolder() { - val name: String = binding.etPostFolderAddSetTitle.text.toString() - val privacy: Privacy = - when (binding.radiogroupPostFolderAddSetPrivate.checkedRadioButtonId) { - binding.radiobuttonPostFolderAddSetPrivateAll.id -> Privacy.ALL - binding.radiobuttonPostFolderAddSetPrivateOnlyMe.id -> Privacy.ONLY_ME - else -> Privacy.ALL - } - writeViewModel.requestCreateFolderInPost(name, privacy) - } - - private fun verifyFolderName() { - val maxLength = 15 - binding.etPostFolderAddSetTitle.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - val text = s.toString() - val newText = TextLimitUtil.trimToMaxLength(text, maxLength) - if (text != newText) { - binding.etPostFolderAddSetTitle.setText(newText) - binding.etPostFolderAddSetTitle.setSelection(newText.length) - } - - if (trimBlankText(s).isEmpty()) { - ButtonActivation.setTextViewConfirmButtonInactive( - requireContext(), - binding.tvPostFolderAddConfirm - ) - } else { - ButtonActivation.setTextViewConfirmButtonActive( - requireContext(), - binding.tvPostFolderAddConfirm - ) - } - } - }) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteFolderFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteFolderFragment.kt deleted file mode 100644 index 3896ef59..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteFolderFragment.kt +++ /dev/null @@ -1,138 +0,0 @@ -package daily.dayo.presentation.fragment.write - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.fragment.findNavController -import daily.dayo.domain.model.Folder -import daily.dayo.presentation.R -import daily.dayo.presentation.adapter.WriteFolderAdapter -import daily.dayo.presentation.common.ButtonActivation -import daily.dayo.presentation.common.Event -import daily.dayo.presentation.common.Status -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentWriteFolderBinding -import daily.dayo.presentation.viewmodel.WriteViewModel -import kotlinx.coroutines.launch - -class WriteFolderFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val writeViewModel by activityViewModels() - private val writeFolderAdapter by lazy { - WriteFolderAdapter( - this::onFolderClicked, - writeViewModel.postFolderId.value!! - ) - } - private val loadingAlertDialog by lazy { LoadingAlertDialog.createLoadingDialog(requireContext()) } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentWriteFolderBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setBackButtonClickListener() - setFolderAddButtonClickListener() - setRvWriteFolderListAdapter() - setWriteFolderList() - } - - - override fun onStop() { - super.onStop() - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - - private fun onDestroyBindingView() { - binding.rvWriteFolderListSaved.adapter = null - } - - private fun setBackButtonClickListener() { - val onBackPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - displayLoadingDialog() - findNavController().navigateUp() - } - } - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - onBackPressedCallback - ) - - binding.btnWriteFolderBack.setOnDebounceClickListener { - displayLoadingDialog() - findNavController().navigateUp() - } - } - - private fun setFolderAddButtonClickListener() { - binding.tvWriteFolderAdd.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.WriteFolderFragment, - action = R.id.action_writeFolderFragment_to_folderAddFragment - ) - } - } - - private fun setRvWriteFolderListAdapter() { - binding.rvWriteFolderListSaved.adapter = writeFolderAdapter - } - - private fun onFolderClicked(folder: Folder) { - writeViewModel.setFolderId(folder.folderId.toString()) - writeViewModel.setFolderName(folder.title) - displayLoadingDialog() - findNavController().navigateUp() - } - - private fun setWriteFolderList() { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - writeViewModel.requestAllMyFolderList() - writeViewModel.folderList.observe(viewLifecycleOwner) { - when (it.status) { - Status.SUCCESS -> { - it.data?.let { folderList -> - writeFolderAdapter.submitList(folderList) - if (folderList.size < 5) { - ButtonActivation.setTextViewButtonActive( - requireContext(), - binding.tvWriteFolderAdd - ) - } else { - ButtonActivation.setTextViewButtonInactive( - requireContext(), - binding.tvWriteFolderAdd - ) - } - } - } - - else -> {} - } - } - } - } - } - - private fun displayLoadingDialog() { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - LoadingAlertDialog.resizeDialogFragment(requireContext(), loadingAlertDialog, 0.8f) - writeViewModel.showWriteOptionDialog.value = Event(true) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteFragment.kt deleted file mode 100644 index 71a36c4c..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteFragment.kt +++ /dev/null @@ -1,291 +0,0 @@ -package daily.dayo.presentation.fragment.write - -import android.os.Bundle -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import android.widget.Toast -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.LinearLayoutManager -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import daily.dayo.presentation.R -import daily.dayo.presentation.common.* -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.databinding.FragmentWriteBinding -import daily.dayo.presentation.adapter.WriteUploadImageListAdapter -import daily.dayo.presentation.viewmodel.WriteViewModel -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.domain.model.Category -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.launch - -@AndroidEntryPoint -class WriteFragment : Fragment() { - private var binding by autoCleared { onDestroyBindingView() } - private val writeViewModel by activityViewModels() - private val args by navArgs() - private var uploadImageListAdapter: WriteUploadImageListAdapter? = null - private var glideRequestManager: RequestManager? = null - - private var isCategorySelected = MutableStateFlow(false) - private var isImageUploaded = MutableStateFlow(false) - private var isLoadedEditPost: MutableLiveData = MutableLiveData(false) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentWriteBinding.inflate(inflater, container, false) - glideRequestManager = Glide.with(this) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setKeyboardMode() - setBackButtonClickListener() - setPostImagesAdapter() - initWritingContents() - observeUploadImages() - observeEditTextCountLimit() - setImageModifyListener() - setCategoryClickListener() - setUploadButtonClickListener() - showOptionDialog() - } - - private fun onDestroyBindingView() { - glideRequestManager = null - uploadImageListAdapter = null - binding.rvImgUploadList.adapter = null - } - - private fun setKeyboardMode() { - requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) - } - - private fun setBackButtonClickListener() { - val onBackPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - writeViewModel.resetWriteInfoValue() - findNavController().navigateUp() - } - } - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - onBackPressedCallback - ) - - binding.btnWritePostBack.setOnDebounceClickListener { - writeViewModel.resetWriteInfoValue() - findNavController().navigateUp() - } - } - - private fun showOptionDialog() { - writeViewModel.showWriteOptionDialog.observe(viewLifecycleOwner) { - if (it.getContentIfNotHandled() == true) { - writeViewModel.showWriteOptionDialog.value = Event(false) - findNavController().navigateSafe( - currentDestinationId = R.id.WriteFragment, - action = R.id.action_writeFragment_to_writeOptionFragment - ) - } - } - } - - private fun observeEditTextCountLimit() { - with(binding.etWriteDetail) { - filters = arrayOf(InputFilter.LengthFilter(200)) - addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - binding.editTextCount = s.toString().length - if (s.toString().length > 200) { - Toast.makeText( - requireContext(), - R.string.write_post_upload_alert_message_edittext_length_fail_max, - Toast.LENGTH_SHORT - ).show() - } - } - }) - } - } - - private fun initWritingContents() { - if (args.postId != 0 && isLoadedEditPost.value == false) { - isLoadedEditPost.value = true - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - isCategorySelected.emit(true) - isImageUploaded.emit(true) - } - } - writeViewModel.requestPostDetail(args.postId) - setOriginalContents() - } - } - - private fun setOriginalContents() { - // ๊ธฐ์กด ๊ธ€ ์ˆ˜์ •ํ•˜๋Š” ๊ฒฝ์šฐ - writeViewModel.writeCurrentPostDetail.observe(viewLifecycleOwner) { postDetail -> - when (postDetail.category) { - Category.SCHEDULER -> binding.radiobuttonWritePostCategoryScheduler.isChecked = - true - Category.STUDY_PLANNER -> binding.radiobuttonWritePostCategoryStudyplanner.isChecked = - true - Category.GOOD_NOTE -> binding.radiobuttonWritePostCategoryGoodnote.isChecked = - true - Category.POCKET_BOOK -> binding.radiobuttonWritePostCategoryPocketbook.isChecked = - true - Category.SIX_DIARY -> binding.radiobuttonWritePostCategorySixHoleDiary.isChecked = - true - Category.ETC -> binding.radiobuttonWritePostCategoryEtc.isChecked = - true - else -> {} - } - binding.etWriteDetail.setText(postDetail.contents) - } - } - - private fun setCategoryClickListener() { - binding.radiogroupWritePostCategory.setOnCheckedChangeListener(object : - RadioGridGroup.OnCheckedChangeListener { - override fun onCheckedChanged(group: RadioGridGroup?, checkedId: Int) { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - isCategorySelected.emit(true) - } - } - setCheckedCategoryName(checkedId) - } - }) - } - - private fun setCheckedCategoryName(checkedId: Int) { - writeViewModel.setPostCategory( - when (checkedId) { - R.id.radiobutton_write_post_category_scheduler -> Category.SCHEDULER - R.id.radiobutton_write_post_category_studyplanner -> Category.STUDY_PLANNER - R.id.radiobutton_write_post_category_goodnote -> Category.GOOD_NOTE - R.id.radiobutton_write_post_category_pocketbook -> Category.POCKET_BOOK - R.id.radiobutton_write_post_category_sixHoleDiary -> Category.SIX_DIARY - R.id.radiobutton_write_post_category_etc -> Category.ETC - else -> null - } - ) - } - - private fun setPostImagesAdapter() { - uploadImageListAdapter = glideRequestManager?.let { requestManager -> - WriteUploadImageListAdapter( - requestManager = requestManager, postId = args.postId - ) - } - with(binding.rvImgUploadList) { - layoutManager = - LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false) - adapter = uploadImageListAdapter - } - } - - private fun observeUploadImages() { - writeViewModel.postImageUriList.observe(viewLifecycleOwner) { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - isImageUploaded.emit(!it.isNullOrEmpty()) - } - } - uploadImageListAdapter?.submitList( - if (args.postId != 0 || it.size >= 5) it - else mutableListOf("") + it - ) - } - } - - private fun setImageModifyListener() { - if (args.postId == 0) { - writeViewModel.postImageUriList.observe(viewLifecycleOwner) { - uploadImageListAdapter?.setOnItemClickListener(object : - WriteUploadImageListAdapter.OnItemClickListener { - override fun deleteUploadImageClick(pos: Int) { - writeViewModel.deleteUploadImage(if (it.size >= 5) pos else pos - 1, true) - } - - override fun addUploadImageClick(pos: Int) { - findNavController().navigateSafe( - currentDestinationId = R.id.WriteFragment, - action = R.id.action_writeFragment_to_writeImageOptionFragment - ) - } - }) - } - } - } - - private fun setUploadButtonClickListener() { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - isCategorySelected.combine(isImageUploaded) { categorySelected, imageSelected -> - if (!categorySelected) { - R.string.write_post_upload_alert_message_empty_category - } else if (!imageSelected) { - R.string.write_post_upload_alert_message_empty_image - } else { - null - } - }.collect { toastMessage -> - with(binding.btnWritePostUpload) { - isSelected = (toastMessage == null) - setOnDebounceClickListener { - if (toastMessage != null) { - Toast.makeText( - requireContext(), - toastMessage, - Toast.LENGTH_SHORT - ).show() - } else { - with(writeViewModel) { - setPostId(args.postId) - setPostContents( - trimBlankText(binding.etWriteDetail.text.toString()).ifEmpty { "" } - ) - } - - findNavController().navigateSafe( - currentDestinationId = R.id.WriteFragment, - action = R.id.action_writeFragment_to_writeOptionFragment - ) - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteImageOptionFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteImageOptionFragment.kt deleted file mode 100644 index 3791a6c0..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteImageOptionFragment.kt +++ /dev/null @@ -1,309 +0,0 @@ -package daily.dayo.presentation.fragment.write - -import android.Manifest -import android.app.Activity -import android.content.Intent -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.provider.Settings -import android.view.* -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.RequiresApi -import androidx.core.app.ActivityCompat -import androidx.core.content.FileProvider -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.BuildConfig -import daily.dayo.presentation.R -import daily.dayo.presentation.common.dialog.DefaultDialogConfigure -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.dialog.LoadingAlertDialog.resizeDialogFragment -import daily.dayo.presentation.common.dialog.LoadingAlertDialog.showLoadingDialog -import daily.dayo.presentation.common.image.ImageUploadUtil.extension -import daily.dayo.presentation.common.image.ImageUploadUtil.isPermitExtension -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentWriteImageOptionBinding -import daily.dayo.presentation.viewmodel.WriteViewModel -import dagger.hilt.android.AndroidEntryPoint -import java.io.File -import java.io.IOException -import java.lang.NullPointerException -import java.text.SimpleDateFormat -import java.util.* - -@AndroidEntryPoint -class WriteImageOptionFragment : DialogFragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val writeViewModel by activityViewModels() - private lateinit var currentTakenPhotoPath: String - private val loadingAlertDialog by lazy { LoadingAlertDialog.createLoadingDialog(requireContext()) } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentWriteImageOptionBinding.inflate(inflater, container, false) - setDialogFragmentStyle() - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setImageSelectGalleryClickListener() - setImageTakePhotoClickListener() - } - - override fun onResume() { - super.onResume() - resizeImageOptionDialogFragment() - } - - private fun setDialogFragmentStyle() { - isCancelable = true - dialog?.window?.let { - it.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - it.requestFeature(Window.FEATURE_NO_TITLE) - it.setGravity(Gravity.BOTTOM) - } - } - - private fun resizeImageOptionDialogFragment() { - val params: ViewGroup.LayoutParams? = dialog?.window?.attributes - val deviceWidth = DefaultDialogConfigure.getDeviceWidthSize(requireContext()) - params?.width = (deviceWidth * 0.9).toInt() - dialog?.window?.attributes = params as WindowManager.LayoutParams - } - - private fun setImageSelectGalleryClickListener() { - binding.layoutWriteImageOptionSelectGallery.setOnDebounceClickListener { - requestOpenGallery.launch(PERMISSIONS_GALLERY) - } - } - - private val requestOpenGallery = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - val deniedList: List = permissions.filter { !it.value }.map { it.key } - - when { - deniedList.isNotEmpty() -> { - val map = deniedList.groupBy { permission -> - if (shouldShowRequestPermissionRationale(permission)) getString(R.string.permission_fail_second) - else getString(R.string.permission_fail_final) - } - map[getString(R.string.permission_fail_second)]?.let { - // request denied , request again - Toast.makeText( - requireContext(), - getString(R.string.permission_fail_message_gallery), - Toast.LENGTH_SHORT - ).show() - ActivityCompat.requestPermissions( - requireActivity(), - PERMISSIONS_GALLERY, - 1000 - ) - } - map[getString(R.string.permission_fail_final)]?.let { - Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).also { - val uri = Uri.parse("package:${requireContext().packageName}") - it.flags = Intent.FLAG_ACTIVITY_NEW_TASK - it.data = uri - } - Toast.makeText( - requireContext(), - getString(R.string.permission_fail_final_message_gallery), - Toast.LENGTH_SHORT - ).show() - //request denied ,send to settings - } - } - - else -> { - //All request are permitted - openGallery() - } - } - } - - private fun openGallery() { - val intent = Intent(Intent.ACTION_GET_CONTENT).apply { - type = "image/*" - putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - } - requestSelectGalleryActivity.launch(intent) - } - - private val requestSelectGalleryActivity = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> - if (activityResult.resultCode == Activity.RESULT_OK) { - val data: Intent? = activityResult.data - var remainingNum = 5 - writeViewModel.postImageUriList.size() - if (data?.clipData != null) { //์‚ฌ์ง„ ์—ฌ๋Ÿฌ ๊ฐœ ์„ ํƒ ์‹œ - var uriIdx = 0 - var count = data.clipData!!.itemCount - if (count >= remainingNum) { - Toast.makeText( - requireContext(), - getString(R.string.write_post_upload_alert_message_image_fail_max), - Toast.LENGTH_SHORT - ).show() - } - // ์„ ํƒํ•œ ๊ฐฏ์ˆ˜ ๋งŒํผ loop - while (count > 0) { - remainingNum = 5 - writeViewModel.postImageUriList.size() - if (remainingNum > 0 && uriIdx < data.clipData!!.itemCount) { - val imageUri = data.clipData!!.getItemAt(uriIdx).uri - if (imageUri.extension(requireActivity().applicationContext.contentResolver).isPermitExtension) { - writeViewModel.addUploadImage(imageUri.toString(), true) - } else { - Toast.makeText( - requireContext(), - getString(R.string.write_post_upload_alert_message_image_fail_file_extension), - Toast.LENGTH_SHORT - ).show() - } - } else break - - uriIdx++ - count-- - } - displayLoadingDialog() - findNavController().popBackStack() - } else { // ๋‹จ์ผ ์„ ํƒ - data?.data?.let { uri -> - if (remainingNum <= 1) { - Toast.makeText( - requireContext(), - getString(R.string.write_post_upload_alert_message_image_fail_max), - Toast.LENGTH_SHORT - ).show() - } - - if (remainingNum >= 1) { - val imageUri: Uri? = data.data - if (imageUri != null) { - if (imageUri.extension(requireActivity().applicationContext.contentResolver).isPermitExtension) { - writeViewModel.addUploadImage(imageUri.toString(), true) - showLoadingDialog(loadingAlertDialog) - resizeDialogFragment(requireContext(), loadingAlertDialog, 0.8f) - } else { - Toast.makeText( - requireContext(), - getString(R.string.write_post_upload_alert_message_image_fail_file_extension), - Toast.LENGTH_SHORT - ).show() - } - } - } - findNavController().popBackStack() - } - } - } - } - - private fun setImageTakePhotoClickListener() { - binding.layoutWriteImageOptionTakePhoto.setOnDebounceClickListener { - requestOpenCamera.launch( - PERMISSIONS_CAMERA - ) - } - } - - private val requestOpenCamera = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - permissions.entries.forEach { - if (!it.value) { - return@registerForActivityResult - } - } - openCamera() - } - - private fun openCamera() { - val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - if (intent.resolveActivity(requireContext().packageManager) != null) { - var photoFile: File? = null - val tmpDir: File? = requireContext().cacheDir - val timeStamp: String = - SimpleDateFormat("yyyy-MM-d-HH-mm-ss", Locale.KOREA).format(Date()) - val photoFileName = "Capture_${timeStamp}_" - try { - val tmpPhoto = File.createTempFile(photoFileName, ".jpg", tmpDir) - currentTakenPhotoPath = tmpPhoto.absolutePath - photoFile = tmpPhoto - } catch (e: IOException) { - e.printStackTrace() - } - if (photoFile != null) { - val photoURI = FileProvider.getUriForFile( - Objects.requireNonNull(requireContext().applicationContext), - BuildConfig.LIBRARY_PACKAGE_NAME + ".fileprovider", - photoFile - ) - intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI) - requestTakePhotoActivity.launch(intent) - } - } - } - - @RequiresApi(Build.VERSION_CODES.O) - private val requestTakePhotoActivity = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> - if (activityResult.resultCode == Activity.RESULT_OK) { - try { - val photoFile = File(currentTakenPhotoPath) - writeViewModel.addUploadImage(Uri.fromFile(photoFile).toString(), true) - displayLoadingDialog() - } catch (nullException: NullPointerException) { - Toast.makeText( - requireContext(), - getString(R.string.write_post_upload_alert_message_image_fail_null), - Toast.LENGTH_SHORT - ).show() - } finally { - findNavController().popBackStack() - } - } - } - - private fun displayLoadingDialog() { - showLoadingDialog(loadingAlertDialog) - resizeDialogFragment(requireContext(), loadingAlertDialog, 0.8f) - } - - companion object { - val PERMISSIONS_CAMERA = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - arrayOf( - Manifest.permission.CAMERA, - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_VIDEO - ) - } else { - arrayOf( - Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - } - val PERMISSIONS_GALLERY = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - arrayOf( - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_VIDEO - ) - } else { - arrayOf( - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteOptionFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteOptionFragment.kt deleted file mode 100644 index af27b06f..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteOptionFragment.kt +++ /dev/null @@ -1,158 +0,0 @@ -package daily.dayo.presentation.fragment.write - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.core.view.isVisible -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.common.Event -import daily.dayo.presentation.common.Px -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.dp -import daily.dayo.presentation.common.extension.navigateSafe -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentWriteOptionBinding -import daily.dayo.presentation.viewmodel.WriteViewModel -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.google.android.material.chip.Chip -import dagger.hilt.android.AndroidEntryPoint -import daily.dayo.presentation.R - -@AndroidEntryPoint -class WriteOptionFragment : BottomSheetDialogFragment() { - private var binding by autoCleared { - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - private val writeViewModel by activityViewModels() - private val loadingAlertDialog by lazy { LoadingAlertDialog.createLoadingDialog(requireContext()) } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentWriteOptionBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - observeUploadStateCallBack() - setUploadButtonClickListener() - setOptionTagListOriginalValue() - setOptionTagClickListener() - setOptionFolderClickListener() - setFolderDescription() - } - - private fun setUploadButtonClickListener() { - binding.btnWriteOptionConfirm.setOnDebounceClickListener { - when { - writeViewModel.postFolderId.value.isNullOrEmpty() -> { // ํด๋” ๋ฏธ์„ ํƒ์‹œ ๊ธ€ ์—…๋กœ๋“œ ๋ถˆ๊ฐ€ - Toast.makeText( - requireContext(), - getString(R.string.write_post_upload_alert_message_empty_folder), - Toast.LENGTH_SHORT - ).show() - } - else -> { - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - writeViewModel.requestUploadPost() - Toast.makeText( - requireContext(), - R.string.write_post_upload_alert_message_loading, - Toast.LENGTH_SHORT - ).show() - } - } - } - } - - private fun observeUploadStateCallBack() { - with(writeViewModel) { - writeSuccess.observe(viewLifecycleOwner) { isSuccess -> - setUploadResultCallback(isSuccess) - } - writeEditSuccess.observe(viewLifecycleOwner) { isSuccess -> - setUploadResultCallback(isSuccess) - } - } - } - - private fun setUploadResultCallback(isSuccess: Event) { - if (isSuccess.getContentIfNotHandled() == true) { - writeViewModel.writePostId.observe(viewLifecycleOwner) { - it.getContentIfNotHandled()?.let { writePostId -> - findNavController().navigateSafe( - currentDestinationId = R.id.WriteOptionFragment, - action = R.id.action_writeOptionFragment_to_postFragment, - args = WriteOptionFragmentDirections.actionWriteOptionFragmentToPostFragment( - writePostId - ).arguments - ) - } - } - } else if (isSuccess.getContentIfNotHandled() == false) { - Toast.makeText( - requireContext(), - R.string.write_post_upload_alert_message_fail, - Toast.LENGTH_SHORT - ).show() - } else { - } - } - - private fun setFolderDescription() { - writeViewModel.postFolderId.observe(viewLifecycleOwner) { - if (it.isNotEmpty()) { - binding.tvWriteOptionDescriptionFolder.text = writeViewModel.postFolderName.value - } - } - } - - private fun setOptionTagListOriginalValue() { - writeViewModel.postTagList.observe(viewLifecycleOwner) { - binding.tvWriteOptionDescriptionTag.isVisible = it.isNullOrEmpty() - if (!it.isNullOrEmpty()) { - (it.indices).mapNotNull { index -> - val chip = LayoutInflater.from(context) - .inflate(R.layout.item_write_post_tag_chip, null) as Chip - val layoutParams = ViewGroup.MarginLayoutParams( - ViewGroup.MarginLayoutParams.WRAP_CONTENT, - 48.dp - ) - with(chip) { - setTextAppearance(R.style.WritePostTagTextStyle) - isCloseIconVisible = false - isCheckable = false - ensureAccessibleTouchTarget(42.Px) - text = "# ${trimBlankText(it[index])}" - } - binding.chipgroupWriteOptionTagListSaved.addView(chip, layoutParams) - } - } - } - } - - private fun setOptionTagClickListener() { - binding.layoutWriteOptionTag.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.WriteOptionFragment, - action = R.id.action_writeOptionFragment_to_writeTagFragment - ) - } - } - - private fun setOptionFolderClickListener() { - binding.layoutWriteOptionFolder.setOnDebounceClickListener { - findNavController().navigateSafe( - currentDestinationId = R.id.WriteOptionFragment, - action = R.id.action_writeOptionFragment_to_writeFolderFragment - ) - } - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteTagFragment.kt b/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteTagFragment.kt deleted file mode 100644 index 03d83860..00000000 --- a/presentation/src/main/java/daily/dayo/presentation/fragment/write/WriteTagFragment.kt +++ /dev/null @@ -1,204 +0,0 @@ -package daily.dayo.presentation.fragment.write - -import android.os.Bundle -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.Toast -import androidx.activity.OnBackPressedCallback -import androidx.core.view.size -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import daily.dayo.presentation.R -import daily.dayo.presentation.common.Event -import daily.dayo.presentation.common.HideKeyBoardUtil -import daily.dayo.presentation.common.ReplaceUnicode.replaceBlankText -import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText -import daily.dayo.presentation.common.autoCleared -import daily.dayo.presentation.common.dialog.LoadingAlertDialog -import daily.dayo.presentation.common.dp -import daily.dayo.presentation.common.setOnDebounceClickListener -import daily.dayo.presentation.databinding.FragmentWriteTagBinding -import daily.dayo.presentation.viewmodel.WriteViewModel -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup - -class WriteTagFragment : Fragment() { - private var binding by autoCleared() - private val writeViewModel by activityViewModels() - private val loadingAlertDialog by lazy { LoadingAlertDialog.createLoadingDialog(requireContext()) } - private val originalTags by lazy { - (writeViewModel.postTagList.value ?: arrayListOf()).toMutableList() - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentWriteTagBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setBackButtonClickListener() - setSubmitButtonClickListener() - setEditTextAddTagKeyClickListener() - setEditTextAddTagLimit() - observeTags() - HideKeyBoardUtil.hideTouchDisplay(requireActivity(), requireView()) - } - - override fun onStop() { - super.onStop() - LoadingAlertDialog.hideLoadingDialog(loadingAlertDialog) - } - - private fun setBackButtonClickListener() { - val onBackPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - displayLoadingDialog() - findNavController().navigateUp() - } - } - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - onBackPressedCallback - ) - - binding.btnWriteTagBack.setOnDebounceClickListener { - displayLoadingDialog() - findNavController().navigateUp() - } - } - - private fun setSubmitButtonClickListener() { - binding.btnWritePostTagSubmit.setOnDebounceClickListener { - displayLoadingDialog() - findNavController().navigateUp() - } - } - - private fun setEditTextAddTagKeyClickListener() { - binding.etWriteTagAdd.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - val removeBlankTag = replaceBlankText(trimBlankText(binding.etWriteTagAdd.text)) - if (removeBlankTag.isNotEmpty() - && !binding.chipgroupWriteTagListSaved - .getAllChipsTagText() - .contains(removeBlankTag) - && binding.chipgroupWriteTagListSaved.size < MAX_TAG_COUNT - ) { - writeViewModel.addPostTag(removeBlankTag, true) - } - binding.etWriteTagAdd.setText("") - true - } - else -> false - } - } - } - - private fun setEditTextAddTagLimit() { - binding.tagCountMax = MAX_TAG_COUNT - - val lengthFilter = InputFilter.LengthFilter(MAX_TAG_LENGTH) - binding.etWriteTagAdd.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (trimBlankText(s).length >= MAX_TAG_LENGTH) { - Toast.makeText( - requireContext(), - String.format( - getString(R.string.write_post_tag_alert_message_tag_length_fail_max), - MAX_TAG_LENGTH - ), - Toast.LENGTH_SHORT - ).show() - } - } - }) - binding.etWriteTagAdd.filters = arrayOf(lengthFilter) - } - - private fun observeTags() { - writeViewModel.postTagList.observe(viewLifecycleOwner) { - with(binding) { - tagCount = it.size - isClickEnable = originalTags != it - } - displayTagCountLimitMessage(tagCount = it.size) - binding.chipgroupWriteTagListSaved.clearChips() - - (0 until it.size).mapNotNull { index -> - val chip = LayoutInflater.from(requireContext()) - .inflate(R.layout.item_write_post_tag_chip, null) as Chip - val layoutParams = ViewGroup.MarginLayoutParams( - ViewGroup.MarginLayoutParams.WRAP_CONTENT, - 48.dp - ) - with(chip) { - setTextAppearance(R.style.WritePostTagTextStyle) - setOnCloseIconClickListener { - writeViewModel.removePostTag( - trimBlankText( - (chip.text as String).replace( - "#", - "" - ) - ), true - ) - } - text = "# ${trimBlankText(it[index])}" - } - binding.chipgroupWriteTagListSaved.addView(chip, layoutParams) - } - } - } - - private fun displayTagCountLimitMessage(tagCount: Int) { - if (tagCount >= MAX_TAG_COUNT) { - Toast.makeText( - requireContext(), - String.format( - getString(R.string.write_post_tag_alert_message_tag_size_fail_max), - MAX_TAG_COUNT - ), - Toast.LENGTH_SHORT - ).show() - } - } - - private fun displayLoadingDialog() { - writeViewModel.showWriteOptionDialog.value = Event(true) - LoadingAlertDialog.showLoadingDialog(loadingAlertDialog) - LoadingAlertDialog.resizeDialogFragment(requireContext(), loadingAlertDialog, 0.8f) - } - - private fun ChipGroup.getAllChipsTagText(): List { - return (0 until childCount).mapNotNull { index -> - val currentChip = getChildAt(index) as? Chip - currentChip?.text.toString().split("# ")[1] - } - } - - private fun ChipGroup.clearChips() { - val chipViews = (0 until childCount).mapNotNull { index -> - val view = getChildAt(index) - if (view is Chip) view else null - } - chipViews.forEach { removeView(it) } - } - - companion object { - private const val MAX_TAG_COUNT = 8 - private const val MAX_TAG_LENGTH = 15 - } -} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/AccountNavigator.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/AccountNavigator.kt new file mode 100644 index 00000000..b47c9228 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/AccountNavigator.kt @@ -0,0 +1,52 @@ +package daily.dayo.presentation.screen.account + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import daily.dayo.presentation.screen.rules.RuleType +import daily.dayo.presentation.screen.rules.navigateRules + +class AccountNavigator( + val navController: NavHostController, +) { + private val currentDestination: NavDestination? + @Composable get() = navController.currentBackStackEntryAsState().value?.destination + + fun navigateSignIn() { + navController.navigateSignIn() + } + + fun navigateSignInEmail() { + navController.navigateSignInEmail() + } + + fun navigateResetPassword() { + navController.navigateResetPassword() + } + + fun navigateSignUpEmail() { + navController.navigateSignUpEmail() + } + + fun navigateProfileSetting() { + navController.navigateProfileSetting() + } + + fun navigateRules(ruleType: RuleType) { + navController.navigateRules(ruleType) + } + + fun popBackStack() { + navController.popBackStack() + } +} + +@Composable +internal fun rememberAccountNavigator( + navController: NavHostController = rememberNavController(), +): AccountNavigator = remember(navController) { + AccountNavigator(navController) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/AccountScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/AccountScreen.kt new file mode 100644 index 00000000..da8ebb45 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/AccountScreen.kt @@ -0,0 +1,69 @@ +package daily.dayo.presentation.screen.account + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Scaffold +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import daily.dayo.presentation.view.dialog.getBottomSheetDialogState + +@SuppressLint("UnusedMaterialScaffoldPaddingParameter") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AccountScreen( + navigator: AccountNavigator = rememberAccountNavigator() +) { + val coroutineScope = rememberCoroutineScope() + val snackBarHostState = remember { SnackbarHostState() } + val bottomSheetState = getBottomSheetDialogState() + var bottomSheet: (@Composable () -> Unit)? by remember { mutableStateOf(null) } + val bottomSheetContent: (@Composable () -> Unit) -> Unit = { + bottomSheet = it + } + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackBarHostState) } + ) { + Scaffold( + bottomBar = { bottomSheet?.let { it() } } + ) { + Scaffold( + content = { innerPadding -> + Box(Modifier.padding(innerPadding)) { + NavHost( + navController = navigator.navController, + startDestination = AccountScreen.SignIn.route + ) { + signInNavGraph( + coroutineScope = coroutineScope, + snackBarHostState = snackBarHostState, + navController = navigator.navController, + onBackClick = { navigator.popBackStack() }, + navigateToSignIn = { navigator.navigateSignIn() }, + navigateToSignInEmail = { navigator.navigateSignInEmail() }, + navigateToResetPassword = { navigator.navigateResetPassword() }, + navigateToSignUpEmail = { navigator.navigateSignUpEmail() }, + navigateToProfileSetting = { navigator.navigateProfileSetting() }, + navigateToRules = { route -> navigator.navigateRules(route) }, + bottomSheetState = bottomSheetState, + bottomSheetContent = bottomSheetContent + ) + } + } + }) + } + } +} + +sealed class AccountScreen(val route: String) { + object SignIn : AccountScreen(SignInRoute.route) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt new file mode 100644 index 00000000..35bc2c24 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/ResetPasswordScreen.kt @@ -0,0 +1,811 @@ +package daily.dayo.presentation.screen.account + +import android.content.Context +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.screen.account.model.CheckOAuthEmailStatus +import daily.dayo.presentation.screen.account.model.EmailCertificationState +import daily.dayo.presentation.screen.account.model.EmailExistenceStatus +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.DayoPasswordTextField +import daily.dayo.presentation.view.DayoTextButton +import daily.dayo.presentation.view.DayoTextField +import daily.dayo.presentation.view.DayoTimerTextField +import daily.dayo.presentation.view.FilledRoundedCornerButton +import daily.dayo.presentation.view.Loading +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.dialog.ConfirmDialog +import daily.dayo.presentation.viewmodel.AccountViewModel +import daily.dayo.presentation.viewmodel.AccountViewModel.Companion.RESET_PASSWORD_EMAIL_CERTIFICATE_AUTH_CODE_FAIL +import daily.dayo.presentation.viewmodel.AccountViewModel.Companion.EMAIL_CERTIFICATE_AUTH_CODE_INITIAL +import kotlinx.coroutines.CoroutineScope + +const val RESET_PASSWORD_EMAIL_CERTIFICATE_AUTH_TIME_OUT = 180 + +@Composable +internal fun ResetPasswordRoute( + coroutineScope: CoroutineScope = rememberCoroutineScope(), + snackBarHostState: SnackbarHostState, + onBackClick: () -> Unit = {}, + navigateToSignIn: () -> Unit = {}, + accountViewModel: AccountViewModel = hiltViewModel() +) { + val context = LocalContext.current + val keyboardController = LocalSoftwareKeyboardController.current + val resetPasswordStep = remember { mutableStateOf(ResetPasswordStep.EMAIL_INPUT) } + val isNextButtonEnabled = remember { mutableStateOf(false) } + val isNextButtonClickable = remember { mutableStateOf(false) } + + val email = remember { mutableStateOf("") } + val emailCertification = + remember { mutableStateOf(EmailCertificationState.BEFORE_CERTIFICATION) } + val isEmailExist by accountViewModel.checkEmailSuccess.collectAsStateWithLifecycle() + val isOAuthEmail by accountViewModel.checkOAuthEmailSuccess.collectAsStateWithLifecycle() + val certificateEmailAuthCode by accountViewModel.certificateEmailAuthCode.collectAsStateWithLifecycle() + val certificationInputCode = remember { mutableStateOf("") } + val isEmailCertificateError: MutableState = remember { mutableStateOf(null) } + + val password = remember { mutableStateOf("") } + val isPasswordFormatValid = remember { mutableStateOf(true) } + val passwordConfirmation = remember { mutableStateOf("") } + val isPasswordMismatch = remember { mutableStateOf(false) } + + val resetPasswordStatus by accountViewModel.resetPasswordSuccess.collectAsStateWithLifecycle() + + var isCheckingEmail by remember { mutableStateOf(false) } + + + LaunchedEffect(resetPasswordStatus) { + when (resetPasswordStatus) { + Status.SUCCESS -> { + if (resetPasswordStep.value == ResetPasswordStep.NEW_PASSWORD_CONFIRM) { + onBackClick() + } + } + Status.ERROR -> { + emailCertification.value = EmailCertificationState.ERROR + } + Status.LOADING -> { /* DO NOTHING*/ } + null -> { /* DO NOTHING*/ } + } + } + + LaunchedEffect(isEmailExist) { + if (!isCheckingEmail) return@LaunchedEffect + when (isEmailExist) { + EmailExistenceStatus.EXISTS -> { + accountViewModel.requestCheckOAuthEmail(email.value) + } + EmailExistenceStatus.NOT_EXISTS -> { + emailCertification.value = EmailCertificationState.NOT_EXIST_EMAIL + isCheckingEmail = false + } + EmailExistenceStatus.LOADING -> { + emailCertification.value = EmailCertificationState.IN_PROGRESS_CHECK_EMAIL + } + + EmailExistenceStatus.IDLE -> { /* DO NOTHING*/ } + EmailExistenceStatus.ERROR -> { + emailCertification.value = EmailCertificationState.ERROR + isCheckingEmail = false + } + } + } + + LaunchedEffect(isOAuthEmail) { + if (!isCheckingEmail || isEmailExist != EmailExistenceStatus.EXISTS) return@LaunchedEffect + + when (isOAuthEmail) { + CheckOAuthEmailStatus.NORMAL_EMAIL -> { + emailCertification.value = EmailCertificationState.SUCCESS_CHECK_EMAIL + accountViewModel.requestCertificateEmailPasswordReset(email.value) + resetPasswordStep.value = ResetPasswordStep.EMAIL_VERIFICATION + isCheckingEmail = false + } + + CheckOAuthEmailStatus.OAUTH_ACCOUNT -> { + emailCertification.value = EmailCertificationState.OAUTH_EMAIL + isCheckingEmail = false + } + + CheckOAuthEmailStatus.ERROR -> { + emailCertification.value = EmailCertificationState.ERROR + isCheckingEmail = false + } + + CheckOAuthEmailStatus.LOADING -> { + emailCertification.value = EmailCertificationState.IN_PROGRESS_CHECK_EMAIL + } + } + } + + ResetPasswordScreen( + context = context, + coroutineScope = coroutineScope, + snackBarHostState = snackBarHostState, + onBackClick = onBackClick, + resetPasswordStep = resetPasswordStep.value, + setResetPasswordStep = { resetPasswordStep.value = it }, + hideKeyboard = { keyboardController?.hide() }, + isNextButtonEnabled = isNextButtonEnabled.value, + setNextButtonEnabled = { isNextButtonEnabled.value = it }, + isNextButtonClickable = isNextButtonClickable.value, + setNextButtonClickable = { isNextButtonClickable.value = it }, + email = email.value, + setEmail = { email.value = it }, + emailCertification = emailCertification.value, + setEmailCertification = { emailCertification.value = it }, + requestIsEmailExist = { accountViewModel.requestCheckEmail(it) }, + isEmailExist = isEmailExist, + requestIsOAuthEmail = { accountViewModel.requestCheckOAuthEmail(it) }, + isOAuthEmail = isOAuthEmail, + certificateEmailAuthCode = certificateEmailAuthCode + ?: EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString(), + certificationInputCode = certificationInputCode.value, + setCertificationInputCode = { certificationInputCode.value = it }, + isEmailCertificateError = isEmailCertificateError.value, + setIsEmailCertificateError = { isEmailCertificateError.value = it }, + requestEmailCertification = { accountViewModel.requestCertificateEmailPasswordReset(it) }, + password = password.value, + setPassword = { password.value = it }, + isPasswordFormatValid = isPasswordFormatValid.value, + setIsPasswordFormatValid = { isPasswordFormatValid.value = it }, + passwordConfirmation = passwordConfirmation.value, + setPasswordConfirmation = { passwordConfirmation.value = it }, + isPasswordMismatch = isPasswordMismatch.value, + setIsPasswordMismatch = { isPasswordMismatch.value = it }, + requestResetPassword = { + accountViewModel.requestChangePassword( + email.value, + password.value + ) + }, + isCheckingEmail = isCheckingEmail, + setIsCheckingEmail = { isCheckingEmail = it } + ) + + if (emailCertification.value == EmailCertificationState.OAUTH_EMAIL) { + ConfirmDialog( + title = stringResource(R.string.reset_password_email_certification_fail_oauth_account_dialog_title), + description = stringResource(R.string.reset_password_email_certification_fail_oauth_account_dialog_description), + onClickConfirmText = stringResource(R.string.confirm), + onClickConfirm = { navigateToSignIn() }, + onClickCancel = null + ) + } + + if (emailCertification.value == EmailCertificationState.ERROR) { + // TODO: ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜์— ๋Œ€ํ•œ Dialog ํ‘œ์‹œ + } + + Loading( + isVisible = resetPasswordStatus == Status.LOADING || resetPasswordStatus == Status.SUCCESS, + lottieFile = R.raw.dayo_loading, + lottieModifier = Modifier + .width(82.dp) + .height(85.dp), + message = stringResource(R.string.reset_password_alert_message_loading) + ) +} + +@Preview +@Composable +fun ResetPasswordScreen( + context: Context = LocalContext.current, + coroutineScope: CoroutineScope = rememberCoroutineScope(), + snackBarHostState: SnackbarHostState = remember { SnackbarHostState() }, + onBackClick: () -> Unit = {}, + resetPasswordStep: ResetPasswordStep = ResetPasswordStep.EMAIL_INPUT, + setResetPasswordStep: (ResetPasswordStep) -> Unit = {}, + hideKeyboard: () -> Unit = {}, + isNextButtonEnabled: Boolean = false, + setNextButtonEnabled: (Boolean) -> Unit = {}, + isNextButtonClickable: Boolean = false, + setNextButtonClickable: (Boolean) -> Unit = {}, + email: String = "", + setEmail: (String) -> Unit = {}, + isEmailExist: EmailExistenceStatus = EmailExistenceStatus.IDLE, + requestIsEmailExist: (String) -> Unit = {}, + isOAuthEmail: CheckOAuthEmailStatus = CheckOAuthEmailStatus.LOADING, + requestIsOAuthEmail: (String) -> Unit = {}, + emailCertification: EmailCertificationState = EmailCertificationState.BEFORE_CERTIFICATION, + setEmailCertification: (EmailCertificationState) -> Unit = {}, + certificateEmailAuthCode: String = EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString(), + requestEmailCertification: (String) -> Unit = {}, + certificationInputCode: String = "", + setCertificationInputCode: (String) -> Unit = {}, + isEmailCertificateError: Boolean? = null, + setIsEmailCertificateError: (Boolean) -> Unit = {}, + password: String = "", + setPassword: (String) -> Unit = {}, + isPasswordFormatValid: Boolean = false, + setIsPasswordFormatValid: (Boolean) -> Unit = {}, + passwordConfirmation: String = "", + setPasswordConfirmation: (String) -> Unit = {}, + isPasswordMismatch: Boolean = false, + setIsPasswordMismatch: (Boolean) -> Unit = {}, + requestResetPassword: () -> Unit = {}, + isCheckingEmail: Boolean = false, + setIsCheckingEmail: (Boolean) -> Unit = {}, +) { + Surface( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize() + ) { + + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize(), + verticalArrangement = Arrangement.Top + ) { + ResetPasswordActionbarLayout( + onBackClick = onBackClick, + resetPasswordStep = resetPasswordStep, + backStep = { + when (resetPasswordStep) { + ResetPasswordStep.EMAIL_INPUT -> { + setEmail("") + setEmailCertification(EmailCertificationState.BEFORE_CERTIFICATION) + setResetPasswordStep(ResetPasswordStep.EMAIL_INPUT) + } + + ResetPasswordStep.EMAIL_VERIFICATION -> { + setEmail("") + setEmailCertification(EmailCertificationState.BEFORE_CERTIFICATION) + setEmailCertification(EmailCertificationState.BEFORE_CERTIFICATION) + setCertificationInputCode("") + setIsEmailCertificateError(false) + setResetPasswordStep(ResetPasswordStep.EMAIL_INPUT) + } + + ResetPasswordStep.NEW_PASSWORD_INPUT -> { + setEmail("") + setEmailCertification(EmailCertificationState.BEFORE_CERTIFICATION) + setEmailCertification(EmailCertificationState.BEFORE_CERTIFICATION) + setCertificationInputCode("") + setIsEmailCertificateError(false) + setPassword("") + setIsPasswordFormatValid(true) + setResetPasswordStep(ResetPasswordStep.EMAIL_INPUT) + } + + ResetPasswordStep.NEW_PASSWORD_CONFIRM -> { + setPassword("") + setIsPasswordFormatValid(true) + setPasswordConfirmation("") + setIsPasswordMismatch(false) + setResetPasswordStep(ResetPasswordStep.NEW_PASSWORD_INPUT) + } + } + } + ) + + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .padding(horizontal = 20.dp, vertical = 0.dp) + .fillMaxWidth() + .wrapContentSize() + ) { + // Title ์˜์—ญ + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = when (resetPasswordStep) { + ResetPasswordStep.EMAIL_INPUT, ResetPasswordStep.EMAIL_VERIFICATION -> + stringResource(R.string.reset_password_email_title) + + ResetPasswordStep.NEW_PASSWORD_INPUT, ResetPasswordStep.NEW_PASSWORD_CONFIRM -> + stringResource(R.string.reset_password_new_password_title) + }, + style = DayoTheme.typography.h1.copy(color = Dark), + ) + + // SubTitle ์˜์—ญ + AnimatedVisibility( + visible = (resetPasswordStep.stepNum in 1..2), + ) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(4.dp) + ) + Text( + text = if (resetPasswordStep == ResetPasswordStep.EMAIL_INPUT) + stringResource(R.string.reset_password_email_sub_title) + else if (resetPasswordStep == ResetPasswordStep.EMAIL_VERIFICATION) + stringResource(R.string.reset_password_new_password_sub_title) + else "", + style = DayoTheme.typography.b6.copy(color = Gray2_767B83), + ) + } + + // Contents ์˜์—ญ + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(28.dp) + ) + when (resetPasswordStep) { + ResetPasswordStep.EMAIL_INPUT -> { + EmailInputLayout( + context = context, + isNextButtonEnabled = isNextButtonEnabled, + setNextButtonEnabled = setNextButtonEnabled, + isNextButtonClickable = isNextButtonClickable, + setIsNextButtonClickable = setNextButtonClickable, + email = email, + setEmail = setEmail, + emailCertification = emailCertification, + setEmailCertification = setEmailCertification, + isEmailExist = isEmailExist, + requestIsEmailExist = requestIsEmailExist, + isOAuthEmail = isOAuthEmail, + requestIsOAuthEmail = requestIsOAuthEmail, + requestEmailCertification = requestEmailCertification, + ) + } + + ResetPasswordStep.EMAIL_VERIFICATION -> { + EmailCertificationLayout( + resetPasswordStep = resetPasswordStep, + isNextButtonEnabled = isNextButtonEnabled, + setNextButtonEnabled = setNextButtonEnabled, + isNextButtonClickable = isNextButtonClickable, + setIsNextButtonClickable = setNextButtonClickable, + email = email, + emailCertification = emailCertification, + certificationCode = certificateEmailAuthCode, + certificationInputCode = certificationInputCode, + setCertificationInputCode = setCertificationInputCode, + isEmailCertificateError = isEmailCertificateError, + setIsEmailCertificateError = setIsEmailCertificateError, + requestEmailCertification = requestEmailCertification, + ) + } + + ResetPasswordStep.NEW_PASSWORD_INPUT, ResetPasswordStep.NEW_PASSWORD_CONFIRM -> { + NewPasswordLayout( + resetPasswordStep = resetPasswordStep, + isNextButtonEnabled = isNextButtonEnabled, + setNextButtonEnabled = setNextButtonEnabled, + isNextButtonClickable = isNextButtonClickable, + setIsNextButtonClickable = setNextButtonClickable, + password = password, + setPassword = setPassword, + isPasswordFormatValid = isPasswordFormatValid, + setIsPasswordFormatValid = setIsPasswordFormatValid, + passwordConfirmation = passwordConfirmation, + setPasswordConfirmation = setPasswordConfirmation, + isPasswordMismatch = isPasswordMismatch, + setIsPasswordMismatch = setIsPasswordMismatch, + ) + } + } + } + Spacer(modifier = Modifier.weight(1f)) + ResetPasswordBottomLayout( + resetPasswordStep = resetPasswordStep, + onNextClick = { + hideKeyboard() + setNextButtonEnabled(false) + + when (resetPasswordStep) { + ResetPasswordStep.EMAIL_INPUT -> { + setIsCheckingEmail(true) + requestIsEmailExist(email) + } + + ResetPasswordStep.EMAIL_VERIFICATION -> { + if (certificateEmailAuthCode == EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString() || + certificateEmailAuthCode == RESET_PASSWORD_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString() + ) { + // ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ์—†๋Š” ์ƒํƒœ + } else { + if (certificationInputCode == certificateEmailAuthCode) { + setResetPasswordStep(ResetPasswordStep.NEW_PASSWORD_INPUT) + } else { + setIsEmailCertificateError(true) + } + } + } + + ResetPasswordStep.NEW_PASSWORD_INPUT -> { + val passwordFormat = Regex("^[a-z0-9]{8,16}$") + if (passwordFormat.matches(password)) { + setResetPasswordStep(ResetPasswordStep.NEW_PASSWORD_CONFIRM) + } else { + setIsPasswordFormatValid(false) + } + } + + ResetPasswordStep.NEW_PASSWORD_CONFIRM -> { + if (password == passwordConfirmation) { + requestResetPassword() + } else { + setIsPasswordMismatch(true) + } + } + } + }, + isNextButtonEnabled = isNextButtonEnabled, + isNextButtonClickable = isNextButtonClickable, + ) + } + } +} + +@Preview +@Composable +fun ResetPasswordActionbarLayout( + onBackClick: () -> Unit = {}, + resetPasswordStep: ResetPasswordStep = ResetPasswordStep.EMAIL_INPUT, + backStep: () -> Unit = {} +) { + BackHandler { + if (resetPasswordStep == ResetPasswordStep.EMAIL_INPUT) { + onBackClick() + } else { + backStep() + } + } + + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = { + if (resetPasswordStep == ResetPasswordStep.EMAIL_INPUT) { + onBackClick() + } else { + backStep() + } + }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + ) +} + +@Preview +@Composable +fun ResetPasswordBottomLayout( + resetPasswordStep: ResetPasswordStep = ResetPasswordStep.EMAIL_INPUT, + onNextClick: () -> Unit = {}, + isNextButtonEnabled: Boolean = false, + isNextButtonClickable: Boolean = false, +) { + Column( + modifier = Modifier + .imePadding() + .fillMaxWidth() + .wrapContentHeight() + ) { + ResetPasswordNextButton( + resetPasswordStep = resetPasswordStep, + onClick = onNextClick, + isEnabled = isNextButtonEnabled, + isClickable = isNextButtonClickable, + ) + } +} + +@Preview +@Composable +fun ResetPasswordNextButton( + resetPasswordStep: ResetPasswordStep = ResetPasswordStep.EMAIL_INPUT, + onClick: () -> Unit = {}, + isEnabled: Boolean = false, + isClickable: Boolean = false, +) { + Column( + modifier = Modifier + .padding(horizontal = 18.dp, vertical = 20.dp) + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + FilledRoundedCornerButton( + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + label = when (resetPasswordStep) { + ResetPasswordStep.EMAIL_INPUT -> stringResource(R.string.reset_password_email_next_button) + ResetPasswordStep.EMAIL_VERIFICATION -> stringResource(R.string.reset_password_email_certification_next_button) + ResetPasswordStep.NEW_PASSWORD_INPUT -> stringResource(R.string.reset_password_new_password_next_button) + ResetPasswordStep.NEW_PASSWORD_CONFIRM -> stringResource(R.string.reset_password_new_password_confirmation_next_button) + else -> "" + }, + textStyle = DayoTheme.typography.b3.copy(color = White_FFFFFF), + onClick = { if (isClickable) onClick() }, + enabled = isEnabled, + ) + } +} + + +@Preview +@Composable +fun EmailInputLayout( + context: Context = LocalContext.current, + isNextButtonEnabled: Boolean = false, + setNextButtonEnabled: (Boolean) -> Unit = {}, + isNextButtonClickable: Boolean = false, + setIsNextButtonClickable: (Boolean) -> Unit = {}, + email: String = "", + setEmail: (String) -> Unit = {}, + emailCertification: EmailCertificationState = EmailCertificationState.BEFORE_CERTIFICATION, + setEmailCertification: (EmailCertificationState) -> Unit = {}, + isEmailExist: EmailExistenceStatus = EmailExistenceStatus.IDLE, + requestIsEmailExist: (String) -> Unit = {}, + isOAuthEmail: CheckOAuthEmailStatus = CheckOAuthEmailStatus.LOADING, + requestIsOAuthEmail: (String) -> Unit = {}, + requestEmailCertification: (String) -> Unit = {}, +) { + val lastErrorMessage = remember { mutableStateOf("") } + + LaunchedEffect(emailCertification) { + lastErrorMessage.value = when (emailCertification) { + EmailCertificationState.BEFORE_CERTIFICATION -> "" + EmailCertificationState.IN_PROGRESS_CHECK_EMAIL -> lastErrorMessage.value + EmailCertificationState.INVALID_FORMAT -> context.getString(R.string.reset_password_email_fail_invalid_format) + EmailCertificationState.NOT_EXIST_EMAIL -> context.getString(R.string.reset_password_email_fail_not_exist) + EmailCertificationState.OAUTH_EMAIL -> context.getString(R.string.reset_password_email_fail_oauth_account) + EmailCertificationState.SUCCESS_CHECK_EMAIL -> "" + else -> lastErrorMessage.value + } + } + + DayoTextField( + value = email, + onValueChange = { + setEmail(it) + val formatValid = android.util.Patterns.EMAIL_ADDRESS.matcher(it).matches() + setNextButtonEnabled(formatValid) + setIsNextButtonClickable(formatValid) + if (!formatValid) { + setEmailCertification(EmailCertificationState.INVALID_FORMAT) + } else { + lastErrorMessage.value = "" + // INVALID FORMAT ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋‹ค์Œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋  ๋–„ ๋‚จ์•„ ์žˆ์ง€ ์•Š๋„๋ก value Clear + } + }, + label = stringResource(R.string.email), + placeholder = stringResource(R.string.reset_password_email_placeholder), + trailingIconId = if (email.isNotBlank()) R.drawable.ic_trailing_check else null, + errorTrailingIconId = R.drawable.ic_trailing_error, + errorMessage = lastErrorMessage.value, + isError = if (email.isBlank()) null else !isNextButtonEnabled, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + ) +} + +@Preview +@Composable +private fun EmailCertificationLayout( + resetPasswordStep: ResetPasswordStep = ResetPasswordStep.EMAIL_VERIFICATION, + isNextButtonEnabled: Boolean = false, + setNextButtonEnabled: (Boolean) -> Unit = {}, + isNextButtonClickable: Boolean = false, + setIsNextButtonClickable: (Boolean) -> Unit = {}, + email: String = "", + emailCertification: EmailCertificationState = EmailCertificationState.BEFORE_CERTIFICATION, + certificationCode: String = "-1", + certificationInputCode: String = "", + setCertificationInputCode: (String) -> Unit = {}, + isEmailCertificateError: Boolean? = null, + setIsEmailCertificateError: (Boolean) -> Unit = {}, + requestEmailCertification: (String) -> Unit = {}, +) { + val certificateEmailAuthCodeFormat = Regex("^\\d{6}$") + + var tryCount by remember { mutableStateOf(1) } + val isPaused = remember { mutableStateOf(false) } + val seconds = remember { + mutableStateOf(RESET_PASSWORD_EMAIL_CERTIFICATE_AUTH_TIME_OUT) + } + val timerErrorMessageRedId = + remember { mutableStateOf((R.string.reset_password_email_certification_fail_wrong)) } + + setNextButtonEnabled( + certificateEmailAuthCodeFormat.matches(certificationInputCode) + ) + setIsNextButtonClickable( + certificateEmailAuthCodeFormat.matches(certificationInputCode) + ) + + key(tryCount) { + DayoTimerTextField( + value = certificationInputCode, + onValueChange = { textValue -> + setCertificationInputCode(textValue) + }, + seconds = seconds.value, + isPaused = isPaused.value, + label = if (certificationInputCode.isBlank()) " " + else stringResource(R.string.reset_password_email_certification_input_title), + placeholder = if (certificationInputCode.isBlank()) + stringResource(R.string.reset_password_email_certification_input_placeholder) else "", + isError = isEmailCertificateError ?: false, + errorMessage = stringResource(timerErrorMessageRedId.value), + timeOutErrorMessage = stringResource(R.string.reset_password_email_certification_fail_time_out), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + ) + } + + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + ) + if (certificationCode != EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString()) { + // ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋กœ ์ •์ƒ์ ์œผ๋กœ ๋น„๊ตํ•  ์ธ์ฆ์ฝ”๋“œ๋ฅผ ๋ณด๋‚ด์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ + if (certificationCode == RESET_PASSWORD_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString()) { + timerErrorMessageRedId.value = + R.string.reset_password_email_certification_fail_network + setIsEmailCertificateError(true) + } + } + + AnimatedVisibility( + visible = certificationCode != RESET_PASSWORD_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString() && + certificationCode != EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString(), + enter = fadeIn(), + exit = shrinkVertically() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + ) { + DayoTextButton( + modifier = Modifier + .align(alignment = Alignment.TopEnd), + text = stringResource(R.string.reset_password_email_certification_resend_button), + onClick = { + tryCount++ + requestEmailCertification(email) + }, + underline = true, + textStyle = DayoTheme.typography.b6.copy(Gray2_767B83), + ) + } + + } +} + + +@Composable +@Preview +private fun NewPasswordLayout( + resetPasswordStep: ResetPasswordStep = ResetPasswordStep.NEW_PASSWORD_INPUT, + isNextButtonEnabled: Boolean = false, + setNextButtonEnabled: (Boolean) -> Unit = {}, + isNextButtonClickable: Boolean = false, + setIsNextButtonClickable: (Boolean) -> Unit = {}, + password: String = "", + setPassword: (String) -> Unit = {}, + isPasswordFormatValid: Boolean = false, + setIsPasswordFormatValid: (Boolean) -> Unit = {}, + passwordConfirmation: String = "", + setPasswordConfirmation: (String) -> Unit = {}, + isPasswordMismatch: Boolean = false, + setIsPasswordMismatch: (Boolean) -> Unit = {}, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + setNextButtonEnabled( + if (resetPasswordStep == ResetPasswordStep.NEW_PASSWORD_INPUT) password.isNotBlank() + else passwordConfirmation.isNotBlank() + ) + setIsNextButtonClickable( + if (resetPasswordStep == ResetPasswordStep.NEW_PASSWORD_INPUT) password.isNotBlank() + else passwordConfirmation.isNotBlank() + ) + + AnimatedVisibility( + visible = resetPasswordStep == ResetPasswordStep.NEW_PASSWORD_CONFIRM, + enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut() + ) { + DayoPasswordTextField( + value = passwordConfirmation, + onValueChange = { + setIsPasswordMismatch(false) // ์ž…๋ ฅ ์‹œ, ์—๋Ÿฌ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ + setPasswordConfirmation(it) + }, + label = if (passwordConfirmation.isBlank()) " " + else stringResource(R.string.reset_password_new_password_confirm_input_title), + placeholder = if (passwordConfirmation.isBlank()) stringResource(R.string.reset_password_new_password_confirm_input_placeholder) else "", + isError = isPasswordMismatch, + errorMessage = stringResource(R.string.reset_password_new_password_confirm_fail_not_match), + ) + } + + if (resetPasswordStep == ResetPasswordStep.NEW_PASSWORD_CONFIRM) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(24.dp) + ) + } + + AnimatedVisibility( + visible = resetPasswordStep == ResetPasswordStep.NEW_PASSWORD_INPUT || resetPasswordStep == ResetPasswordStep.NEW_PASSWORD_CONFIRM, + enter = slideInVertically(), + ) { + DayoPasswordTextField( + value = password, + onValueChange = { + setIsPasswordFormatValid(true) // ์ž…๋ ฅ ์‹œ, ์—๋Ÿฌ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ + setPassword(it) + }, + label = if (password.isBlank()) " " + else stringResource(R.string.reset_password_new_password_input_title), + placeholder = if (password.isBlank()) + stringResource(R.string.reset_password_new_password_input_placeholder) else "", + isError = if (resetPasswordStep == ResetPasswordStep.NEW_PASSWORD_INPUT) !isPasswordFormatValid else false, + errorMessage = stringResource(R.string.reset_password_new_password_message_format_fail), + isEnabled = resetPasswordStep == ResetPasswordStep.NEW_PASSWORD_INPUT, + ) + } + } +} + +enum class ResetPasswordStep(val stepNum: Int) { + EMAIL_INPUT(1), // ์ด๋ฉ”์ผ ์ฃผ์†Œ ์ž…๋ ฅ + EMAIL_VERIFICATION(2), // ์ธ์ฆ๋ฒˆํ˜ธ ์ž…๋ ฅ + NEW_PASSWORD_INPUT(3), // ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ + NEW_PASSWORD_CONFIRM(4), // ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์ž…๋ ฅ +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt new file mode 100644 index 00000000..a66b5bbc --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailCertificationView.kt @@ -0,0 +1,128 @@ +package daily.dayo.presentation.screen.account + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.R +import daily.dayo.presentation.screen.account.model.EmailCertificationState +import daily.dayo.presentation.screen.account.model.SignUpStep +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.view.DayoTextButton +import daily.dayo.presentation.view.DayoTimerTextField +import daily.dayo.presentation.viewmodel.AccountViewModel + +@Preview +@Composable +fun SetEmailCertificationView( + timeOutSeconds: Int = 60, + signUpStep: SignUpStep = SignUpStep.EMAIL_VERIFICATION, + isNextButtonEnabled: Boolean = false, + setNextButtonEnabled: (Boolean) -> Unit = {}, + isNextButtonClickable: Boolean = false, + setIsNextButtonClickable: (Boolean) -> Unit = {}, + email: String = "", + emailCertification: EmailCertificationState = EmailCertificationState.BEFORE_CERTIFICATION, + certificationCode: String = "-1", + certificationInputCode: String = "", + setCertificationInputCode: (String) -> Unit = {}, + isEmailCertificateError: Boolean? = null, + setIsEmailCertificateError: (Boolean) -> Unit = {}, + requestEmailCertification: (String) -> Unit = {}, +) { + val certificateEmailAuthCodeFormat = Regex("^\\d{6}$") + + var tryCount by remember { mutableStateOf(1) } + val isPaused = remember { mutableStateOf(false) } + val seconds = remember { mutableStateOf(timeOutSeconds) } + val timerErrorMessageRedId = + remember { mutableStateOf((R.string.sign_up_email_set_address_certification_fail_wrong)) } + val isTimeOut = remember { mutableStateOf(false) } + + setNextButtonEnabled( + certificateEmailAuthCodeFormat.matches(certificationInputCode) && !isTimeOut.value + ) + setIsNextButtonClickable( + certificateEmailAuthCodeFormat.matches(certificationInputCode) && !isTimeOut.value + ) + + key(tryCount) { + DayoTimerTextField( + value = certificationInputCode, + onValueChange = { textValue -> + setCertificationInputCode(textValue) + }, + seconds = seconds.value, + isPaused = isPaused.value, + label = if (certificationInputCode.isBlank()) " " + else stringResource(R.string.sign_up_email_set_address_certification_input_title), + placeholder = if (certificationInputCode.isBlank()) + stringResource(R.string.sign_up_email_set_address_certification_input_placeholder) else "", + isError = isEmailCertificateError ?: false, + errorMessage = stringResource(timerErrorMessageRedId.value), + timeOutErrorMessage = stringResource(R.string.sign_up_email_set_address_certification_fail_time_out), + onTimeOut = { + isTimeOut.value = true + setNextButtonEnabled(false) + setIsNextButtonClickable(false) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + ) + } + + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + ) + if (certificationCode != AccountViewModel.EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString()) { + // ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋กœ ์ •์ƒ์ ์œผ๋กœ ๋น„๊ตํ•  ์ธ์ฆ์ฝ”๋“œ๋ฅผ ๋ณด๋‚ด์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ + if (certificationCode == AccountViewModel.SIGN_UP_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString()) { + timerErrorMessageRedId.value = + R.string.sign_up_email_set_address_certification_fail_network + setIsEmailCertificateError(true) + } + } + + AnimatedVisibility( + visible = certificationCode != AccountViewModel.SIGN_UP_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString() && + certificationCode != AccountViewModel.EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString(), + enter = fadeIn(), + exit = shrinkVertically() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + ) { + DayoTextButton( + modifier = Modifier + .align(alignment = Alignment.TopEnd), + text = stringResource(R.string.sign_up_email_set_address_resend_button), + onClick = { + tryCount++ + isTimeOut.value = false + requestEmailCertification(email) + }, + underline = true, + textStyle = DayoTheme.typography.b6.copy(Gray2_767B83), + ) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailView.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailView.kt new file mode 100644 index 00000000..1c22c471 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetEmailView.kt @@ -0,0 +1,65 @@ +package daily.dayo.presentation.screen.account + +import android.content.Context +import android.util.Patterns +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import daily.dayo.presentation.R +import daily.dayo.presentation.screen.account.model.EmailCertificationState +import daily.dayo.presentation.view.DayoTextField + +@Composable +fun SetEmailView( + context: Context = LocalContext.current, + isNextButtonEnabled: Boolean = false, + setNextButtonEnabled: (Boolean) -> Unit = {}, + isNextButtonClickable: Boolean = false, + setIsNextButtonClickable: (Boolean) -> Unit = {}, + email: String = "", + setEmail: (String) -> Unit = {}, + emailCertification: EmailCertificationState = EmailCertificationState.BEFORE_CERTIFICATION, + requestEmailCertification: (String) -> Unit = {}, +) { + val lastErrorMessage = remember { mutableStateOf("") } + + // text๋ฅผ ์ž…๋ ฅํ• ๋•Œ๋งˆ๋‹ค button enable ์ƒํƒœ๊ฐ€ ๋ฐ˜๊ฒฝ๋˜๋Š” ๊ฒƒ์€ ๋ฐฉ์ง€ํ•˜๋˜, ๊ฐ€๋Šฅํ•œ ์ด๋ฉ”์ผ ์ƒํƒœ -> ๋ถˆ๊ฐ€๋Šฅ์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒฝ์šฐ์— ๋‹ค์Œ์œผ๋กœ ๋„˜์–ด๊ฐ€์ง€ ์•Š๋„๋ก clickable์€ ์กฐ์ • + setNextButtonEnabled( + emailCertification == EmailCertificationState.SUCCESS_CHECK_EMAIL + || (isNextButtonEnabled && emailCertification == EmailCertificationState.IN_PROGRESS_CHECK_EMAIL) + ) + setIsNextButtonClickable(emailCertification == EmailCertificationState.SUCCESS_CHECK_EMAIL) + + LaunchedEffect(emailCertification) { + lastErrorMessage.value = when (emailCertification) { + EmailCertificationState.BEFORE_CERTIFICATION -> "" + EmailCertificationState.IN_PROGRESS_CHECK_EMAIL -> lastErrorMessage.value + EmailCertificationState.INVALID_FORMAT -> context.getString(R.string.sign_up_email_set_address_fail_invalid_format) + EmailCertificationState.DUPLICATE_EMAIL -> context.getString(R.string.sign_up_email_set_address_fail_duplicate) + EmailCertificationState.SUCCESS_CHECK_EMAIL -> "" + else -> lastErrorMessage.value + } + } + + DayoTextField( + value = email, + onValueChange = { + setEmail(it) + if (Patterns.EMAIL_ADDRESS.matcher(it).matches()) { + requestEmailCertification(it) + } + }, + label = stringResource(R.string.email), + placeholder = stringResource(R.string.sign_up_email_set_address_placeholder), + trailingIconId = if (email.isNotBlank()) R.drawable.ic_trailing_check else null, + errorTrailingIconId = R.drawable.ic_trailing_error, + errorMessage = lastErrorMessage.value, + isError = if (email.isBlank()) null else !isNextButtonEnabled, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetPasswordView.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetPasswordView.kt new file mode 100644 index 00000000..e862f3d0 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetPasswordView.kt @@ -0,0 +1,109 @@ +package daily.dayo.presentation.screen.account + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.R +import daily.dayo.presentation.common.ReplaceUnicode +import daily.dayo.presentation.screen.account.model.SignUpStep +import daily.dayo.presentation.view.DayoPasswordTextField + +@Composable +@Preview +fun SetPasswordView( + passwordInputViewCondition: Boolean = true, + passwordConfirmationViewCondition: Boolean = false, + isNextButtonEnabled: Boolean = false, + setNextButtonEnabled: (Boolean) -> Unit = {}, + isNextButtonClickable: Boolean = false, + setIsNextButtonClickable: (Boolean) -> Unit = {}, + password: String = "", + setPassword: (String) -> Unit = {}, + isPasswordFormatValid: Boolean = false, + setIsPasswordFormatValid: (Boolean) -> Unit = {}, + passwordConfirmation: String = "", + setPasswordConfirmation: (String) -> Unit = {}, + isPasswordMismatch: Boolean = false, + setIsPasswordMismatch: (Boolean) -> Unit = {}, + passwordFailMessage: String = "", + passwordConfirmationFailMessage: String = "", +) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + setNextButtonEnabled( + if (passwordInputViewCondition) password.isNotBlank() + else passwordConfirmation.isNotBlank() + ) + setIsNextButtonClickable( + if (passwordInputViewCondition) password.isNotBlank() + else passwordConfirmation.isNotBlank() + ) + + AnimatedVisibility( + visible = passwordConfirmationViewCondition, + enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut() + ) { + DayoPasswordTextField( + value = passwordConfirmation, + onValueChange = { + val trimmedText = ReplaceUnicode.trimBlankText(it) + setIsPasswordMismatch(false) // ์ž…๋ ฅ ์‹œ, ์—๋Ÿฌ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ + setPasswordConfirmation(it) + }, + label = if (passwordConfirmation.isBlank()) " " + else stringResource(R.string.sign_up_email_set_password_confirm_input_title), + placeholder = if (passwordConfirmation.isBlank()) stringResource(R.string.sign_up_email_set_password_confirm_input_placeholder) else "", + isError = isPasswordMismatch, + errorMessage = + if (passwordConfirmationFailMessage.isBlank()) stringResource(R.string.sign_up_email_set_password_confirm_fail_not_match) else passwordConfirmationFailMessage, + ) + } + + if (passwordConfirmationViewCondition) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(24.dp) + ) + } + + AnimatedVisibility( + visible = passwordInputViewCondition || passwordConfirmationViewCondition, + enter = slideInVertically(), + ) { + DayoPasswordTextField( + value = password, + onValueChange = { + setIsPasswordFormatValid(true) // ์ž…๋ ฅ ์‹œ, ์—๋Ÿฌ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ + setPassword(it) + }, + label = if (password.isBlank()) " " + else stringResource(R.string.sign_up_email_set_password_input_title), + placeholder = if (password.isBlank()) + stringResource(R.string.sign_up_email_set_password_input_placeholder) else "", + isError = if (passwordInputViewCondition) !isPasswordFormatValid else false, + errorMessage = + if (passwordFailMessage.isBlank()) stringResource(R.string.sign_up_email_set_password_message_format_fail) else passwordFailMessage, + isEnabled = passwordInputViewCondition, + ) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SetProfileSetupView.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetProfileSetupView.kt new file mode 100644 index 00000000..c4e92d34 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SetProfileSetupView.kt @@ -0,0 +1,140 @@ +package daily.dayo.presentation.screen.account + +import android.content.Context +import android.graphics.Bitmap +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.R +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.screen.account.model.NicknameCertificationState +import daily.dayo.presentation.view.BadgeRoundImageView +import daily.dayo.presentation.view.DayoTextField +import daily.dayo.presentation.view.dialog.getBottomSheetDialogState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +fun SetProfileSetupView( + context: Context = LocalContext.current, + bottomSheetState: BottomSheetScaffoldState = getBottomSheetDialogState(), + coroutineScope: CoroutineScope = rememberCoroutineScope(), + isNextButtonEnabled: MutableState = remember { mutableStateOf(false) }, + isNextButtonClickable: MutableState = remember { mutableStateOf(false) }, + nicknameState: MutableState = remember { mutableStateOf("") }, + nicknameCertificationState: MutableState = remember { + mutableStateOf( + NicknameCertificationState.BEFORE_CERTIFICATION + ) + }, + requestIsNicknameDuplicate: (String) -> Unit = {}, + profileImg: Bitmap? = null, +) { + val placeholderResId = remember { R.drawable.ic_profile_default_user_profile } + val interactionSource = remember { MutableInteractionSource() } + val profileImageClickModifier = remember { + Modifier + .size(110.dp) + .aspectRatio(1f) + .clip(RoundedCornerShape(percent = 50)) + .clickableSingle( + interactionSource = interactionSource, + indication = null, + onClick = { + coroutineScope.launch { bottomSheetState.bottomSheetState.expand() } + } + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + BadgeRoundImageView( + modifier = Modifier.align(Alignment.CenterHorizontally), + context = context, + imageUrl = profileImg ?: "", + imageDescription = "Set Profile image", + placeholderResId = placeholderResId, + roundSize = 55.dp, + contentModifier = profileImageClickModifier + ) + Spacer(modifier = Modifier.height(36.dp)) + SetNicknameLayout( + context = context, + isNextButtonEnabled = isNextButtonEnabled.value, + setNextButtonEnabled = { isNextButtonEnabled.value = it }, + setIsNextButtonClickable = { isNextButtonClickable.value = it }, + nickname = nicknameState.value, + setNickname = { nicknameState.value = it }, + nicknameCertification = nicknameCertificationState.value, + setNicknameCertification = { nicknameCertificationState.value = it }, + requestIsNicknameDuplicate = { requestIsNicknameDuplicate(it) }, + ) + } +} + +@Preview +@Composable +fun SetNicknameLayout( + context: Context = LocalContext.current, + isNextButtonEnabled: Boolean = false, + setNextButtonEnabled: (Boolean) -> Unit = {}, + setIsNextButtonClickable: (Boolean) -> Unit = {}, + nickname: String = "", + setNickname: (String) -> Unit = {}, + nicknameCertification: NicknameCertificationState = NicknameCertificationState.BEFORE_CERTIFICATION, + setNicknameCertification: (NicknameCertificationState) -> Unit = {}, + requestIsNicknameDuplicate: (String) -> Unit = {}, +) { + val nicknamePermitFormatRegex = Regex(NICKNAME_PERMIT_FORMAT) + setNextButtonEnabled(nicknameCertification == NicknameCertificationState.SUCCESS) + setIsNextButtonClickable(nicknameCertification == NicknameCertificationState.SUCCESS) + + DayoTextField( + value = nickname, + onValueChange = { + setNickname(it) + if (nicknamePermitFormatRegex.matches(it)) { + requestIsNicknameDuplicate(it) + } + }, + label = if (nickname.isBlank()) " " + else stringResource(R.string.sign_up_email_set_profile_nickname_input_title), + placeholder = if (nickname.isBlank()) + stringResource(R.string.sign_up_email_set_profile_nickname_input_placeholder) else "", + isError = if (nickname.isBlank()) null else !isNextButtonEnabled, + trailingIconId = if (nickname.isNotBlank()) R.drawable.ic_trailing_delete else null, + errorTrailingIconId = R.drawable.ic_trailing_delete, + onTrailingIconClick = { setNickname("") }, + errorMessage = when (nicknameCertification) { + NicknameCertificationState.DUPLICATE_NICKNAME -> stringResource(R.string.sign_up_email_set_profile_nickname_message_duplicate_fail) + NicknameCertificationState.INVALID_FORMAT -> stringResource(R.string.sign_up_email_set_profile_nickname_message_format_fail) + else -> "" + }, + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInEmailScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInEmailScreen.kt new file mode 100644 index 00000000..e2039c6d --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInEmailScreen.kt @@ -0,0 +1,343 @@ +package daily.dayo.presentation.screen.account + +import android.app.Activity +import android.content.Intent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel +import daily.dayo.presentation.R +import daily.dayo.presentation.activity.MainActivity +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.DayoPasswordTextField +import daily.dayo.presentation.view.DayoTextField +import daily.dayo.presentation.view.FilledRoundedCornerButton +import daily.dayo.presentation.view.Loading +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.viewmodel.AccountViewModel +import kotlinx.coroutines.CoroutineScope + +@Composable +internal fun SignInEmailRoute( + coroutineScope: CoroutineScope = rememberCoroutineScope(), + snackBarHostState: SnackbarHostState, + onBackClick: () -> Unit = {}, + navigateToFindPassword: () -> Unit = {}, + navigateToSignUpEmail: () -> Unit = {}, + accountViewModel: AccountViewModel = hiltViewModel() +) { + val context = LocalContext.current + var isInitialized by remember { mutableStateOf(false) } + val signInSuccess by accountViewModel.signInSuccess.collectAsState() + + LaunchedEffect(signInSuccess) { + if (!isInitialized) { + accountViewModel.initializeSignInSuccess() + isInitialized = true + } + + when (signInSuccess) { + Status.SUCCESS -> { + val intent = Intent(context, MainActivity::class.java) + intent.flags = + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + context.startActivity(intent) + (context as Activity).finish() + } + + Status.ERROR -> { + snackBarHostState.showSnackbar( + ContextCompat.getString(context, R.string.sign_in_email_fail_message) + ) + } + + Status.LOADING -> {} + null -> { + + } + } + } + + SignInEmailScreen( + onBackClick = onBackClick, + onForgetPasswordClick = navigateToFindPassword, + onSignUpClick = navigateToSignUpEmail, + onSignInClick = { email, password -> + accountViewModel.requestSignInEmail( + email = email, + password = password + ) + }, + accountViewModel = accountViewModel + ) + + Loading( + isVisible = (signInSuccess == Status.LOADING || signInSuccess == Status.SUCCESS), + lottieFile = R.raw.dayo_loading, + lottieModifier = Modifier + .width(82.dp) + .height(85.dp) + ) +} + +@Composable +@Preview +fun SignInEmailScreen( + onBackClick: () -> Unit = {}, + onForgetPasswordClick: () -> Unit = {}, + onSignUpClick: () -> Unit = {}, + onSignInClick: (email: String, password: String) -> Unit = { _, _ -> }, + accountViewModel: AccountViewModel = hiltViewModel() +) { + val emailState = remember { mutableStateOf("") } + val passwordState = remember { mutableStateOf("") } + val isSignInButtonEnabled = remember(emailState.value, passwordState.value) { + emailState.value.isNotBlank() && passwordState.value.isNotBlank() + } + + Surface( + modifier = Modifier + .background(White_FFFFFF) + .fillMaxSize() + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Top + ) { + SignInEmailActionbarLayout(onBackClick = onBackClick) + Column( + modifier = Modifier + .background(White_FFFFFF) + .padding(horizontal = 20.dp, vertical = 0.dp) + .fillMaxWidth() + .wrapContentSize(), + verticalArrangement = Arrangement.Top + ) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + ) + SignInEmailTitle() + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + ) + SignInEmailInputLayout( + emailValue = emailState.value, + onEmailChange = { emailState.value = it }, + passwordValue = passwordState.value, + onPasswordChange = { passwordState.value = it }, + onForgetPasswordClick = onForgetPasswordClick, + onSignInClick = { onSignInClick(emailState.value, passwordState.value) } + ) + } + Spacer(modifier = Modifier.weight(1f)) + SignInEmailBottomLayout( + onSignUpClick = onSignUpClick, + onSignInClick = { onSignInClick(emailState.value, passwordState.value) }, + isSignInButtonEnabled = isSignInButtonEnabled + ) + } + } +} + +@Composable +@Preview +fun SignInEmailActionbarLayout(onBackClick: () -> Unit = {}) { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = { + onBackClick() + }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + } + ) +} + +@Composable +@Preview +fun SignInEmailTitle() { + Text( + text = stringResource(R.string.sign_in_email_title), + style = DayoTheme.typography.h1.copy(color = Dark), + modifier = Modifier.fillMaxWidth() + ) +} + +@Composable +@Preview +fun SignInEmailInputLayout( + emailValue: String = "", + onEmailChange: (String) -> Unit = {}, + passwordValue: String = "", + onPasswordChange: (String) -> Unit = {}, + onForgetPasswordClick: () -> Unit = {}, + onSignInClick: () -> Unit = {} +) { + val focusRequesterEmail = FocusRequester() + val focusRequesterPassword = FocusRequester() + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + DayoTextField( + modifier = Modifier.focusRequester(focusRequesterEmail), + label = stringResource(R.string.sign_in_email_input_email_title), + placeholder = stringResource(R.string.sign_in_email_input_email_title), + value = emailValue, + onValueChange = onEmailChange, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email).copy( + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions(onNext = { focusRequesterPassword.requestFocus() }), + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + ) + DayoPasswordTextField( + modifier = Modifier.focusRequester(focusRequesterPassword), + label = stringResource(R.string.sign_in_email_input_password_title), + placeholder = stringResource(R.string.sign_in_email_input_password_placeholder), + value = passwordValue, + onValueChange = onPasswordChange, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password).copy( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions(onDone = { onSignInClick() }), + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(12.dp) + ) + Text( + text = stringResource(R.string.sign_in_email_forget_password), + style = DayoTheme.typography.caption4.copy(color = Gray1_50545B), + modifier = Modifier + .padding(horizontal = 2.dp, vertical = 8.dp) + .wrapContentSize() + .align(Alignment.End) + .clickableSingle { onForgetPasswordClick() }, + textAlign = TextAlign.End, + ) + } +} + +@Composable +@Preview +fun SignInEmailBottomLayout( + onSignUpClick: () -> Unit = {}, + onSignInClick: () -> Unit = {}, + isSignInButtonEnabled: Boolean = false +) { + Column( + modifier = Modifier + .imePadding() + .fillMaxWidth() + .wrapContentHeight() + ) { + SignInEmailSignUpLayout(onSignUpClick = onSignUpClick) + SignInEmailButton( + onSignInClick = onSignInClick, + isSignInButtonEnabled = isSignInButtonEnabled + ) + } +} + +@Composable +@Preview +fun SignInEmailSignUpLayout( + onSignUpClick: () -> Unit = {} +) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + Text( + text = stringResource(R.string.sign_up_email), + style = DayoTheme.typography.b5.copy(color = Gray3_9FA5AE), + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterHorizontally) + .clickableSingle { onSignUpClick() }, + textAlign = TextAlign.Center + ) + } +} + +@Composable +@Preview +fun SignInEmailButton( + onSignInClick: () -> Unit = {}, + isSignInButtonEnabled: Boolean = false +) { + Column( + modifier = Modifier + .padding(horizontal = 18.dp, vertical = 20.dp) + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + FilledRoundedCornerButton( + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + label = stringResource(R.string.sign_in), + textStyle = DayoTheme.typography.b3.copy(color = White_FFFFFF), + onClick = { onSignInClick() }, + enabled = isSignInButtonEnabled + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInNavigation.kt new file mode 100644 index 00000000..500a08d4 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInNavigation.kt @@ -0,0 +1,141 @@ +package daily.dayo.presentation.screen.account + +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import daily.dayo.presentation.screen.account.model.SignUpStep +import daily.dayo.presentation.screen.rules.RuleRoute +import daily.dayo.presentation.screen.rules.RuleType +import kotlinx.coroutines.CoroutineScope + +fun NavController.navigateSignIn() { + this.navigate(SignInRoute.route) { + popUpTo(SignInRoute.route) { inclusive = true } + } +} + +fun NavController.navigateSignInEmail() { + this.navigate(SignInRoute.signInEmail) +} + +fun NavController.navigateResetPassword() { + this.navigate(SignInRoute.resetPassword) +} + +fun NavController.navigateSignUpEmail() { + this.navigate(SignInRoute.signUpEmail) +} + +fun NavController.navigateProfileSetting() { + this.navigate(SignInRoute.profileSetting) +} + +@OptIn(ExperimentalMaterial3Api::class) +fun NavGraphBuilder.signInNavGraph( + coroutineScope: CoroutineScope, + snackBarHostState: SnackbarHostState, + navController: NavController, + onBackClick: () -> Unit, + navigateToSignIn: () -> Unit, + navigateToSignInEmail: () -> Unit, + navigateToResetPassword: () -> Unit, + navigateToSignUpEmail: () -> Unit, + navigateToRules: (RuleType) -> Unit, + navigateToProfileSetting: () -> Unit, + bottomSheetState: BottomSheetScaffoldState, + bottomSheetContent: (@Composable () -> Unit) -> Unit, +) { + composable(route = SignInRoute.route) { + val parentStackEntry = remember(it) { + navController.getBackStackEntry(SignInRoute.route) + } + SignInRoute( + accountViewModel = hiltViewModel(parentStackEntry), + navigateToSignInEmail = navigateToSignInEmail, + navigateToRules = navigateToRules, + navigateToProfileSetting = navigateToProfileSetting, + ) + } + composable(route = SignInRoute.signInEmail) { + val parentStackEntry = remember(it) { + navController.getBackStackEntry(SignInRoute.route) + } + SignInEmailRoute( + coroutineScope = coroutineScope, + snackBarHostState = snackBarHostState, + onBackClick = onBackClick, + navigateToFindPassword = navigateToResetPassword, + navigateToSignUpEmail = navigateToSignUpEmail, + accountViewModel = hiltViewModel(parentStackEntry), + ) + } + + composable(route = SignInRoute.resetPassword) { + val parentStackEntry = remember(it) { + navController.getBackStackEntry(SignInRoute.route) + } + ResetPasswordRoute( + coroutineScope = coroutineScope, + snackBarHostState = snackBarHostState, + onBackClick = onBackClick, + navigateToSignIn = navigateToSignIn, + accountViewModel = hiltViewModel(parentStackEntry), + ) + } + + composable(route = SignInRoute.signUpEmail) { + val parentStackEntry = remember(it) { + navController.getBackStackEntry(SignInRoute.route) + } + SignUpEmailRoute( + coroutineScope = coroutineScope, + snackBarHostState = snackBarHostState, + onBackClick = onBackClick, + accountViewModel = hiltViewModel(parentStackEntry), + profileSettingViewModel = hiltViewModel(parentStackEntry), + ) + } + + composable(route = SignInRoute.profileSetting) { + val parentStackEntry = remember(it) { + navController.getBackStackEntry(SignInRoute.route) + } + SignUpEmailRoute( + coroutineScope = coroutineScope, + snackBarHostState = snackBarHostState, + onBackClick = onBackClick, + accountViewModel = hiltViewModel(parentStackEntry), + profileSettingViewModel = hiltViewModel(parentStackEntry), + startSignUpStep = SignUpStep.PROFILE_SETUP + ) + } + + composable(route = RuleRoute.privacyPolicy) { + RuleRoute( + onBackClick = onBackClick, + ruleType = RuleType.PRIVACY_POLICY + ) + } + + composable(route = RuleRoute.termsAndConditions) { + RuleRoute( + onBackClick = onBackClick, + ruleType = RuleType.TERMS_AND_CONDITIONS + ) + } + +} + +object SignInRoute { + const val route = "signIn" + const val signInEmail = "$route/email" + const val resetPassword = "$route/resetPassword" + const val signUpEmail = "$route/signUpEmail" + const val profileSetting = "$route/profileSetting" +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInScreen.kt new file mode 100644 index 00000000..67ce53a8 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignInScreen.kt @@ -0,0 +1,421 @@ +package daily.dayo.presentation.screen.account + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition +import com.kakao.sdk.auth.model.OAuthToken +import com.kakao.sdk.common.KakaoSdk +import com.kakao.sdk.common.model.AuthError +import com.kakao.sdk.common.model.AuthErrorCause +import com.kakao.sdk.common.model.ClientError +import com.kakao.sdk.common.model.ClientErrorCause +import com.kakao.sdk.user.UserApiClient +import daily.dayo.domain.model.User +import daily.dayo.presentation.R +import daily.dayo.presentation.activity.LoginActivity +import daily.dayo.presentation.activity.MainActivity +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.screen.rules.RuleType +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.DayoTextButton +import daily.dayo.presentation.view.FilledRoundedCornerButton +import daily.dayo.presentation.view.Loading +import daily.dayo.presentation.viewmodel.AccountViewModel + +@Composable +internal fun SignInRoute( + snackBarHostState: SnackbarHostState = SnackbarHostState(), + accountViewModel: AccountViewModel = hiltViewModel(), + navigateToSignInEmail: () -> Unit = {}, + navigateToRules: (RuleType) -> Unit = {}, + navigateToProfileSetting: () -> Unit = {}, +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val signInSuccess by accountViewModel.signInSuccess.collectAsState() + ObserveSignInStatus( + context = context, + snackBarHostState = snackBarHostState, + signInSuccess = signInSuccess, + getCurrentUserInfo = { accountViewModel.getCurrentUserInfo() }, + navigateToProfileSetting = { navigateToProfileSetting() }, + ) + + Box(modifier = Modifier.fillMaxSize()) { + SignInScreen( + context = context, + requestSignInKakao = { accessToken -> + accountViewModel.requestSignInKakao(accessToken) + }, + navigateToSignInEmail = navigateToSignInEmail, + navigateToTermsAndPolicy = { type -> navigateToRules(type) } + ) + + Loading( + isVisible = (signInSuccess == Status.LOADING || signInSuccess == Status.SUCCESS), + lottieFile = R.raw.dayo_loading, + lottieModifier = Modifier.width(82.dp).height(85.dp) + ) + } +} + +@Composable +private fun SignInScreen( + context: Context = LocalContext.current, + requestSignInKakao: (accessToken: String) -> Unit = {}, + navigateToSignInEmail: () -> Unit = {}, + navigateToTermsAndPolicy: (type: RuleType) -> Unit = {}, +) { + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween + ) { + // Logo + Image( + painter = painterResource(id = R.drawable.ic_dayo_logo_onbording), + contentDescription = stringResource(id = R.string.app_logo), + modifier = Modifier.padding(start = 18.dp, top = 8.dp) + ) + + // Onboarding Pager + Column(verticalArrangement = Arrangement.spacedBy(32.dp)) { + val pagerState = rememberPagerState(pageCount = { 4 }) + HorizontalPager(state = pagerState) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 60.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OnBoardingItem(page = it) + } + } + + // Indicator + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + repeat(pagerState.pageCount) { iteration -> + val color = if (pagerState.currentPage == iteration) Dark else Gray6_F0F1F3 + Box( + modifier = Modifier + .padding(horizontal = 4.5.dp) + .clip(CircleShape) + .background(color) + .size(6.dp) + ) + } + } + } + + Column { + FilledRoundedCornerButton( + onClick = { onClickKakaoLoginButton(context, requestSignInKakao) }, + label = stringResource(id = R.string.login_select_method_kakao), + modifier = Modifier + .fillMaxWidth() + .height(44.dp) + .padding(horizontal = 18.dp), + color = ButtonDefaults.buttonColors( + containerColor = Color(0xFFFBE44D), + contentColor = Color(0xFF3B1F1E) + ), + icon = { Icon(painter = painterResource(id = R.drawable.ic_kakao), "Kakao") }, + textStyle = DayoTheme.typography.b5 + ) + + Spacer(modifier = Modifier.height(12.dp)) + + FilledRoundedCornerButton( + onClick = { navigateToSignInEmail() }, + label = stringResource(id = R.string.login_select_method_email), + modifier = Modifier + .fillMaxWidth() + .height(44.dp) + .padding(horizontal = 18.dp), + color = ButtonDefaults.buttonColors( + containerColor = Dark, + contentColor = White_FFFFFF + ), + icon = { Icon(Icons.Filled.Email, "Email") }, + textStyle = DayoTheme.typography.b5 + ) + + // Policy Text + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 19.dp, vertical = 19.dp), + horizontalArrangement = Arrangement.Center + ) { + Text(text = "๊ฐ€์ž… ์‹œ DAYO์˜ ", style = DayoTheme.typography.caption3.copy(Gray4_C5CAD2)) + DayoTextButton( + onClick = { navigateToTermsAndPolicy(RuleType.TERMS_AND_CONDITIONS) }, + text = "์ด์šฉ์•ฝ๊ด€", + textStyle = DayoTheme.typography.caption3.copy( + Gray4_C5CAD2 + ), + underline = true + ) + Text(text = " ๋ฐ ", style = DayoTheme.typography.caption3.copy(Gray4_C5CAD2)) + DayoTextButton( + onClick = { navigateToTermsAndPolicy(RuleType.PRIVACY_POLICY) }, + text = "๊ฐœ์ธ์ •๋ณด ์ทจ๊ธ‰๋ฐฉ์นจ", + textStyle = DayoTheme.typography.caption3.copy( + Gray4_C5CAD2 + ), + underline = true + ) + Text( + text = "์— ๋™์˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.", style = DayoTheme.typography.caption3.copy( + Gray4_C5CAD2 + ) + ) + } + } + } +} + +@Composable +private fun OnBoardingItem( + page: Int +) { + // Show Lottie + val onboardingLottieSpec: LottieCompositionSpec? = when (page) { + 0 -> LottieCompositionSpec.RawRes(R.raw.onboarding_first) + 1 -> LottieCompositionSpec.RawRes(R.raw.onboarding_second) + 2 -> LottieCompositionSpec.RawRes(R.raw.onboarding_third) + else -> null + } + + if (onboardingLottieSpec != null) { + val onboardingLottie by rememberLottieComposition(onboardingLottieSpec) + + // TODO FIX : ํŠน์ • ํ•ด์ƒ๋„์—์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๊นจ์ง€๋Š” ํ˜„์ƒ ์ˆ˜์ • ํ•„์š” + LottieAnimation( + composition = onboardingLottie, + iterations = LottieConstants.IterateForever, + modifier = Modifier + .size(240.dp, 200.dp) + ) + + Spacer(modifier = Modifier.height(32.dp)) + } + + // Show Text + val annotatedString = when (page) { + 0 -> buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append("์ธ๊ธฐ๊ฐ€ ๋งŽ์€ ๋‹ค๊พธ") + } + append("๋“ค์„\n ๊ตฌ๊ฒฝํ•  ์ˆ˜ ์žˆ์–ด์š”") + } + + 1 -> buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append("๋‚ด ์ทจํ–ฅ์˜ ๋‹ค๊พธ๋Ÿฌ") + } + append("๋“ค์„\n ํŒ”๋กœ์šฐํ•˜๊ณ  ๋ชจ์•„๋ณผ ์ˆ˜ ์žˆ์–ด์š”") + } + + 2 -> buildAnnotatedString { + append("๋‚ด ๋‹ค๊พธ๋“ค์„ ") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append("ํด๋”๋ฅผ ๋งŒ๋“ค์–ด\n ๋ถ„๋ฅ˜") + } + append("ํ•  ์ˆ˜ ์žˆ์–ด์š”") + } + + 3 -> buildAnnotatedString { + append("๋‚ด๊ฐ€ ์—ด์‹ฌํžˆ ๊พธ๋ฏผ ๋‹ค์ด์–ด๋ฆฌ,\n") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold, color = Primary_23C882)) { + append("DAYO") + } + append("์— ๊ณต์œ ํ•˜๋Ÿฌ ๊ฐ€๋ณผ๊นŒ์š”?") + } + + else -> buildAnnotatedString { } + } + + Text( + text = annotatedString, + modifier = if (page == 3) Modifier.padding(vertical = 116.dp) else Modifier, + textAlign = TextAlign.Center, + style = DayoTheme.typography.b2 + ) +} + +private fun onClickKakaoLoginButton( + context: Context, + requestSignInKakao: (accessToken: String) -> Unit, +) { + val callback: (OAuthToken?, Throwable?) -> Unit = { token, error -> + if (error != null) { + when (error) { + is ClientError -> + when (error.reason) { + ClientErrorCause.Cancelled -> + Log.d("kakao login", "์ทจ์†Œ๋จ (back button)", error) + + ClientErrorCause.NotSupported -> + Log.e("kakao login", "์ง€์›๋˜์ง€ ์•Š์Œ (์นดํ†ก ๋ฏธ์„ค์น˜)", error) + + else -> + Log.e("kakao login", "๊ธฐํƒ€ ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ", error) + } + + is AuthError -> + when (error.reason) { + AuthErrorCause.AccessDenied -> + Log.d("kakao login", "์ทจ์†Œ๋จ (๋™์˜ ์ทจ์†Œ)", error) + + AuthErrorCause.Misconfigured -> + Log.e( + "kakao login", + "๊ฐœ๋ฐœ์ž์‚ฌ์ดํŠธ ์•ฑ ์„ค์ •์— ํ‚คํ•ด์‹œ๋ฅผ ๋“ฑ๋กํ•˜์„ธ์š”. ํ˜„์žฌ ๊ฐ’: ${KakaoSdk.keyHash}", + error + ) + + else -> + Log.e("kakao login", "๊ธฐํƒ€ ์ธ์ฆ ์—๋Ÿฌ", error) + } + + else -> + Log.e("kakao login", "๊ธฐํƒ€ ์—๋Ÿฌ (๋„คํŠธ์›Œํฌ ์žฅ์•  ๋“ฑ..)", error) + } + } else if (token != null) { + Log.i("kakao login", "๋กœ๊ทธ์ธ ์„ฑ๊ณต ${token.accessToken}") + requestSignInKakao(token.accessToken) + } + } + + if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { + UserApiClient.instance.loginWithKakaoTalk(context) { token, error -> + if (error != null) { + Log.e("kakao login", "์นด์นด์˜คํ†ก์œผ๋กœ ๋กœ๊ทธ์ธ ์‹คํŒจ", error) + if (error is ClientError && error.reason == ClientErrorCause.Cancelled) { + return@loginWithKakaoTalk + } + + UserApiClient.instance.loginWithKakaoAccount( + context, + callback = callback + ) + } else if (token != null) { + Log.i("kakao login", "์นด์นด์˜คํ†ก์œผ๋กœ ๋กœ๊ทธ์ธ ์„ฑ๊ณต ${token.accessToken}") + requestSignInKakao(token.accessToken) + } + } + } else { + UserApiClient.instance.loginWithKakaoAccount(context, callback = callback) + } +} + +@Composable +private fun ObserveSignInStatus( + context: Context, + snackBarHostState: SnackbarHostState = SnackbarHostState(), + signInSuccess: Status?, + getCurrentUserInfo: () -> User = { User() }, + navigateToProfileSetting: () -> Unit = {}, +) { + LaunchedEffect(signInSuccess) { + when (signInSuccess) { + Status.SUCCESS -> { + if (getCurrentUserInfo().nickname?.isNullOrEmpty() == true) { + navigateToProfileSetting() + } else { + navigateToHome(context) + } + } + + Status.ERROR -> { + // ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์‹คํŒจ + snackBarHostState.showSnackbar( + ContextCompat.getString(context, R.string.sign_in_kakao_fail_message) + ) + } + + Status.LOADING -> { } + + else -> {} + } + } +} + +fun navigateToHome(context: Context) { + val intent = Intent(context, MainActivity::class.java) + intent.flags = + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + (context as? LoginActivity)?.let { + it.startActivity(intent) + it.finish() + } +} + +@Composable +@Preview(showBackground = true) +private fun PreviewLoginScreen() { + DayoTheme { + SignInScreen() + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpCompleteScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpCompleteScreen.kt new file mode 100644 index 00000000..036faab2 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpCompleteScreen.kt @@ -0,0 +1,55 @@ +package daily.dayo.presentation.screen.account + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme + +@Preview +@Composable +fun SignUpCompleteScreen(nickname: String = "๋‹‰๋„ค์ž„") { + val completeLottieSpec: LottieCompositionSpec = + LottieCompositionSpec.RawRes(R.raw.signup_email_complete_dayo_logo) + val completeLottie by rememberLottieComposition(completeLottieSpec) + Box(modifier = Modifier.fillMaxSize()) { + Text( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(0.dp, 12.dp, 0.dp, 0.dp), + text = buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = DayoTheme.typography.h1.fontWeight)) { + append(nickname) + } + append("๋‹˜,\n๊ฐ€์ž…์„ ์ถ•ํ•˜๋“œ๋ ค์š”!") + }, + style = DayoTheme.typography.h2.copy(color = Dark), + textAlign = TextAlign.Center, + ) + + LottieAnimation( + composition = completeLottie, + iterations = LottieConstants.IterateForever, + modifier = Modifier + .size(200.dp) + .align(Alignment.Center), + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt new file mode 100644 index 00000000..4746159c --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/SignUpEmailScreen.kt @@ -0,0 +1,688 @@ +package daily.dayo.presentation.screen.account + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.Text +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.common.image.ImageResizeUtil.USER_PROFILE_THUMBNAIL_RESIZE_SIZE +import daily.dayo.presentation.common.image.ImageResizeUtil.resizeBitmap +import daily.dayo.presentation.common.image.launchCamera +import daily.dayo.presentation.common.image.launchGallery +import daily.dayo.presentation.screen.account.model.EmailCertificationState +import daily.dayo.presentation.screen.account.model.NicknameCertificationState +import daily.dayo.presentation.screen.account.model.SignUpStep +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.FilledRoundedCornerButton +import daily.dayo.presentation.view.Loading +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.dialog.ProfileImageBottomSheetDialog +import daily.dayo.presentation.view.dialog.getBottomSheetDialogState +import daily.dayo.presentation.viewmodel.AccountViewModel +import daily.dayo.presentation.viewmodel.AccountViewModel.Companion.EMAIL_CERTIFICATE_AUTH_CODE_INITIAL +import daily.dayo.presentation.viewmodel.AccountViewModel.Companion.SIGN_UP_EMAIL_CERTIFICATE_AUTH_CODE_FAIL +import daily.dayo.presentation.viewmodel.ProfileSettingViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +const val SIGN_UP_EMAIL_CERTIFICATE_AUTH_TIME_OUT = 180 +const val NICKNAME_PERMIT_FORMAT = "^[๊ฐ€-ํžฃใ„ฑ-ใ…Žใ…-ใ…ฃa-zA-Z0-9]{2,10}$" +const val PASSWORD_PERMIT_FORMAT = "^[a-z0-9]{8,16}$" +const val IMAGE_TEMP_FILE_NAME_FORMAT = "yyyy-MM-d-HH-mm-ss-SSS" +const val IMAGE_TEMP_FILE_EXTENSION = ".jpg" + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SignUpEmailRoute( + coroutineScope: CoroutineScope = rememberCoroutineScope(), + snackBarHostState: SnackbarHostState, + onBackClick: () -> Unit = {}, + accountViewModel: AccountViewModel = hiltViewModel(), + profileSettingViewModel: ProfileSettingViewModel = hiltViewModel(), + startSignUpStep: SignUpStep = SignUpStep.EMAIL_INPUT +) { + val context = LocalContext.current + val contentResolver = context.contentResolver + val bottomSheetState = getBottomSheetDialogState() + val keyboardController = LocalSoftwareKeyboardController.current + val bitmapOptions = + BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.ARGB_8888 } + + var signUpStep by remember { mutableStateOf(startSignUpStep) } + val isEmailDuplicate by accountViewModel.isEmailDuplicate.collectAsStateWithLifecycle() + val certificateEmailAuthCode by accountViewModel.certificateEmailAuthCode.collectAsStateWithLifecycle() + val isNicknameDuplicate by accountViewModel.isNicknameDuplicate.collectAsStateWithLifecycle() + var showProfileGallery by remember { mutableStateOf(false) } + var showProfileCapture by remember { mutableStateOf(false) } + val profileImgState = remember { mutableStateOf(null) } + val signUpStatus by accountViewModel.signupSuccess.collectAsStateWithLifecycle() + val updateProfileStatus by profileSettingViewModel.isUpdateSuccess.collectAsStateWithLifecycle() + + val openGallery = launchGallery( + context = context, + onImageSelected = { uri -> + if (uri != null) { + coroutineScope.launch(Dispatchers.IO) { + val inputStream = contentResolver.openInputStream(uri) + inputStream?.use { + BitmapFactory.decodeStream(it, null, bitmapOptions)?.let { + profileImgState.value = it + } + } + } + } + }, + onPermissionDenied = { + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.permission_fail_message_gallery)) + } + } + ) + val openCamera = launchCamera( + context = context, + onImageCaptured = { bitmap -> + bitmap?.let { profileImgState.value = it } + }, + onPermissionDenied = { + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.permission_fail_message_camera)) + } + } + ) + + if (showProfileGallery) { + openGallery() + showProfileGallery = false + } + + if (showProfileCapture) { + openCamera() + showProfileCapture = false + } + + SignUpEmailScreen( + context = context, + coroutineScope = coroutineScope, + bottomSheetState = bottomSheetState, + hideKeyboard = { keyboardController?.hide() }, + onBackClick = onBackClick, + requestIsEmailDuplicate = { accountViewModel.requestCheckEmailDuplicate(it) }, + onCertificateEmailClick = { + coroutineScope.launch { + accountViewModel.requestCertificateEmail(it) + snackBarHostState.showSnackbar(context.getString(R.string.sign_up_email_set_address_certification_send_snackbar)) + } + }, + requestIsNicknameDuplicate = { accountViewModel.requestCheckNicknameDuplicate(it) }, + onProfileUpdateClick = { nickname, profileImg -> + val imageFileTimeFormat = SimpleDateFormat(IMAGE_TEMP_FILE_NAME_FORMAT, Locale.KOREA) + val fileName = imageFileTimeFormat.format(Date(System.currentTimeMillis())).toString() + + IMAGE_TEMP_FILE_EXTENSION + + coroutineScope.launch { + val resizedProfileImg = withContext(Dispatchers.Default) { + profileImg?.let { + resizeBitmap( + it, + USER_PROFILE_THUMBNAIL_RESIZE_SIZE, + USER_PROFILE_THUMBNAIL_RESIZE_SIZE + ) + } + } + + profileSettingViewModel.requestUpdateMyProfileWithResizedFile( + nickname = nickname, + profileImg = resizedProfileImg, + profileImgTempDir = "${context.cacheDir}/$fileName", + isReset = profileImg == null + ) + } + }, + onSignUpEmailClick = { email, nickname, password, profileImg -> + val imageFileTimeFormat = SimpleDateFormat(IMAGE_TEMP_FILE_NAME_FORMAT, Locale.KOREA) + val fileName = imageFileTimeFormat.format(Date(System.currentTimeMillis())).toString() + + IMAGE_TEMP_FILE_EXTENSION + + coroutineScope.launch { + val resizedProfileImg = withContext(Dispatchers.Default) { + profileImg?.let { + resizeBitmap( + it, + USER_PROFILE_THUMBNAIL_RESIZE_SIZE, + USER_PROFILE_THUMBNAIL_RESIZE_SIZE + ) + } + } + + accountViewModel.requestSignupEmail( + email = email, + nickname = nickname, + password = password, + profileImg = resizedProfileImg, + profileImgTempDir = "${context.cacheDir}/$fileName" + ) + } + }, + isEmailDuplicate = isEmailDuplicate, + certificateEmailAuthCode = certificateEmailAuthCode + ?: EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString(), + isNicknameDuplicate = isNicknameDuplicate, + profileImg = profileImgState.value, + signUpStatus = signUpStatus, + updateProfileStatus = updateProfileStatus, + startSignUpStep = startSignUpStep, + signUpStep = signUpStep, + setSignUpStep = { signUpStep = it } + ) + Loading( + isVisible = (signUpStatus == Status.LOADING || updateProfileStatus == Status.LOADING), + message = stringResource(R.string.signup_email_alert_message_loading) + ) + + ProfileImageBottomSheetDialog( + bottomSheetState, + onClickProfileSelect = { + coroutineScope.launch { + showProfileGallery = true + bottomSheetState.bottomSheetState.hide() + } + }, + onClickProfileCapture = { + coroutineScope.launch { + showProfileCapture = true + bottomSheetState.bottomSheetState.hide() + } + }, + onClickProfileReset = { + profileImgState.value = null + coroutineScope.launch { + bottomSheetState.bottomSheetState.hide() + } + }, + ) +} + +@Composable +fun SignUpEmailTitleLayout( + title: String = "", + subTitle: String = "", +) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = title, + style = DayoTheme.typography.h1.copy(color = Dark), + ) + + // SubTitle ์˜์—ญ + AnimatedVisibility(visible = subTitle.isNotBlank()) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(4.dp) + ) + Text( + text = subTitle, + style = DayoTheme.typography.b6.copy(color = Gray2_767B83), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@Preview +fun SignUpEmailScreen( + context: Context = LocalContext.current, + coroutineScope: CoroutineScope = rememberCoroutineScope(), + bottomSheetState: BottomSheetScaffoldState = getBottomSheetDialogState(), + hideKeyboard: () -> Unit = {}, + onBackClick: () -> Unit = {}, + requestIsEmailDuplicate: (email: String) -> Unit = {}, + onCertificateEmailClick: (email: String) -> Unit = {}, + requestIsNicknameDuplicate: (nickname: String) -> Unit = {}, + onProfileUpdateClick: (nickname: String, profileImg: Bitmap?) -> Unit = { _, _ -> }, + onSignUpEmailClick: (email: String, nickname: String, password: String, profileImg: Bitmap?) -> Unit = { _, _, _, _ -> }, + isEmailDuplicate: Status = Status.LOADING, + certificateEmailAuthCode: String = EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString(), + isNicknameDuplicate: Boolean = false, + profileImg: Bitmap? = null, + setProfileImg: (Bitmap?) -> Unit = {}, + signUpStatus: Status? = null, + updateProfileStatus: Status? = null, + startSignUpStep: SignUpStep = SignUpStep.EMAIL_INPUT, + signUpStep: SignUpStep = SignUpStep.EMAIL_INPUT, + setSignUpStep: (SignUpStep) -> Unit = {}, +) { + val isNextButtonEnabled = remember { mutableStateOf(false) } + val isNextButtonClickable = remember { mutableStateOf(false) } + + // Email + val emailState = remember { mutableStateOf("") } + val emailCertificationState = + remember { mutableStateOf(EmailCertificationState.BEFORE_CERTIFICATION) } + val emailCertificateCodeState = remember { mutableStateOf("") } + val isEmailCertificateError: MutableState = remember { mutableStateOf(null) } + + // Password + val passwordState = remember { mutableStateOf("") } + val isPasswordPassFormatError = remember { mutableStateOf(false) } + val passwordConfirmState = remember { mutableStateOf("") } + val isPasswordMatchError = remember { mutableStateOf(false) } + + // Nickname + val nicknameState = remember { mutableStateOf("") } + val nicknameCertificationState = + remember { mutableStateOf(NicknameCertificationState.BEFORE_CERTIFICATION) } + + LaunchedEffect(signUpStatus, updateProfileStatus) { + if (signUpStep == SignUpStep.PROFILE_SETUP && + (signUpStatus == Status.SUCCESS || updateProfileStatus == Status.SUCCESS) + ) { + setSignUpStep(SignUpStep.SIGNUP_COMPLETE) + } + } + + LaunchedEffect(emailState.value, isEmailDuplicate) { + emailCertificationState.value = when { + emailState.value.isBlank() -> { + EmailCertificationState.BEFORE_CERTIFICATION + } + + !android.util.Patterns.EMAIL_ADDRESS.matcher(emailState.value).matches() -> { + EmailCertificationState.INVALID_FORMAT + } + + isEmailDuplicate == Status.ERROR -> { + EmailCertificationState.DUPLICATE_EMAIL + } + + isEmailDuplicate == Status.LOADING -> { + EmailCertificationState.IN_PROGRESS_CHECK_EMAIL + } + + isEmailDuplicate == Status.SUCCESS -> { + EmailCertificationState.SUCCESS_CHECK_EMAIL + } + + else -> emailCertificationState.value + } + } + + LaunchedEffect(isNicknameDuplicate, nicknameState.value) { + nicknameCertificationState.value = when { + nicknameState.value.isBlank() -> { + NicknameCertificationState.BEFORE_CERTIFICATION + } + + !Regex(NICKNAME_PERMIT_FORMAT).matches(nicknameState.value) -> { + NicknameCertificationState.INVALID_FORMAT + } + + isNicknameDuplicate -> { + NicknameCertificationState.DUPLICATE_NICKNAME + } + + !isNicknameDuplicate -> { + NicknameCertificationState.SUCCESS + } + + else -> nicknameCertificationState.value + } + } + + SignUpEmailScaffold( + context = context, + startSignUpStep = startSignUpStep, + signUpStep = signUpStep, + onBackClick = { + when (signUpStep) { + SignUpStep.EMAIL_VERIFICATION, SignUpStep.PASSWORD_INPUT -> { + emailState.value = "" + emailCertificationState.value = + EmailCertificationState.BEFORE_CERTIFICATION + emailCertificateCodeState.value = "" + isEmailCertificateError.value = null + passwordState.value = "" + setSignUpStep(SignUpStep.EMAIL_INPUT) + } + + SignUpStep.PASSWORD_CONFIRM, SignUpStep.PROFILE_SETUP -> { + passwordState.value = "" + isPasswordPassFormatError.value = false + passwordConfirmState.value = "" + isPasswordMatchError.value = false + nicknameState.value = "" + nicknameCertificationState.value = + NicknameCertificationState.BEFORE_CERTIFICATION + setProfileImg(null) + setSignUpStep(SignUpStep.PASSWORD_INPUT) + } + + SignUpStep.EMAIL_INPUT, SignUpStep.SIGNUP_COMPLETE -> { + onBackClick() + } + } + }, + title = when (signUpStep) { + SignUpStep.EMAIL_INPUT, SignUpStep.EMAIL_VERIFICATION -> stringResource(R.string.sign_up_email_set_address_title) + SignUpStep.PASSWORD_INPUT, SignUpStep.PASSWORD_CONFIRM -> stringResource(R.string.sign_up_email_set_password_title) + else -> "" + }, + subTitle = when (signUpStep) { + SignUpStep.EMAIL_VERIFICATION -> + stringResource(R.string.sign_up_email_set_address_subtitle) + + else -> "" + }, + isNextButtonEnabled = isNextButtonEnabled.value, + isNextButtonClickable = isNextButtonClickable.value, + onNextClick = { + hideKeyboard() + isNextButtonEnabled.value = false + when (signUpStep) { + SignUpStep.EMAIL_INPUT -> { + onCertificateEmailClick(emailState.value) + setSignUpStep(SignUpStep.EMAIL_VERIFICATION) + } + + SignUpStep.EMAIL_VERIFICATION -> { + if (certificateEmailAuthCode != EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString() && + certificateEmailAuthCode != SIGN_UP_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString() + ) { + if (certificateEmailAuthCode == emailCertificateCodeState.value) { + setSignUpStep(SignUpStep.PASSWORD_INPUT) + } else { + isEmailCertificateError.value = true + } + } + } + + SignUpStep.PASSWORD_INPUT -> { + val passwordFormat = Regex(PASSWORD_PERMIT_FORMAT) + if (passwordFormat.matches(passwordState.value)) { + setSignUpStep(SignUpStep.PASSWORD_CONFIRM) + } else { + isPasswordPassFormatError.value = true + } + } + + SignUpStep.PASSWORD_CONFIRM -> { + if (passwordState.value == passwordConfirmState.value) { + setSignUpStep(SignUpStep.PROFILE_SETUP) + } else { + isPasswordMatchError.value = true + } + } + + SignUpStep.PROFILE_SETUP -> { + if (startSignUpStep == SignUpStep.PROFILE_SETUP) { + onProfileUpdateClick(nicknameState.value, profileImg) + } else { + onSignUpEmailClick( + emailState.value, + nicknameState.value, + passwordState.value, + profileImg + ) + } + } + + SignUpStep.SIGNUP_COMPLETE -> Unit + } + } + ) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(28.dp) + ) + when (signUpStep) { + SignUpStep.EMAIL_INPUT -> { + SetEmailView( + context = context, + isNextButtonEnabled = isNextButtonEnabled.value, + setNextButtonEnabled = { isNextButtonEnabled.value = it }, + isNextButtonClickable = isNextButtonClickable.value, + setIsNextButtonClickable = { isNextButtonClickable.value = it }, + email = emailState.value, + setEmail = { emailState.value = it }, + emailCertification = emailCertificationState.value, + requestEmailCertification = { requestIsEmailDuplicate(it) } + ) + } + + SignUpStep.EMAIL_VERIFICATION -> { + SetEmailCertificationView( + timeOutSeconds = SIGN_UP_EMAIL_CERTIFICATE_AUTH_TIME_OUT, + signUpStep = signUpStep, + isNextButtonEnabled = isNextButtonEnabled.value, + setNextButtonEnabled = { isNextButtonEnabled.value = it }, + isNextButtonClickable = isNextButtonClickable.value, + setIsNextButtonClickable = { isNextButtonClickable.value = it }, + email = emailState.value, + emailCertification = emailCertificationState.value, + certificationCode = certificateEmailAuthCode, + certificationInputCode = emailCertificateCodeState.value, + setCertificationInputCode = { emailCertificateCodeState.value = it }, + isEmailCertificateError = isEmailCertificateError.value, + setIsEmailCertificateError = { isEmailCertificateError.value = it }, + requestEmailCertification = { onCertificateEmailClick(it) } + ) + } + + SignUpStep.PASSWORD_INPUT, SignUpStep.PASSWORD_CONFIRM -> { + SetPasswordView( + passwordInputViewCondition = signUpStep == SignUpStep.PASSWORD_INPUT, + passwordConfirmationViewCondition = signUpStep == SignUpStep.PASSWORD_CONFIRM, + isNextButtonEnabled = isNextButtonEnabled.value, + setNextButtonEnabled = { isNextButtonEnabled.value = it }, + isNextButtonClickable = isNextButtonClickable.value, + setIsNextButtonClickable = { isNextButtonClickable.value = it }, + password = passwordState.value, + setPassword = { passwordState.value = it }, + isPasswordFormatValid = !isPasswordPassFormatError.value, + setIsPasswordFormatValid = { isPasswordPassFormatError.value = !it }, + passwordConfirmation = passwordConfirmState.value, + setPasswordConfirmation = { passwordConfirmState.value = it }, + isPasswordMismatch = isPasswordMatchError.value, + setIsPasswordMismatch = { isPasswordMatchError.value = it } + ) + } + + SignUpStep.PROFILE_SETUP -> { + SetProfileSetupView( + bottomSheetState = bottomSheetState, + coroutineScope = coroutineScope, + isNextButtonEnabled = isNextButtonEnabled, + isNextButtonClickable = isNextButtonClickable, + nicknameState = nicknameState, + nicknameCertificationState = nicknameCertificationState, + requestIsNicknameDuplicate = requestIsNicknameDuplicate, + profileImg = profileImg + ) + } + + SignUpStep.SIGNUP_COMPLETE -> { + SignUpCompleteScreen(nickname = nicknameState.value) + } + } + } +} + +@Composable +fun SignUpEmailScaffold( + context: Context = LocalContext.current, + startSignUpStep: SignUpStep = SignUpStep.EMAIL_INPUT, + signUpStep: SignUpStep = SignUpStep.EMAIL_INPUT, + onBackClick: () -> Unit = {}, + title: String = "", + subTitle: String = "", + isNextButtonEnabled: Boolean = false, + isNextButtonClickable: Boolean = false, + onNextClick: () -> Unit = {}, + content: @Composable (ColumnScope.() -> Unit), +) { + BackHandler { onBackClick() } + Scaffold( + topBar = { + TopNavigation( + leftIcon = { + if ((signUpStep != SignUpStep.SIGNUP_COMPLETE)) { + NoRippleIconButton( + onClick = { onBackClick() }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + } + }, + rightIcon = { + if (signUpStep == SignUpStep.SIGNUP_COMPLETE) { + NoRippleIconButton( + onClick = { + if (startSignUpStep == SignUpStep.PROFILE_SETUP) { + navigateToHome(context = context) + } else { + onBackClick() + } + }, + iconContentDescription = stringResource(R.string.close_sign), + iconPainter = painterResource(id = R.drawable.ic_x_sign), + ) + } + }, + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + ) { + + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .padding(horizontal = 20.dp, vertical = 0.dp) + .fillMaxWidth() + .wrapContentSize() + ) { + if (signUpStep.stepNum <= SignUpStep.PASSWORD_CONFIRM.stepNum) { + SignUpEmailTitleLayout(title = title, subTitle = subTitle) + } + content() + } + + if (signUpStep != SignUpStep.SIGNUP_COMPLETE) { + Spacer(modifier = Modifier.weight(1f)) + SignUpEmailBottomLayout( + signUpStep = signUpStep, + onNextClick = { onNextClick() }, + isSignUpButtonEnabled = isNextButtonEnabled, + isSignUpButtonClickable = isNextButtonClickable, + ) + } + } + } +} + +@Composable +fun SignUpEmailBottomLayout( + signUpStep: SignUpStep = SignUpStep.EMAIL_INPUT, + onNextClick: () -> Unit = {}, + isSignUpButtonEnabled: Boolean, + isSignUpButtonClickable: Boolean +) { + Column( + modifier = Modifier + .imePadding() + .fillMaxWidth() + .wrapContentHeight() + ) { + SignUpEmailNextButton( + signUpStep = signUpStep, + onButtonClick = { onNextClick() }, + isSignUpButtonEnabled = isSignUpButtonEnabled, + isSignUpButtonClickable = isSignUpButtonClickable + ) + } +} + +@Composable +@Preview +fun SignUpEmailNextButton( + signUpStep: SignUpStep = SignUpStep.EMAIL_INPUT, + onButtonClick: () -> Unit = {}, + isSignUpButtonEnabled: Boolean = false, + isSignUpButtonClickable: Boolean = false, +) { + if (signUpStep == SignUpStep.SIGNUP_COMPLETE) return + + Column( + modifier = Modifier + .padding(horizontal = 18.dp, vertical = 20.dp) + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + FilledRoundedCornerButton( + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + label = when (signUpStep) { + SignUpStep.EMAIL_INPUT -> stringResource(R.string.sign_up_email_set_address_next_button) + SignUpStep.EMAIL_VERIFICATION -> stringResource(R.string.sign_up_email_set_address_certification_next_button) + SignUpStep.PASSWORD_INPUT -> stringResource(R.string.sign_up_email_set_password_next_button) + SignUpStep.PASSWORD_CONFIRM -> stringResource(R.string.sign_up_email_set_password_confirm_next_button) + SignUpStep.PROFILE_SETUP -> stringResource(R.string.sign_up_email_set_profile_next_button) + else -> "" + }, + textStyle = DayoTheme.typography.b3.copy(color = White_FFFFFF), + onClick = { if (isSignUpButtonClickable) onButtonClick() }, + enabled = isSignUpButtonEnabled, + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt new file mode 100644 index 00000000..336f6c5b --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/WithdrawScreen.kt @@ -0,0 +1,943 @@ +package daily.dayo.presentation.screen.account + +import android.content.Context +import android.content.Intent +import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SheetValue +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import coil.request.ImageRequest +import daily.dayo.presentation.R +import daily.dayo.presentation.activity.LoginActivity +import daily.dayo.presentation.activity.MainActivity +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.PrimaryL1_8FD9B9 +import daily.dayo.presentation.theme.PrimaryL3_F2FBF7 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.FilledRoundedCornerButton +import daily.dayo.presentation.view.Loading +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.dialog.ConfirmDialog +import daily.dayo.presentation.viewmodel.AccountViewModel +import daily.dayo.domain.model.WithdrawalReason +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +const val OTHER_REASON_TEXT_MAX_LENGTH = 100 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WithdrawScreen( + onBackClick: () -> Unit, + bottomSheetState: BottomSheetScaffoldState, + bottomSheetContent: (@Composable () -> Unit) -> Unit, + snackBarHostState: SnackbarHostState = remember { SnackbarHostState() }, + accountViewModel: AccountViewModel = hiltViewModel(), + onNavigateToHome: () -> Unit = {}, + onNavigateToMyPage: () -> Unit = {}, +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + val withdrawStep = remember { mutableStateOf(WithdrawStep.REASON_SELECT) } + var selectedReason by remember { mutableStateOf(null) } + val onClickWithdrawContinue: (WithdrawalReason) -> Unit = { reason -> + coroutineScope.launch { bottomSheetState.bottomSheetState.expand() } + bottomSheetContent { + selectedReason?.let { selected -> + WithdrawContinueBottomSheetDialog( + coroutineScope = coroutineScope, + bottomSheetState = bottomSheetState, + keyboardController = keyboardController, + focusManager = focusManager, + setWithdrawStep = { withdrawStep.value = it }, + selectedReason = selected, + setSelectedReason = { selectedReason = it }, + snackBarHostState = snackBarHostState, + accountViewModel = accountViewModel, + onNavigateToHome = onNavigateToHome, + onNavigateToMyPage = onNavigateToMyPage, + ) + } + } + } + + LaunchedEffect(bottomSheetState) { + snapshotFlow { bottomSheetState.bottomSheetState.currentValue } + .collect { value -> + if (value == SheetValue.Hidden) { + focusManager.clearFocus(force = true) + keyboardController?.hide() + if (withdrawStep.value == WithdrawStep.REASON_SELECT) { + selectedReason = null + } + } + } + } + + // BottomSheet๊ฐ€ ์—ด๋ ค์žˆ์„ ๋•Œ์˜ ๋’ค๋กœ๊ฐ€๊ธฐ ์ฒ˜๋ฆฌ + if (bottomSheetState.bottomSheetState.currentValue == SheetValue.Expanded) { + BackHandler { + coroutineScope.launch { + keyboardController?.hide() + focusManager.clearFocus() + bottomSheetState.bottomSheetState.hide() + } + } + } + + val onBackClickInWithdraw = { + when (withdrawStep.value) { + WithdrawStep.REASON_SELECT -> { + onBackClick() + } + + WithdrawStep.CONFIRM -> { + withdrawStep.value = WithdrawStep.REASON_SELECT + selectedReason = null + } + } + } + val showWithdrawDialog = remember { mutableStateOf(false) } + val withdrawSuccess by accountViewModel.withdrawSuccess.collectAsStateWithLifecycle() + + BackHandler { + if (withdrawStep.value == WithdrawStep.CONFIRM) { + withdrawStep.value = WithdrawStep.REASON_SELECT + selectedReason = null + } else { + onBackClick() + } + } + + LaunchedEffect(withdrawSuccess) { + if (withdrawSuccess == Status.SUCCESS) { + accountViewModel.clearCurrentUser() + val intent = Intent(context, LoginActivity::class.java).apply { + flags = + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + (context as? MainActivity)?.let { + it.startActivity(intent) + it.finish() + } + } + } + + Box(modifier = Modifier.fillMaxSize()) { + when (withdrawStep.value) { + WithdrawStep.REASON_SELECT -> { + WithdrawReasonList( + onBackClick = onBackClickInWithdraw, + onShowBottomSheet = { reason -> + selectedReason = SelectedWithdrawReason(reason) + onClickWithdrawContinue(reason) + } + ) + } + + WithdrawStep.CONFIRM -> { + WithdrawConfirmScreen( + context = context, + onBackClick = onBackClickInWithdraw, + showWithdrawDialog = showWithdrawDialog, + ) + + if (showWithdrawDialog.value) { + val withDrawReason = if (selectedReason?.reason == WithdrawalReason.OTHER) { + selectedReason?.otherReasonText + } else { + selectedReason?.reason?.content?.reasonTextResId?.let { + stringResource(it) + } + } + WithdrawDialog( + onConfirmClick = { + accountViewModel.requestWithdraw(content = withDrawReason ?: "") + showWithdrawDialog.value = false + }, + onDismissClick = { showWithdrawDialog.value = false }, + ) + } + + Loading( + isVisible = (withdrawSuccess == Status.LOADING || withdrawSuccess == Status.SUCCESS), + ) + } + } + } +} + +@Preview +@Composable +fun WithdrawActionbarLayout( + onBackClick: () -> Unit = {}, +) { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = { onBackClick() }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + ) +} + +@Composable +fun WithdrawReasonList( + onBackClick: () -> Unit = {}, + onShowBottomSheet: (WithdrawalReason) -> Unit +) { + Scaffold( + topBar = { WithdrawActionbarLayout(onBackClick = onBackClick) }, + modifier = Modifier + .fillMaxSize() + .background(DayoTheme.colorScheme.background) + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .padding(top = 4.dp, start = 20.dp, end = 20.dp, bottom = 8.dp) + .background(DayoTheme.colorScheme.background) + ) { + Text( + text = stringResource(id = R.string.withdraw_reason_title), + style = DayoTheme.typography.h4.copy( + color = Dark, + fontWeight = FontWeight.SemiBold + ), + ) + Spacer(modifier = Modifier.height(20.dp)) + enumValues().forEach { reason -> + WithdrawReasonItem( + reason = reason, + onClickWithdraw = { onShowBottomSheet(reason) } + ) + } + } + } +} + +@Composable +private fun WithdrawReasonItem( + reason: WithdrawalReason, + onClickWithdraw: (reason: WithdrawalReason) -> Unit, +) { + Row( + modifier = Modifier + .padding(start = 8.dp) + .fillMaxWidth() + .height(44.dp) + .clickable { onClickWithdraw(reason) }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = reason.content.reasonTextResId), + style = DayoTheme.typography.b4.copy( + color = Gray1_50545B, + fontWeight = FontWeight.Medium + ) + ) + + Spacer(modifier = Modifier.weight(1f)) + + Icon( + painter = painterResource(id = R.drawable.ic_next), + contentDescription = stringResource(id = reason.content.reasonTextResId), + tint = Gray4_C5CAD2 + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WithdrawContinueBottomSheetDialog( + coroutineScope: CoroutineScope, + bottomSheetState: BottomSheetScaffoldState, + keyboardController: SoftwareKeyboardController?, + focusManager: FocusManager, + setWithdrawStep: (WithdrawStep) -> Unit, + selectedReason: SelectedWithdrawReason?, + setSelectedReason: (SelectedWithdrawReason?) -> Unit, + snackBarHostState: SnackbarHostState, + accountViewModel: AccountViewModel, + onNavigateToHome: () -> Unit = {}, + onNavigateToMyPage: () -> Unit = {}, +) { + val recordWords by accountViewModel.recordGuideWords.collectAsStateWithLifecycle() + val followWords by accountViewModel.followGuideWords.collectAsStateWithLifecycle() + val guideImages by accountViewModel.guideImages.collectAsStateWithLifecycle() + + Surface( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .imePadding() + .padding(bottom = 0.dp), + shape = RoundedCornerShape(12.dp, 12.dp, 0.dp, 0.dp), + color = White_FFFFFF + ) { + selectedReason?.let { selected -> + Column( + modifier = Modifier + .padding(bottom = 0.dp) + .imePadding() + ) { + WithdrawHoldBottomSheet( + reason = selected.reason, + otherReasonText = selected.otherReasonText, + setOtherReasonText = { newText -> + setSelectedReason( + selected.copy(otherReasonText = newText) + ) + }, + recordWords = recordWords, + followWords = followWords, + guideImages = guideImages, + onRequestWords = { withdrawalReason -> + accountViewModel.requestWithdrawGuideWords( + withdrawalReason + ) + }, + onRequestImage = { fileName, withdrawalReason -> + accountViewModel.requestWithdrawGuideImage( + fileName, + withdrawalReason + ) + }, + onClearImages = { accountViewModel.clearGuideImages() }, + onConfirm = { + when (selected.reason) { + WithdrawalReason.OTHER -> { + if (selected.otherReasonText.isBlank()) { + return@WithdrawHoldBottomSheet + } else { + coroutineScope.launch { + keyboardController?.hide() + focusManager.clearFocus() + } + } + } + + else -> { + + } + } + + coroutineScope.launch { + setWithdrawStep(WithdrawStep.CONFIRM) + bottomSheetState.bottomSheetState.hide() + } + }, + onCancel = { + coroutineScope.launch { + keyboardController?.hide() + focusManager.clearFocus() + bottomSheetState.bottomSheetState.hide() + } + when (selected.reason) { + WithdrawalReason.WANT_TO_DELETE_HISTORY -> { + onNavigateToMyPage() + } + + WithdrawalReason.CONTENT_NOT_SATISFYING -> { + onNavigateToHome() + } + + else -> { /* DO NOTHING */ + } + } + } + ) + } + } + } +} + +@Composable +fun WithdrawHoldBottomSheet( + reason: WithdrawalReason, + onConfirm: () -> Unit, + onCancel: () -> Unit, + modifier: Modifier = Modifier, + setOtherReasonText: (String) -> Unit = {}, + otherReasonText: String = "", + recordWords: List = emptyList(), + followWords: List = emptyList(), + guideImages: Map = emptyMap(), + onRequestWords: (WithdrawalReason) -> Unit = {}, + onRequestImage: (String, WithdrawalReason) -> Unit = { _, _ -> }, + onClearImages: () -> Unit = {}, +) { + val content = reason.content + val hasWithdrawReasonGuide = reason in listOf( + WithdrawalReason.WANT_TO_DELETE_HISTORY, + WithdrawalReason.CONTENT_NOT_SATISFYING + ) + val isOtherReason = (content.reasonTextResId == R.string.withdraw_reason_other) + var otherReasonContentValue by remember { mutableStateOf(TextFieldValue(otherReasonText)) } + + // otherReasonText๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๋กœ์ปฌ ์ƒํƒœ๋„ ๋™๊ธฐํ™” + LaunchedEffect(otherReasonText) { + if (otherReasonContentValue.text != otherReasonText) { + otherReasonContentValue = TextFieldValue(otherReasonText) + } + } + + Column( + modifier = modifier + .padding(top = 20.dp, bottom = 8.dp) + .fillMaxWidth() + .wrapContentHeight() + ) { + Column( + modifier = Modifier + .padding(start = 20.dp, end = 20.dp, bottom = 8.dp) + .fillMaxWidth() + .wrapContentHeight() + ) { + Column( + modifier = Modifier + .padding(bottom = 8.dp) + ) { + + Text( + text = stringResource(id = content.titleResId), + style = DayoTheme.typography.b1, + color = Dark, + ) + + val descriptionText = stringResource(id = content.descriptionResId) + if (descriptionText.isNotBlank()) { + Spacer( + modifier = Modifier.height( + if (isOtherReason) 2.dp else 4.dp + ) + ) + Text( + text = descriptionText, + style = DayoTheme.typography.caption2.copy( + color = Gray2_767B83, + fontWeight = FontWeight.Medium + ) + ) + } + } + Spacer( + modifier = Modifier.height( + if (isOtherReason || hasWithdrawReasonGuide) 8.dp + else 12.dp + ) + ) + + if (hasWithdrawReasonGuide) { + WithdrawGuideContentUI( + reason = reason, + recordWords = recordWords, + followWords = followWords, + guideImages = guideImages, + onRequestWords = onRequestWords, + onRequestImage = onRequestImage, + onClearImages = onClearImages + ) + } + + if (isOtherReason) { + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + BasicTextField( + value = otherReasonContentValue, + onValueChange = { + if (it.text.length > OTHER_REASON_TEXT_MAX_LENGTH) return@BasicTextField + otherReasonContentValue = it + setOtherReasonText(it.text) + }, + singleLine = false, + modifier = Modifier + .fillMaxWidth() + .height(100.dp) + .focusRequester(focusRequester) + .background( + color = Color(0xFFF6F6F7), + shape = RoundedCornerShape(size = 8.dp) + ) + .padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp), + textStyle = DayoTheme.typography.b6.copy( + color = Dark, + ), + decorationBox = { innerTextField -> + if (otherReasonContentValue.text.isEmpty()) { + Text( + text = stringResource(R.string.withdraw_reason_other_hint), + style = DayoTheme.typography.b6.copy( + color = Gray4_C5CAD2, + ) + ) + } + innerTextField() + }, + ) + Spacer(modifier = Modifier.height(24.dp)) + } + + Row( + modifier = Modifier + .padding(top = 8.dp, bottom = 4.dp) + .fillMaxWidth() + ) { + FilledRoundedCornerButton( + onClick = onCancel, + label = stringResource(id = content.cancelButtonTextResId), + modifier = Modifier + .weight(1f) + .height(52.dp), + color = ButtonDefaults.buttonColors( + containerColor = PrimaryL3_F2FBF7, + contentColor = Primary_23C882 + ), + textStyle = DayoTheme.typography.b3, + radius = 16, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + FilledRoundedCornerButton( + onClick = onConfirm, + label = stringResource(id = content.confirmButtonTextResId), + modifier = Modifier + .weight(1f) + .height(52.dp), + enabled = !(reason == WithdrawalReason.OTHER && otherReasonText.isBlank()), + color = ButtonDefaults.buttonColors( + containerColor = Primary_23C882, + contentColor = White_FFFFFF, + disabledContainerColor = PrimaryL1_8FD9B9, + disabledContentColor = White_FFFFFF, + ), + textStyle = DayoTheme.typography.b3, + radius = 16, + ) + } + } + } +} + +@Composable +fun WithdrawConfirmScreen( + context: Context = LocalContext.current, + onBackClick: () -> Unit = {}, + showWithdrawDialog: MutableState = remember { mutableStateOf(false) }, +) { + val withdrawCheckLists = + remember { context.resources.getStringArray(R.array.withdraw_confirm_check_list) } + + Scaffold( + topBar = { + WithdrawActionbarLayout(onBackClick = onBackClick) + }, + modifier = Modifier + .fillMaxSize() + .background(DayoTheme.colorScheme.background) + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .background(DayoTheme.colorScheme.background) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 4.dp, start = 20.dp, end = 20.dp, bottom = 8.dp) + .background(DayoTheme.colorScheme.background) + ) { + Text( + text = stringResource(id = R.string.withdraw_confirm_title), + style = DayoTheme.typography.h4.copy( + color = Dark, + fontWeight = FontWeight.SemiBold + ), + ) + Spacer(modifier = Modifier.height(20.dp)) + withdrawCheckLists.forEachIndexed { index, text -> + WithdrawConfirmCheckItems(checkText = text) + if (index != withdrawCheckLists.lastIndex) { + Spacer(modifier = Modifier.height(12.dp)) + } + } + } + Spacer(modifier = Modifier.weight(1f)) + WithdrawButton( + onWithDrawClick = { + showWithdrawDialog.value = true + } + ) + } + } +} + +@Composable +@Preview +fun WithdrawDialog( + onConfirmClick: () -> Unit = {}, + onDismissClick: () -> Unit = {} +) { + ConfirmDialog( + title = stringResource(R.string.withdraw_final_message), + description = "", + onClickConfirmText = stringResource(R.string.withdraw_final_confirm), + onClickConfirm = onConfirmClick, + onClickCancelText = stringResource(R.string.cancel), + onClickCancel = onDismissClick, + ) +} + +@Composable +@Preview +fun WithdrawButton( + onWithDrawClick: () -> Unit = {}, +) { + Column( + modifier = Modifier + .padding(horizontal = 18.dp, vertical = 20.dp) + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + FilledRoundedCornerButton( + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + label = stringResource(R.string.withdraw_confirm), + color = ButtonDefaults.buttonColors( + containerColor = Primary_23C882, + contentColor = White_FFFFFF, + disabledContainerColor = PrimaryL1_8FD9B9, + disabledContentColor = White_FFFFFF, + ), + textStyle = DayoTheme.typography.b3, + onClick = { onWithDrawClick() }, + ) + } +} + +@Composable +fun WithdrawConfirmCheckItems( + checkText: String, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_mark), + contentDescription = null, + tint = Primary_23C882, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = checkText, + style = DayoTheme.typography.b6.copy( + color = Gray1_50545B, + fontWeight = FontWeight.Medium + ) + ) + } +} + +@Composable +private fun WithdrawGuideContentUI( + reason: WithdrawalReason, + recordWords: List, + followWords: List, + guideImages: Map, + onRequestWords: (WithdrawalReason) -> Unit, + onRequestImage: (String, WithdrawalReason) -> Unit, + onClearImages: () -> Unit +) { + val words = when (reason) { + WithdrawalReason.WANT_TO_DELETE_HISTORY -> recordWords + WithdrawalReason.CONTENT_NOT_SATISFYING -> followWords + else -> emptyList() + } + + LaunchedEffect(reason) { + when (reason) { + WithdrawalReason.WANT_TO_DELETE_HISTORY -> { + onRequestWords(reason) + onClearImages() + repeat(4) { index -> + onRequestImage("record${index + 1}.png", reason) + } + } + + WithdrawalReason.CONTENT_NOT_SATISFYING -> { + onRequestWords(reason) + onClearImages() + repeat(4) { index -> + onRequestImage("follow${index + 1}.png", reason) + } + } + + else -> { + onClearImages() + } + } + } + Column( + modifier = Modifier + .padding(top = 8.dp) + .fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + val availableImages = (1..4).mapNotNull { index -> + val fileName = when (reason) { + WithdrawalReason.WANT_TO_DELETE_HISTORY -> "record${index}.png" + WithdrawalReason.CONTENT_NOT_SATISFYING -> "follow${index}.png" + else -> return@mapNotNull null + } + if (guideImages.containsKey(fileName)) index else null + } + + if (availableImages.isNotEmpty()) { + val pagerState = rememberPagerState(pageCount = { availableImages.size }) + + HorizontalPager(state = pagerState) { pageIndex -> + val imageIndex = availableImages[pageIndex] + val fileName = when (reason) { + WithdrawalReason.WANT_TO_DELETE_HISTORY -> "record${imageIndex}.png" + WithdrawalReason.CONTENT_NOT_SATISFYING -> "follow${imageIndex}.png" + else -> "" + } + WithDrawGuideImage( + reasonTextResId = reason.content.reasonTextResId, + index = imageIndex, + imageData = guideImages[fileName] + ) + } + + if (availableImages.size > 1) { + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + horizontalArrangement = Arrangement.spacedBy( + space = 9.dp, + alignment = Alignment.CenterHorizontally + ), + ) { + repeat(availableImages.size) { iteration -> + val color = + if (pagerState.currentPage == iteration) Dark else Gray4_C5CAD2 + Box( + modifier = Modifier + .clip(CircleShape) + .background(color) + .width(6.dp) + .height(6.dp) + ) + } + } + } + } + } + + Spacer(modifier = Modifier.height(20.dp)) + val guideStrings = words.ifEmpty { emptyList() } + + Row( + modifier = Modifier + .padding(bottom = 16.dp) + .wrapContentHeight() + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + guideStrings.forEachIndexed { index, guide -> + Text( + text = guide, + color = Gray1_50545B, + textAlign = TextAlign.Center, + style = DayoTheme.typography.caption4 + ) + if (index != guideStrings.lastIndex) { + Spacer(modifier = Modifier.width(6.dp)) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_right), + contentDescription = null, + tint = Gray3_9FA5AE, + modifier = Modifier.size(12.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + } + } + } + } +} + +@Composable +fun WithDrawGuideImage( + @StringRes reasonTextResId: Int, + index: Int, + imageData: ByteArray? +) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageData) + .crossfade(true) + .memoryCacheKey("guide_${reasonTextResId}_$index") + .diskCacheKey("guide_${reasonTextResId}_$index") + .build(), + contentDescription = "withdraw guide image", + modifier = Modifier + .fillMaxWidth() + .aspectRatio(320f / 200f) + .clip(RoundedCornerShape(size = 12.dp)), + contentScale = ContentScale.Crop, + ) +} + +data class SelectedWithdrawReason( + val reason: WithdrawalReason, + val otherReasonText: String = "" +) + +val WithdrawalReason.content: WithdrawRetentionSheetContent + get() = when (this) { + WithdrawalReason.INCONVENIENT_USE -> WithdrawRetentionSheetContent( + reasonTextResId = R.string.withdraw_reason_inconvenient_use, + titleResId = R.string.withdraw_reason_inconvenient_use_hold_title, + descriptionResId = R.string.withdraw_reason_inconvenient_use_hold_description, + confirmButtonTextResId = R.string.withdraw_reason_inconvenient_use_hold_confirm, + cancelButtonTextResId = R.string.withdraw_reason_inconvenient_use_hold_cancel + ) + + WithdrawalReason.WANT_TO_DELETE_HISTORY -> WithdrawRetentionSheetContent( + reasonTextResId = R.string.withdraw_reason_want_to_delete_history, + titleResId = R.string.withdraw_reason_want_to_delete_history_hold_title, + descriptionResId = R.string.withdraw_reason_want_to_delete_history_hold_description, + confirmButtonTextResId = R.string.withdraw_reason_want_to_delete_history_hold_confirm, + cancelButtonTextResId = R.string.withdraw_reason_want_to_delete_history_hold_cancel + ) + + WithdrawalReason.RARELY_USED -> WithdrawRetentionSheetContent( + reasonTextResId = R.string.withdraw_reason_rarely_used, + titleResId = R.string.withdraw_reason_rarely_used_hold_title, + descriptionResId = R.string.withdraw_reason_rarely_used_hold_description, + confirmButtonTextResId = R.string.withdraw_reason_rarely_used_hold_confirm, + cancelButtonTextResId = R.string.withdraw_reason_rarely_used_hold_cancel + ) + + WithdrawalReason.CONTENT_NOT_SATISFYING -> WithdrawRetentionSheetContent( + reasonTextResId = R.string.withdraw_reason_content_not_satisfying, + titleResId = R.string.withdraw_reason_content_not_satisfying_hold_title, + descriptionResId = R.string.withdraw_reason_content_not_satisfying_hold_description, + confirmButtonTextResId = R.string.withdraw_reason_content_not_satisfying_hold_confirm, + cancelButtonTextResId = R.string.withdraw_reason_content_not_satisfying_hold_cancel + ) + + WithdrawalReason.OTHER -> WithdrawRetentionSheetContent( + reasonTextResId = R.string.withdraw_reason_other, + titleResId = R.string.withdraw_reason_other_hold_title, + descriptionResId = R.string.withdraw_reason_other_hold_description, + confirmButtonTextResId = R.string.withdraw_reason_other_hold_confirm, + cancelButtonTextResId = R.string.withdraw_reason_other_hold_cancel + ) + } + +data class WithdrawRetentionSheetContent( + @StringRes val reasonTextResId: Int, // ํƒˆํ‡ด ์‚ฌ์œ  ํ…์ŠคํŠธ + @StringRes val titleResId: Int, // ๋ฐ”ํ…€์‹œํŠธ ์ œ๋ชฉ + @StringRes val descriptionResId: Int, // ๋ฐ”ํ…€์‹œํŠธ ์„ค๋ช… + @StringRes val confirmButtonTextResId: Int, + @StringRes val cancelButtonTextResId: Int, +) + +enum class WithdrawStep(val stepNum: Int) { + REASON_SELECT(0), + CONFIRM(1), +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/model/CheckOAuthEmailStatus.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/model/CheckOAuthEmailStatus.kt new file mode 100644 index 00000000..5165c54d --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/model/CheckOAuthEmailStatus.kt @@ -0,0 +1,8 @@ +package daily.dayo.presentation.screen.account.model + +enum class CheckOAuthEmailStatus { + LOADING, + NORMAL_EMAIL, // ์ผ๋ฐ˜ ์ด๋ฉ”์ผ + OAUTH_ACCOUNT, // OAuth ์ด๋ฉ”์ผ + ERROR +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/model/EmailCertificationState.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/model/EmailCertificationState.kt new file mode 100644 index 00000000..f627e986 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/model/EmailCertificationState.kt @@ -0,0 +1,12 @@ +package daily.dayo.presentation.screen.account.model + +enum class EmailCertificationState { + BEFORE_CERTIFICATION, // ์ธ์ฆ ์ „ + IN_PROGRESS_CHECK_EMAIL, // ์ด๋ฉ”์ผ ์ค‘๋ณต/ํ˜•์‹ ํ™•์ธ ์ค‘ + SUCCESS_CHECK_EMAIL, // ์ธ์ฆ ์„ฑ๊ณต + NOT_EXIST_EMAIL, // ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ด๋ฉ”์ผ๋กœ ์ธ์ฆ ์‹คํŒจ + DUPLICATE_EMAIL, // ์ค‘๋ณต๋œ ์ด๋ฉ”์ผ๋กœ ์ธ์ฆ ์‹คํŒจ + INVALID_FORMAT, // ์ž˜๋ชป๋œ ํ˜•์‹์˜ ์ด๋ฉ”์ผ๋กœ ์ธ์ฆ ์‹คํŒจ + OAUTH_EMAIL, // ์†Œ์…œ๊ณ„์ •์œผ๋กœ ๊ฐ€์ž…ํ•œ ์ด๋ฉ”์ผ๋กœ ์ธ์ฆ ์‹คํŒจ + ERROR, // ์ธ์ฆ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/model/EmailExistenceStatus.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/model/EmailExistenceStatus.kt new file mode 100644 index 00000000..99157d6f --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/model/EmailExistenceStatus.kt @@ -0,0 +1,9 @@ +package daily.dayo.presentation.screen.account.model + +enum class EmailExistenceStatus { + IDLE, // ์ฒ˜์Œ ์ง„์ž…ํ•œ ์ƒํƒœ, ์•„์ง ์•„๋ฌด ์ž…๋ ฅ๋„ ์—†์Œ + LOADING, // ์ด๋ฉ”์ผ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ ์ค‘ + EXISTS, // ์ด๋ฉ”์ผ ์กด์žฌ + NOT_EXISTS, // ์ด๋ฉ”์ผ ์กด์žฌํ•˜์ง€ ์•Š์Œ + ERROR // ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜, ์˜ˆ์™ธ ๋“ฑ ์‹คํŒจ ์ƒํ™ฉ +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/model/NicknameCertificationState.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/model/NicknameCertificationState.kt new file mode 100644 index 00000000..1db69c3a --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/model/NicknameCertificationState.kt @@ -0,0 +1,9 @@ +package daily.dayo.presentation.screen.account.model + +enum class NicknameCertificationState { + BEFORE_CERTIFICATION, // ํ™•์ธ ์ „ + IN_PROGRESS_CHECK_NICKNAME, // ๋‹‰๋„ค์ž„ ์ค‘๋ณต ํ™•์ธ ์ค‘ + SUCCESS, // ์ค‘๋ณต๋˜์ง€ ์•Š์€ ๋‹‰๋„ค์ž„ + DUPLICATE_NICKNAME, // ์ค‘๋ณต๋œ ๋‹‰๋„ค์ž„ + INVALID_FORMAT // ์ž˜๋ชป๋œ ํ˜•์‹์˜ ๋‹‰๋„ค์ž„ +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/account/model/SignUpStep.kt b/presentation/src/main/java/daily/dayo/presentation/screen/account/model/SignUpStep.kt new file mode 100644 index 00000000..429fd6dc --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/account/model/SignUpStep.kt @@ -0,0 +1,10 @@ +package daily.dayo.presentation.screen.account.model + +enum class SignUpStep(val stepNum: Int) { + EMAIL_INPUT(1), // ์ด๋ฉ”์ผ ์ฃผ์†Œ ์ž…๋ ฅ + EMAIL_VERIFICATION(2), // ์ธ์ฆ๋ฒˆํ˜ธ ์ž…๋ ฅ + PASSWORD_INPUT(3), // ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ + PASSWORD_CONFIRM(4), // ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์ž…๋ ฅ + PROFILE_SETUP(5), // ํ”„๋กœํ•„ ์„ค์ • (์‚ฌ์ง„ ๋ฐ ๋‹‰๋„ค์ž„) + SIGNUP_COMPLETE(6) // ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt new file mode 100644 index 00000000..cadbaa02 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/bookmark/BookmarkScreen.kt @@ -0,0 +1,251 @@ +package daily.dayo.presentation.screen.bookmark + +import androidx.compose.foundation.background +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.compose.collectAsLazyPagingItems +import daily.dayo.domain.model.BookmarkPost +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.view.DayoCheckbox +import daily.dayo.presentation.view.FilledRoundedCornerButton +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.viewmodel.BookmarkViewModel +import java.text.DecimalFormat + +@Composable +fun BookmarkScreen( + onBackClick: () -> Unit, + bookmarkViewModel: BookmarkViewModel = hiltViewModel() +) { + val bookmarkUiState by bookmarkViewModel.uiState.collectAsStateWithLifecycle() + val bookmarkPosts = bookmarkUiState.bookmarks.collectAsLazyPagingItems() + + Scaffold( + topBar = { + BookmarkTopNavigation( + isEditMode = bookmarkUiState.isEditMode, + onBackClick = onBackClick, + onCancelClick = { bookmarkViewModel.toggleEditMode() } + ) + }, + bottomBar = { + if (bookmarkUiState.isEditMode) { + FilledRoundedCornerButton( + onClick = { + bookmarkViewModel.deleteSelectedBookmarks() + }, + label = stringResource(id = R.string.delete), + enabled = bookmarkUiState.selectedBookmarks.isNotEmpty(), + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + .height(44.dp) + ) + } + } + ) { contentPadding -> + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize() + .padding(contentPadding) + ) { + BookmarkHeader( + bookmarkCount = bookmarkUiState.count, + isEditMode = bookmarkUiState.isEditMode, + selectedCount = bookmarkUiState.selectedBookmarks.size, + onEditClick = { bookmarkViewModel.toggleEditMode() } + ) + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(bottom = 12.dp, start = 18.dp, end = 18.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(bookmarkPosts.itemCount) { index -> + bookmarkPosts[index]?.let { post -> + BookmarkPostItem( + post = post, + isEditMode = bookmarkUiState.isEditMode, + isSelected = bookmarkUiState.selectedBookmarks.contains(post.postId), + onBookmarkClick = { bookmarkViewModel.toggleSelection(post.postId) } + ) + } + } + } + } + } +} + +@Composable +private fun BookmarkTopNavigation( + isEditMode: Boolean, + onBackClick: () -> Unit, + onCancelClick: () -> Unit +) { + if (isEditMode) { + TopNavigation( + leftIcon = { + Text( + modifier = Modifier + .padding(vertical = 14.dp) + .padding(start = 18.dp, end = 27.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onCancelClick + ), + text = stringResource(id = R.string.cancel), + style = DayoTheme.typography.b6.copy(color = Gray1_50545B), + ) + } + ) + } else { + TopNavigation( + leftIcon = { + IconButton( + onClick = onBackClick, + modifier = Modifier.indication( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back_sign), + contentDescription = "back sign", + tint = Gray1_50545B + ) + } + }, + title = stringResource(id = R.string.bookmark), + titleAlignment = TopNavigationAlign.CENTER + ) + } +} + +@Composable +private fun BookmarkHeader( + bookmarkCount: Int, + selectedCount: Int, + isEditMode: Boolean, + onEditClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .padding(start = 18.dp, end = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + val dec = DecimalFormat("#,###") + Text( + text = " ${dec.format(bookmarkCount)} ", + style = DayoTheme.typography.caption2.copy(Primary_23C882), + ) + Text( + text = stringResource(id = R.string.bookmark_count), + style = DayoTheme.typography.caption2.copy(Gray2_767B83) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + if (isEditMode) { + Text( + modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp), + text = stringResource(id = R.string.bookmark_selected_count, selectedCount), + style = DayoTheme.typography.caption4.copy(color = Gray2_767B83), + ) + } else { + Text( + modifier = Modifier + .padding(vertical = 8.dp, horizontal = 12.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onEditClick + ), + text = stringResource(id = R.string.bookmark_edit), + style = DayoTheme.typography.caption4.copy(color = Gray2_767B83), + ) + } + } + } +} + +@Composable +private fun BookmarkPostItem( + post: BookmarkPost, + isEditMode: Boolean, + isSelected: Boolean, + onBookmarkClick: () -> Unit +) { + Box { + RoundImageView( + context = LocalContext.current, + imageUrl = "${BuildConfig.BASE_URL}/images/${post.thumbnailImage}", + imageDescription = "bookmark post thumbnail", + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) + + if (isEditMode) { + DayoCheckbox( + checked = isSelected, + onCheckedChange = { onBookmarkClick() }, + modifier = Modifier.align(Alignment.TopEnd) + ) + } + } +} + +@Preview +@Composable +private fun PreviewBookmarkHeader() { + BookmarkHeader(0, 0, true, {}) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/feed/FeedNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/feed/FeedNavigation.kt new file mode 100644 index 00000000..2e8ac3a8 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/feed/FeedNavigation.kt @@ -0,0 +1,37 @@ +package daily.dayo.presentation.screen.feed + +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable + +@OptIn(ExperimentalMaterial3Api::class) +fun NavGraphBuilder.feedNavGraph( + snackBarHostState: SnackbarHostState, + onEmptyViewClick: () -> Unit, + onPostClick: (Long) -> Unit, + onProfileClick: (String) -> Unit, + onPostLikeUsersClick: (Long) -> Unit, + onPostHashtagClick: (String) -> Unit, + bottomSheetState: BottomSheetScaffoldState, + bottomSheetContent: (@Composable () -> Unit) -> Unit, +) { + composable(FeedRoute.route) { + FeedScreen( + snackBarHostState = snackBarHostState, + onEmptyViewClick = onEmptyViewClick, + onPostClick = onPostClick, + onProfileClick = onProfileClick, + onPostLikeUsersClick = onPostLikeUsersClick, + onPostHashtagClick = onPostHashtagClick, + bottomSheetState = bottomSheetState, + bottomSheetContent = bottomSheetContent + ) + } +} + +object FeedRoute { + const val route = "feed" +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/feed/FeedScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/feed/FeedScreen.kt new file mode 100644 index 00000000..ccd7d6c3 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/feed/FeedScreen.kt @@ -0,0 +1,173 @@ +package daily.dayo.presentation.screen.feed + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import daily.dayo.presentation.R +import daily.dayo.presentation.screen.home.CategoryMenu +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.view.CategoryHorizontalGroup +import daily.dayo.presentation.view.FeedPostView +import daily.dayo.presentation.view.FilledButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.viewmodel.FeedViewModel + +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) +@Composable +fun FeedScreen( + snackBarHostState: SnackbarHostState, + onEmptyViewClick: () -> Unit, + onPostClick: (Long) -> Unit, + onProfileClick: (String) -> Unit, + onPostLikeUsersClick: (Long) -> Unit, + onPostHashtagClick: (String) -> Unit, + bottomSheetState: BottomSheetScaffoldState, + bottomSheetContent: (@Composable () -> Unit) -> Unit, + feedViewModel: FeedViewModel = hiltViewModel() +) { + val feedPosts = feedViewModel.feedPosts.collectAsLazyPagingItems() + val refreshing by feedViewModel.isRefreshing.collectAsStateWithLifecycle() + val pullRefreshState = rememberPullRefreshState(refreshing, { feedViewModel.loadFeedPosts() }) + + // category + val categoryMenus = listOf( + CategoryMenu.All, + CategoryMenu.Scheduler, + CategoryMenu.StudyPlanner, + CategoryMenu.PocketBook, + CategoryMenu.SixHoleDiary, + CategoryMenu.Digital, + CategoryMenu.ETC + ) + var selectedCategory by remember { mutableStateOf(categoryMenus[0]) } + + LaunchedEffect(selectedCategory) { + feedViewModel.setCurrentCategory(selectedCategory.category) + feedViewModel.loadFeedPosts() + } + + Box( + modifier = Modifier + .fillMaxSize() + .pullRefresh(pullRefreshState) + ) { + Scaffold( + topBar = { + TopNavigation( + leftIcon = { + Text( + text = stringResource(id = R.string.feed), + modifier = Modifier.padding(start = 18.dp), + style = DayoTheme.typography.h1.copy( + color = Dark + ) + ) + } + ) + } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding) + ) { + + CategoryHorizontalGroup( + categoryMenus = categoryMenus, + selectedCategory = selectedCategory, + onCategorySelect = { category -> selectedCategory = category }, + modifier = Modifier.padding(top = 8.dp, bottom = 12.dp) + ) + + // feed post list + LazyColumn( + verticalArrangement = Arrangement.spacedBy(32.dp), + contentPadding = PaddingValues(vertical = 8.dp) + ) { + items( + count = feedPosts.itemCount, + key = feedPosts.itemKey() + ) { index -> + val feedPost = feedPosts[index] + feedPost?.let { post -> + FeedPostView( + post = post, + snackBarHostState = snackBarHostState, + onClickProfile = onProfileClick, + onClickPost = { post.postId?.let { onPostClick(it) } }, + onClickLikePost = { feedViewModel.toggleLikePost(post = post) }, + onClickBookmark = { feedViewModel.toggleBookmarkPost(post = post) }, + onPostLikeUsersClick = onPostLikeUsersClick, + onPostHashtagClick = onPostHashtagClick, + bottomSheetState = bottomSheetState, + bottomSheetContent = bottomSheetContent + ) + } + } + } + + // empty view + if (feedPosts.loadState.refresh is LoadState.NotLoading && feedPosts.itemCount == 0) { + FeedEmptyView(onEmptyViewClick) + } + } + } + + // refresh indicator + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) + } +} + +@Composable +private fun FeedEmptyView(onEmptyViewClick: () -> Unit) { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image(imageVector = ImageVector.vectorResource(id = R.drawable.ic_feed_empty), contentDescription = null) + Spacer(modifier = Modifier.height(20.dp)) + + Text(text = stringResource(id = R.string.feed_empty_title), style = DayoTheme.typography.b3.copy(Gray3_9FA5AE)) + Spacer(modifier = Modifier.height(2.dp)) + Text(text = stringResource(id = R.string.feed_empty_description), style = DayoTheme.typography.caption1.copy(Gray4_C5CAD2)) + + Spacer(modifier = Modifier.height(36.dp)) + FilledButton(onClick = onEmptyViewClick, label = stringResource(id = R.string.feed_empty_button)) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderCreateScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderCreateScreen.kt new file mode 100644 index 00000000..6112b590 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderCreateScreen.kt @@ -0,0 +1,201 @@ +package daily.dayo.presentation.screen.folder + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.domain.model.Privacy +import daily.dayo.presentation.R +import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_DESCRIPTION_MAX_LENGTH +import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_NAME_MAX_LENGTH +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.createLoadingDialog +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.hideLoadingDialog +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.resizeDialogFragment +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.showLoadingDialog +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray5_E8EAEE +import daily.dayo.presentation.view.DayoTextField +import daily.dayo.presentation.view.ToggleButtonWithLabel +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.viewmodel.FolderViewModel + +@Composable +fun FolderCreateScreen( + onBackClick: () -> Unit, + folderViewModel: FolderViewModel = hiltViewModel() +) { + val name = remember { mutableStateOf("") } + val subheading = remember { mutableStateOf("") } + val privacy = remember { mutableStateOf(Privacy.ALL) } + + val context = LocalContext.current + val focusManager = LocalFocusManager.current + val alertDialog = remember { mutableStateOf(createLoadingDialog(context)) } + val createSuccess by folderViewModel.createSuccess.collectAsStateWithLifecycle(false) + + LaunchedEffect(createSuccess) { + if (createSuccess) { + hideLoadingDialog(alertDialog.value) + onBackClick() + } + } + + FolderCreateScreen( + name = name, + subheading = subheading, + privacy = privacy, + onConfirmClick = { + showLoadingDialog(alertDialog.value) + resizeDialogFragment(context, alertDialog.value, 0.8f) + folderViewModel.requestCreateFolder( + name = name.value, + subheading = subheading.value, + privacy = privacy.value, + ) + focusManager.clearFocus() + }, + onBackClick = onBackClick + ) +} + +@Composable +private fun FolderCreateScreen( + name: MutableState, + subheading: MutableState, + privacy: MutableState, + onConfirmClick: () -> Unit, + onBackClick: () -> Unit +) { + Scaffold( + topBar = { + FolderCreateTopNavigation( + confirmEnabled = name.value.isNotEmpty(), + onConfirmClick = onConfirmClick, + onBackClick = onBackClick + ) + }, + ) { contentPadding -> + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize() + .padding(contentPadding) + .padding(horizontal = 18.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + DayoTextField( + value = name.value, + onValueChange = { textValue -> + if (textValue.length > FOLDER_NAME_MAX_LENGTH) + return@DayoTextField + + name.value = textValue + }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = R.string.folder_setting_add_set_title), + placeholder = stringResource(R.string.write_post_folder_new_folder_name_placeholder) + ) + + Spacer(modifier = Modifier.height(28.dp)) + + DayoTextField( + value = subheading.value, + onValueChange = { textValue -> + if (textValue.length > FOLDER_DESCRIPTION_MAX_LENGTH) + return@DayoTextField + + subheading.value = textValue + }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = R.string.folder_setting_add_set_subheading), + placeholder = stringResource(R.string.write_post_folder_new_folder_description_placeholder) + ) + + Spacer(modifier = Modifier.height(40.dp)) + + ToggleButtonWithLabel( + label = stringResource(R.string.write_post_folder_new_folder_privacy_title), + isToggled = privacy.value == Privacy.ONLY_ME, + onToggleChanged = { privacy.value = if (it) Privacy.ONLY_ME else Privacy.ALL } + ) + } + } +} + +@Composable +private fun FolderCreateTopNavigation( + confirmEnabled: Boolean, + onConfirmClick: () -> Unit, + onBackClick: () -> Unit, +) { + TopNavigation( + title = stringResource(id = R.string.folder_add_title), + leftIcon = { + IconButton(onClick = onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_back_sign), + contentDescription = stringResource(id = R.string.back_sign), + tint = Dark + ) + } + }, + rightIcon = { + Text( + modifier = Modifier + .padding(vertical = 12.dp) + .padding(start = 24.dp, end = 18.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + enabled = confirmEnabled, + onClick = onConfirmClick + ), + text = stringResource(id = R.string.confirm), + color = if (confirmEnabled) Dark else Gray5_E8EAEE, + style = DayoTheme.typography.b3, + ) + }, + titleAlignment = TopNavigationAlign.CENTER + ) +} + +@Preview +@Composable +private fun PreviewFolderCreateScreen() { + DayoTheme { + FolderCreateScreen( + name = remember { mutableStateOf("Folder Title") }, + subheading = remember { mutableStateOf("Folder Title") }, + privacy = remember { mutableStateOf(Privacy.ONLY_ME) }, + onConfirmClick = {}, + onBackClick = {} + ) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderEditScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderEditScreen.kt new file mode 100644 index 00000000..dc20598e --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderEditScreen.kt @@ -0,0 +1,230 @@ +package daily.dayo.presentation.screen.folder + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.domain.model.FolderInfo +import daily.dayo.domain.model.Privacy +import daily.dayo.presentation.R +import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_DESCRIPTION_MAX_LENGTH +import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_NAME_MAX_LENGTH +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.createLoadingDialog +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.hideLoadingDialog +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.resizeDialogFragment +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.showLoadingDialog +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray5_E8EAEE +import daily.dayo.presentation.view.DayoTextField +import daily.dayo.presentation.view.ToggleButtonWithLabel +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.viewmodel.FolderViewModel + +@Composable +fun FolderEditScreen( + folderId: Long, + onBackClick: () -> Unit, + folderViewModel: FolderViewModel = hiltViewModel() +) { + val folderUiState by folderViewModel.uiState.collectAsStateWithLifecycle() + val folderInfo = remember { mutableStateOf(null) } + + val context = LocalContext.current + val focusManager = LocalFocusManager.current + val alertDialog = remember { mutableStateOf(createLoadingDialog(context)) } + val editSuccess by folderViewModel.editSuccess.collectAsStateWithLifecycle(false) + + LaunchedEffect(folderId) { + folderViewModel.requestFolderInfo(folderId) + } + + LaunchedEffect(folderUiState) { + folderInfo.value = folderUiState.folderInfo + } + + LaunchedEffect(editSuccess) { + if (editSuccess) { + hideLoadingDialog(alertDialog.value) + onBackClick() + } + } + + FolderEditScreen( + folderInfo = folderInfo, + confirmEnabled = (folderUiState.folderInfo != folderInfo.value) && (folderInfo.value?.name?.isNotEmpty() ?: false), + onConfirmClick = { + showLoadingDialog(alertDialog.value) + resizeDialogFragment(context, alertDialog.value, 0.8f) + + folderInfo.value?.run { + folderViewModel.requestEditFolder( + folderId = folderId, + name = name, + subheading = subheading, + privacy = privacy + ) + } + focusManager.clearFocus() + }, + onBackClick = onBackClick + ) +} + +@Composable +private fun FolderEditScreen( + folderInfo: MutableState, + confirmEnabled: Boolean, + onConfirmClick: () -> Unit, + onBackClick: () -> Unit +) { + Scaffold( + topBar = { + FolderEditTopNavigation( + confirmEnabled = confirmEnabled, + onConfirmClick = onConfirmClick, + onBackClick = onBackClick + ) + }, + ) { contentPadding -> + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize() + .padding(contentPadding) + .padding(horizontal = 18.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + DayoTextField( + value = folderInfo.value?.name ?: "", + onValueChange = { textValue -> + if (textValue.length > FOLDER_NAME_MAX_LENGTH) + return@DayoTextField + + folderInfo.value = folderInfo.value?.copy( + name = textValue + ) + }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = R.string.folder_setting_add_set_title), + placeholder = stringResource(R.string.write_post_folder_new_folder_name_placeholder) + ) + + Spacer(modifier = Modifier.height(28.dp)) + + DayoTextField( + value = folderInfo.value?.subheading ?: "", + onValueChange = { textValue -> + if (textValue.length > FOLDER_DESCRIPTION_MAX_LENGTH) + return@DayoTextField + + folderInfo.value = folderInfo.value?.copy( + subheading = textValue + ) + }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = R.string.folder_setting_add_set_subheading), + placeholder = stringResource(R.string.write_post_folder_new_folder_description_placeholder) + ) + + Spacer(modifier = Modifier.height(40.dp)) + + ToggleButtonWithLabel( + label = stringResource(R.string.write_post_folder_new_folder_privacy_title), + isToggled = folderInfo.value?.privacy == Privacy.ONLY_ME, + onToggleChanged = { + folderInfo.value = folderInfo.value?.copy( + privacy = if (it) Privacy.ONLY_ME else Privacy.ALL + ) + } + ) + } + } +} + +@Composable +private fun FolderEditTopNavigation( + confirmEnabled: Boolean, + onConfirmClick: () -> Unit, + onBackClick: () -> Unit, +) { + TopNavigation( + title = stringResource(id = R.string.folder_edit_title), + leftIcon = { + IconButton(onClick = onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_back_sign), + contentDescription = stringResource(id = R.string.back_sign), + tint = Dark + ) + } + }, + rightIcon = { + Text( + modifier = Modifier + .padding(vertical = 12.dp) + .padding(start = 24.dp, end = 18.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + enabled = confirmEnabled, + onClick = onConfirmClick + ), + text = stringResource(id = R.string.confirm), + color = if (confirmEnabled) Dark else Gray5_E8EAEE, + style = DayoTheme.typography.b3, + ) + }, + titleAlignment = TopNavigationAlign.CENTER + ) +} + +@Preview +@Composable +private fun PreviewFolderEditScreen() { + DayoTheme { + FolderEditScreen( + folderInfo = remember { + mutableStateOf( + FolderInfo( + memberId = "", + name = "Folder Title", + postCount = 27, + privacy = Privacy.ALL, + subheading = "Description", + thumbnailImage = "" + ) + ) + }, + confirmEnabled = false, + onConfirmClick = {}, + onBackClick = {} + ) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderPostMoveScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderPostMoveScreen.kt new file mode 100644 index 00000000..d5658d3c --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderPostMoveScreen.kt @@ -0,0 +1,135 @@ +package daily.dayo.presentation.screen.folder + +import android.widget.Toast +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.flowWithLifecycle +import daily.dayo.domain.model.Folder +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.common.constant.FolderConstants.MAX_FOLDER_COUNT +import daily.dayo.presentation.screen.write.WriteFolderScreen +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.view.FilledRoundedCornerButton +import daily.dayo.presentation.viewmodel.FolderViewModel + +@Composable +internal fun FolderPostMoveScreen( + currentFolderId: Long, + navigateToCreateNewFolder: () -> Unit, + navigateBackToFolder: () -> Unit, + onAdRequest: (onRewardSuccess: () -> Unit) -> Unit, + onBackClick: () -> Unit, + folderViewModel: FolderViewModel = hiltViewModel() +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + val folderListState = folderViewModel.folderList.observeAsState() + val folderList = when (folderListState.value?.status) { + Status.SUCCESS -> folderListState.value?.data ?: emptyList() + else -> emptyList() + } + var selectedFolder by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + folderViewModel.requestAllMyFolderList() + } + + LaunchedEffect(Unit) { + folderViewModel.postMoveSuccess + .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) + .collect { postMoveSuccess -> + if (postMoveSuccess) { + navigateBackToFolder() + } else { + Toast.makeText(context, context.getString(R.string.error_message), Toast.LENGTH_SHORT).show() + } + } + } + + FolderPostMoveScreen( + currentFolderId = currentFolderId, + folders = folderList, + selectedFolder = selectedFolder, + onFolderClick = { folderId, _ -> + selectedFolder = folderId + }, + onPostMoveClick = { + selectedFolder?.let { folderViewModel.moveSelectedPost(it) } + }, + navigateToCreateNewFolder = navigateToCreateNewFolder, + onAdRequest = onAdRequest, + onBackClick = onBackClick + ) +} + +@Composable +private fun FolderPostMoveScreen( + currentFolderId: Long, + folders: List, + selectedFolder: Long?, + onFolderClick: (Long, String) -> Unit, + onPostMoveClick: () -> Unit, + navigateToCreateNewFolder: () -> Unit, + onAdRequest: (onRewardSuccess: () -> Unit) -> Unit, + onBackClick: () -> Unit +) { + Scaffold( + bottomBar = { + Box(modifier = Modifier.padding(20.dp)) { + FilledRoundedCornerButton( + label = stringResource(id = R.string.folder_post_move), + onClick = onPostMoveClick, + modifier = Modifier.height(44.dp), + enabled = selectedFolder != null, + textStyle = DayoTheme.typography.b5 + ) + } + } + ) { contentPadding -> + Box(modifier = Modifier.padding(contentPadding)) { + WriteFolderScreen( + showCreateFolder = folders.size < MAX_FOLDER_COUNT, + onBackClick = onBackClick, + onFolderClick = onFolderClick, + navigateToCreateNewFolder = navigateToCreateNewFolder, + folders = folders.filterNot { it.folderId == currentFolderId }, + currentFolderId = selectedFolder, + onAdRequest = onAdRequest + ) + } + } +} + +@Preview +@Composable +private fun PreviewFolderPostMoveScreen() { + FolderPostMoveScreen( + currentFolderId = 0, + folders = emptyList(), + selectedFolder = null, + onFolderClick = { folderId, folderName -> }, + onPostMoveClick = {}, + navigateToCreateNewFolder = {}, + onAdRequest = {}, + onBackClick = {} + ) +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderScreen.kt new file mode 100644 index 00000000..1ae449bb --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/folder/FolderScreen.kt @@ -0,0 +1,697 @@ +package daily.dayo.presentation.screen.folder + +import android.widget.Toast +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import daily.dayo.domain.model.FolderInfo +import daily.dayo.domain.model.FolderOrder +import daily.dayo.domain.model.FolderPost +import daily.dayo.domain.model.Privacy +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.createLoadingDialog +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.hideLoadingDialog +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.resizeDialogFragment +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.showLoadingDialog +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray5_E8EAEE +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.Gray7_F6F6F7 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.Red_FF4545 +import daily.dayo.presentation.view.DayoCheckbox +import daily.dayo.presentation.view.FilledRoundedCornerButton +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.dialog.ConfirmDialog +import daily.dayo.presentation.viewmodel.FolderUiState +import daily.dayo.presentation.viewmodel.FolderViewModel +import kotlinx.coroutines.flow.flowOf + +@Composable +fun FolderScreen( + folderId: Long, + onPostClick: (Long) -> Unit, + onFolderEditClick: () -> Unit, + onPostMoveClick: () -> Unit, + onWritePostWithFolderClick: () -> Unit, + onBackClick: () -> Unit, + folderViewModel: FolderViewModel = hiltViewModel() +) { + val folderUiState by folderViewModel.uiState.collectAsStateWithLifecycle() + val folderPosts = folderUiState.folderPosts.collectAsLazyPagingItems() + + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + val loadingAlertDialog = remember { mutableStateOf(createLoadingDialog(context)) } + val folderDeleteSuccess by folderViewModel.folderDeleteSuccess.collectAsStateWithLifecycle(false) + var showFolderDeleteAlertDialog by remember { mutableStateOf(false) } + var showPostDeleteAlertDialog by remember { mutableStateOf(false) } + + val optionMenu = listOf( + FolderOptionMenu( + name = stringResource(id = R.string.folder_option_post_edit), + iconRes = R.drawable.ic_menu_post, + color = Dark, + onClickMenu = { + folderViewModel.toggleEditMode() + } + ), + FolderOptionMenu( + name = stringResource(id = R.string.folder_option_edit), + iconRes = R.drawable.ic_menu_folder, + color = Dark, + onClickMenu = onFolderEditClick + ), + FolderOptionMenu( + name = stringResource(id = R.string.folder_option_delete), + iconRes = R.drawable.ic_menu_delete, + color = Red_FF4545, + onClickMenu = { + showFolderDeleteAlertDialog = true + } + ) + ) + + LaunchedEffect(folderId) { + folderViewModel.requestFolderInfo(folderId) + folderViewModel.requestFolderPostList(folderId) + } + + LaunchedEffect(folderDeleteSuccess) { + if (folderDeleteSuccess) { + hideLoadingDialog(loadingAlertDialog.value) + onBackClick() + } + } + + LaunchedEffect(Unit) { + folderViewModel.postDeleteSuccess + .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) + .collect { postDeleteSuccess -> + if (!postDeleteSuccess) { + Toast.makeText(context, context.getString(R.string.error_message), Toast.LENGTH_SHORT).show() + } + folderViewModel.toggleEditMode() + folderViewModel.requestFolderPostList(folderId) + } + } + + FolderScreen( + folderUiState = folderUiState, + folderPosts = folderPosts, + optionMenu = optionMenu, + onPostClick = { postId -> onPostClick(postId) }, + onPostSelect = { postId -> folderViewModel.toggleSelection(postId) }, + onWritePostWithFolderClick = onWritePostWithFolderClick, + onCancelClick = { folderViewModel.toggleEditMode() }, + onPostDeleteClick = { showPostDeleteAlertDialog = true }, + onClickSort = { folderViewModel.toggleFolderOrder(folderId) }, + onPostMoveClick = onPostMoveClick, + onBackClick = onBackClick + ) + + if (showFolderDeleteAlertDialog) { + FolderDeleteAlertDialog( + folderName = folderUiState.folderInfo.name, + onClickConfirm = { + showFolderDeleteAlertDialog = false + showLoadingDialog(loadingAlertDialog.value) + resizeDialogFragment(context, loadingAlertDialog.value, 0.8f) + folderViewModel.requestDeleteFolder(folderId) + }, + onShowChange = { showFolderDeleteAlertDialog = it } + ) + } + + if (showPostDeleteAlertDialog) { + PostDeleteAlertDialog( + selectedCount = folderUiState.selectedPosts.size, + onClickConfirm = { + showPostDeleteAlertDialog = false + // TODO Show Loading + folderViewModel.deletePosts() + folderViewModel.requestDeleteFolder(folderId) + }, + onShowChange = { showPostDeleteAlertDialog = it } + ) + } +} + +@Composable +private fun FolderScreen( + folderUiState: FolderUiState, + folderPosts: LazyPagingItems, + optionMenu: List, + onPostClick: (Long) -> Unit, + onPostSelect: (Long) -> Unit, + onWritePostWithFolderClick: () -> Unit, + onPostDeleteClick: () -> Unit, + onPostMoveClick: () -> Unit, + onClickSort: () -> Unit, + onCancelClick: () -> Unit, + onBackClick: () -> Unit +) { + val optionExpanded = remember { mutableStateOf(false) } + + with(folderUiState) { + Scaffold( + topBar = { + if (isEditMode) { + TopNavigation( + leftIcon = { + androidx.compose.material.Text( + modifier = Modifier + .padding(vertical = 14.dp) + .padding(start = 18.dp, end = 27.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onCancelClick + ), + text = stringResource(id = R.string.cancel), + style = DayoTheme.typography.b6.copy(color = Gray1_50545B), + ) + } + ) + } else { + TopNavigation( + leftIcon = { + IconButton(onClick = onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_back_sign), + contentDescription = stringResource(id = R.string.back_sign), + tint = Dark + ) + } + }, + rightIcon = { + IconButton( + onClick = { optionExpanded.value = optionExpanded.value.not() } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_option_horizontal), + contentDescription = stringResource(id = R.string.folder_option), + tint = Dark + ) + } + + FolderDropdownMenu( + menuItems = optionMenu, + expanded = optionExpanded + ) + } + ) + } + }, + bottomBar = { + if (isEditMode) { + Row( + modifier = Modifier.padding(20.dp) + ) { + FilledRoundedCornerButton( + label = stringResource(R.string.delete), + onClick = onPostDeleteClick, + modifier = Modifier + .weight(1f) + .height(44.dp), + enabled = selectedPosts.isNotEmpty(), + color = ButtonDefaults.buttonColors( + containerColor = Gray5_E8EAEE, + contentColor = Gray2_767B83, + disabledContainerColor = Gray7_F6F6F7, + disabledContentColor = Gray4_C5CAD2 + ) + ) + + Spacer(modifier = Modifier.width(10.dp)) + + FilledRoundedCornerButton( + label = stringResource(R.string.move), + onClick = onPostMoveClick, + modifier = Modifier + .weight(1f) + .height(44.dp), + enabled = selectedPosts.isNotEmpty() + ) + } + } + } + ) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding)) { + if (!isEditMode) { + FolderInformation(folderInfo, onWritePostWithFolderClick) + } + FolderHeader( + postCount = folderInfo.postCount, + selectedCount = selectedPosts.size, + isEditMode = isEditMode, + folderOrder = folderOrder, + onClickSort = onClickSort + ) + FolderContent( + folderUiState = folderUiState, + folderPosts = folderPosts, + onPostClick = onPostClick, + onPostSelect = onPostSelect + ) + } + } + } +} + +@Composable +private fun FolderInformation(folderInfo: FolderInfo, onWritePostWithFolderClick: () -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 18.dp) + .padding(bottom = 16.dp) + ) { + Icon( + painter = when (folderInfo.privacy) { + Privacy.ALL -> painterResource(id = R.drawable.ic_folder_public_my_page) + Privacy.ONLY_ME -> painterResource(id = R.drawable.ic_folder_private_my_page) + }, + contentDescription = when (folderInfo.privacy) { + Privacy.ALL -> stringResource(id = R.string.folder_privacy_public_icon_description) + Privacy.ONLY_ME -> stringResource(id = R.string.folder_privacy_privacy_icon_description) + }, + tint = Color.Unspecified + ) + + Spacer(modifier = Modifier.height(14.dp)) + + Text( + text = folderInfo.name, + color = Dark, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = DayoTheme.typography.h3 + ) + + Spacer(modifier = Modifier.height(2.dp)) + + Text( + text = folderInfo.subheading, + color = Gray1_50545B, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = DayoTheme.typography.b6 + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = onWritePostWithFolderClick, + modifier = Modifier + .fillMaxWidth() + .height(40.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Gray6_F0F1F3, + contentColor = Gray2_767B83 + ), + contentPadding = PaddingValues(vertical = 9.5.dp) + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = stringResource(id = R.string.folder_post_add_icon_description) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = stringResource(id = R.string.folder_post_add), + color = Gray2_767B83, + style = DayoTheme.typography.b5 + ) + } + } +} + +@Composable +private fun FolderDropdownMenu( + menuItems: List, + expanded: MutableState +) { + DropdownMenu( + expanded = expanded.value, + onDismissRequest = { expanded.value = false }, + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .width(140.dp) + ) { + menuItems.forEach { + DropdownMenuItem( + text = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = it.iconRes), + contentDescription = it.name, + tint = it.color + ) + + Text( + text = it.name, + color = it.color, + style = DayoTheme.typography.b6 + ) + } + }, + onClick = { + it.onClickMenu() + expanded.value = false + }, + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 11.5.dp) + ) + } + } +} + +@Composable +private fun FolderHeader( + postCount: Int, + selectedCount: Int, + isEditMode: Boolean, + folderOrder: FolderOrder, + onClickSort: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "$postCount", + color = Primary_23C882, + style = DayoTheme.typography.caption2 + ) + + Text( + text = " ${stringResource(id = R.string.folder_post_count_description)}", + color = Gray2_767B83, + style = DayoTheme.typography.caption2 + ) + + Spacer(modifier = Modifier.weight(1f)) + + if (isEditMode) { + Text( + text = stringResource(id = R.string.folder_post_selected_count, selectedCount), + color = Gray2_767B83, + style = DayoTheme.typography.caption2 + ) + } else { + FolderSortSelector(folderOrder = folderOrder, onClickSort = onClickSort) + } + } +} + +@Composable +private fun FolderSortSelector(folderOrder: FolderOrder, onClickSort: () -> Unit) { + val sortResId = when (folderOrder) { + FolderOrder.NEW -> R.string.folder_post_sort_newest + FolderOrder.OLD -> R.string.folder_post_sort_oldest + } + + Row( + modifier = Modifier.clickableSingle { onClickSort() } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_sort), + contentDescription = stringResource(id = sortResId), + tint = Color.Unspecified + ) + + Text( + text = stringResource(id = sortResId), + color = Gray2_767B83, + style = DayoTheme.typography.caption2 + ) + } +} + +@Composable +private fun FolderContent( + folderUiState: FolderUiState, + folderPosts: LazyPagingItems, + onPostClick: (Long) -> Unit, + onPostSelect: (Long) -> Unit +) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 18.dp, vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(folderPosts.itemCount) { index -> + folderPosts[index]?.let { post -> + FolderPostItem( + post = post, + isEditMode = folderUiState.isEditMode, + isSelected = folderUiState.selectedPosts.contains(post.postId), + onPostClick = onPostClick, + onPostSelect = onPostSelect + ) + } + } + } +} + +@Composable +private fun FolderPostItem( + post: FolderPost, + isEditMode: Boolean, + isSelected: Boolean, + onPostClick: (Long) -> Unit, + onPostSelect: (Long) -> Unit +) { + val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + + Box( + modifier = Modifier.clickable( + interactionSource = interactionSource, + indication = null, + onClick = { + if (isEditMode) { + onPostSelect(post.postId) + } else { + onPostClick(post.postId) + } + } + ) + ) { + RoundImageView( + context = LocalContext.current, + imageUrl = "${BuildConfig.BASE_URL}/images/${post.thumbnailImage}", + imageDescription = "bookmark post thumbnail", + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) + + if (isEditMode) { + DayoCheckbox( + checked = isSelected, + onCheckedChange = { onPostSelect(post.postId) }, + modifier = Modifier.align(Alignment.TopEnd), + interactionSource = interactionSource + ) + } + } +} + +@Composable +private fun FolderDeleteAlertDialog( + folderName: String, + onClickConfirm: () -> Unit, + onShowChange: (Boolean) -> Unit +) { + val folderDeleteDescription = stringResource( + R.string.folder_delete_description_message, + folderName + ) + + val folderDeleteExplanation = stringResource( + R.string.folder_delete_explanation_message, + folderName + ) + + ConfirmDialog( + title = folderDeleteDescription, + description = folderDeleteExplanation, + onClickConfirm = onClickConfirm, + onClickCancel = { onShowChange(false) } + ) +} + +@Composable +private fun PostDeleteAlertDialog( + selectedCount: Int, + onClickConfirm: () -> Unit, + onShowChange: (Boolean) -> Unit +) { + val postDeleteDescription = stringResource( + R.string.folder_post_delete_description_message, + selectedCount + ) + + val postDeleteExplanation = stringResource(R.string.folder_post_delete_explanation_message) + + ConfirmDialog( + title = postDeleteDescription, + description = postDeleteExplanation, + onClickConfirm = onClickConfirm, + onClickCancel = { onShowChange(false) } + ) +} + +@Preview +@Composable +private fun PreviewFolderScreen() { + val folderPostPagingData = PagingData.from( + listOf( + FolderPost("", 0, "") + ) + ) + + val folderUiState = FolderUiState( + folderInfo = FolderInfo( + memberId = "", + name = "Folder Title", + postCount = 27, + privacy = Privacy.ALL, + subheading = "Description", + thumbnailImage = "" + ), + folderPosts = flowOf(folderPostPagingData), + isEditMode = false, + selectedPosts = emptySet() + ) + + DayoTheme { + FolderScreen( + folderUiState = folderUiState, + folderPosts = folderUiState.folderPosts.collectAsLazyPagingItems(), + optionMenu = listOf(), + onPostClick = { }, + onPostSelect = { }, + onWritePostWithFolderClick = { }, + onPostDeleteClick = { }, + onPostMoveClick = { }, + onClickSort = { }, + onCancelClick = { }, + onBackClick = { } + ) + } +} + +@Preview +@Composable +private fun PreviewFolderScreenEditMode() { + val folderPostPagingData = PagingData.from( + listOf( + FolderPost("", 0, "") + ) + ) + + val folderUiState = FolderUiState( + folderInfo = FolderInfo( + memberId = "", + name = "Folder Title", + postCount = 27, + privacy = Privacy.ALL, + subheading = "Description", + thumbnailImage = "" + ), + folderPosts = flowOf(folderPostPagingData), + isEditMode = true, + selectedPosts = emptySet() + ) + + DayoTheme { + FolderScreen( + folderUiState = folderUiState, + folderPosts = folderUiState.folderPosts.collectAsLazyPagingItems(), + optionMenu = listOf(), + onPostClick = { }, + onPostSelect = { }, + onWritePostWithFolderClick = { }, + onPostDeleteClick = { }, + onPostMoveClick = { }, + onClickSort = { }, + onCancelClick = { }, + onBackClick = { } + ) + } +} + +data class FolderOptionMenu( + val name: String, + @DrawableRes val iconRes: Int, + val color: Color, + val onClickMenu: () -> Unit +) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeDayoPickScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeDayoPickScreen.kt new file mode 100644 index 00000000..85dd45d6 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeDayoPickScreen.kt @@ -0,0 +1,201 @@ +package daily.dayo.presentation.screen.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.EmojiView +import daily.dayo.presentation.view.FilledButton +import daily.dayo.presentation.view.HomePostView +import daily.dayo.presentation.viewmodel.HomeViewModel + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun HomeDayoPickScreen( + selectedCategoryName: String, + onPostClick: (Long) -> Unit, + onProfileClick: (String) -> Unit, + onCategoryClick: () -> Unit, + homeViewModel: HomeViewModel +) { + val dayoPickPostList = homeViewModel.dayoPickPostList.observeAsState() + val currentCategory = homeViewModel.currentCategory.collectAsStateWithLifecycle() + val refreshing by homeViewModel.isRefreshing.collectAsStateWithLifecycle() + val pullRefreshState = rememberPullRefreshState(refreshing, { homeViewModel.loadDayoPickPosts() }) + var isEmpty by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(currentCategory.value) { + homeViewModel.loadDayoPickPosts() + } + + Box( + modifier = Modifier + .fillMaxSize() + .pullRefresh(pullRefreshState) + ) { + Column { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 18.dp), + modifier = Modifier.wrapContentHeight() + ) { + // description + item(span = { GridItemSpan(maxLineSpan) }) { + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(top = 8.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row { + EmojiView( + emoji = "\uD83D\uDCA1", + emojiSize = DayoTheme.typography.b6.fontSize, + modifier = Modifier.align(Alignment.CenterVertically) + ) + + Text( + text = stringResource(id = R.string.home_dayopick_description), + style = DayoTheme.typography.b6.copy(Color(0xFF73777C)), + modifier = Modifier + .padding(horizontal = 4.dp) + .align(Alignment.CenterVertically) + ) + } + CategoryButton(selectedCategoryName, onCategoryClick) + } + } + + // dayo pick list + when (dayoPickPostList.value?.status) { + Status.SUCCESS -> { + isEmpty = dayoPickPostList.value?.data?.isEmpty() == true + dayoPickPostList.value?.data?.mapIndexed { index, post -> + item { + HomePostView( + post = post, + isDayoPick = index in 0..4, + modifier = Modifier.padding(bottom = 20.dp), + onClickPost = { post.postId?.let { onPostClick(it) } }, + onClickLikePost = { + post.postId?.let { postId -> + if (!post.heart) { + homeViewModel.requestLikePost(postId, isDayoPickLike = true) + } else { + homeViewModel.requestUnlikePost(postId, isDayoPickLike = true) + } + } + }, + onClickProfile = { post.memberId?.let { onProfileClick(it) } } + ) + } + } + } + + Status.LOADING -> { + } + + Status.ERROR -> { + + } + + else -> {} + } + } + + // empty view + if (isEmpty) HomeDayoPickEmptyView() + } + + // refresh indicator + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) + } +} + +@Composable +private fun HomeDayoPickEmptyView() { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image(imageVector = ImageVector.vectorResource(id = R.drawable.ic_home_empty), contentDescription = null) + Spacer(modifier = Modifier.height(20.dp)) + + Text(text = stringResource(id = R.string.home_dayopick_empty_title), style = DayoTheme.typography.b3.copy(Gray3_9FA5AE)) + Spacer(modifier = Modifier.height(2.dp)) + Text(text = stringResource(id = R.string.home_dayopick_empty_detail), style = DayoTheme.typography.caption1.copy(Gray4_C5CAD2)) + + Spacer(modifier = Modifier.height(28.dp)) + FilledButton(onClick = { /*TODO*/ }, label = stringResource(id = R.string.home_dayopick_empty_action)) + } +} + +@Composable +fun CategoryButton( + selectedCategory: String, + onCategoryClick: () -> Unit +) { + Button( + onClick = onCategoryClick, + shape = RoundedCornerShape(8.dp), + contentPadding = PaddingValues(top = 6.dp, bottom = 6.dp, start = 12.dp, end = 4.dp), + colors = androidx.compose.material3.ButtonDefaults.buttonColors(containerColor = White_FFFFFF, contentColor = Gray2_767B83), + modifier = Modifier.border(1.dp, Gray6_F0F1F3, shape = RoundedCornerShape(8.dp)) + ) { + Text(text = selectedCategory, style = DayoTheme.typography.caption3) + Spacer(modifier = Modifier.width(8.dp)) + Icon(Icons.Filled.ArrowDropDown, "category menu") + } +} + diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeNavigation.kt new file mode 100644 index 00000000..d1db9dc3 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeNavigation.kt @@ -0,0 +1,40 @@ +package daily.dayo.presentation.screen.home + +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import kotlinx.coroutines.CoroutineScope + +fun NavController.navigateHome() { + this.navigate(HomeRoute.route) +} + +@OptIn(ExperimentalMaterial3Api::class) +fun NavGraphBuilder.homeNavGraph( + onPostClick: (Long) -> Unit, + onProfileClick: (String) -> Unit, + onSearchClick: () -> Unit, + coroutineScope: CoroutineScope, + bottomSheetState: BottomSheetScaffoldState, + bottomSheetContent: (@Composable () -> Unit) -> Unit +) { + composable(HomeRoute.route) { + HomeScreen( + onPostClick = onPostClick, + onProfileClick = onProfileClick, + onSearchClick = onSearchClick, + coroutineScope = coroutineScope, + bottomSheetState = bottomSheetState, + bottomSheetContent = bottomSheetContent + ) + } +} + +object HomeRoute { + const val route = "home" +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeNewScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeNewScreen.kt new file mode 100644 index 00000000..259b04c8 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeNewScreen.kt @@ -0,0 +1,171 @@ +package daily.dayo.presentation.screen.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.view.EmojiView +import daily.dayo.presentation.view.FilledButton +import daily.dayo.presentation.view.HomePostView +import daily.dayo.presentation.viewmodel.HomeViewModel + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun HomeNewScreen( + selectedCategoryName: String, + onPostClick: (Long) -> Unit, + onProfileClick: (String) -> Unit, + onCategoryClick: () -> Unit, + homeViewModel: HomeViewModel +) { + val newPostList = homeViewModel.newPostList.observeAsState() + val currentCategory = homeViewModel.currentCategory.collectAsStateWithLifecycle() + val refreshing by homeViewModel.isRefreshing.collectAsStateWithLifecycle() + val pullRefreshState = rememberPullRefreshState(refreshing, { homeViewModel.loadNewPosts() }) + var isEmpty by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(currentCategory.value) { + homeViewModel.loadNewPosts() + } + + Box( + modifier = Modifier + .fillMaxSize() + .pullRefresh(pullRefreshState) + ) { + Column { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 18.dp), + modifier = Modifier.wrapContentHeight() + ) { + // description + item(span = { GridItemSpan(2) }) { + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(top = 8.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row { + EmojiView( + emoji = "\uD83D\uDC40", + emojiSize = DayoTheme.typography.b6.fontSize, + modifier = Modifier.align(Alignment.CenterVertically) + ) + + Text( + text = stringResource(id = R.string.home_new_description), + style = DayoTheme.typography.b6.copy(Color(0xFF73777C)), + modifier = Modifier + .padding(horizontal = 4.dp) + .align(Alignment.CenterVertically) + ) + } + CategoryButton(selectedCategoryName, onCategoryClick) + } + } + + // new post list + when (newPostList.value?.status) { + Status.SUCCESS -> { + isEmpty = newPostList.value?.data?.isEmpty() == true + newPostList.value?.data?.mapIndexed { _, post -> + item { + HomePostView( + post = post, + modifier = Modifier.padding(bottom = 20.dp), + onClickPost = { post.postId?.let { onPostClick(it) } }, + onClickLikePost = { + post.postId?.let { postId -> + if (!post.heart) { + homeViewModel.requestLikePost(postId, isDayoPickLike = false) + } else { + homeViewModel.requestUnlikePost(postId, isDayoPickLike = false) + } + } + }, + onClickProfile = { post.memberId?.let { onProfileClick(it) } } + ) + } + } + } + + Status.LOADING -> { + } + + Status.ERROR -> { + + } + + else -> {} + } + } + + // empty view + if (isEmpty) HomeNewEmptyView() + } + + // refresh indicator + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) + } +} + +@Composable +private fun HomeNewEmptyView() { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image(imageVector = ImageVector.vectorResource(id = R.drawable.ic_home_empty), contentDescription = null) + Spacer(modifier = Modifier.height(20.dp)) + + Text(text = stringResource(id = R.string.home_new_empty_title), style = DayoTheme.typography.b3.copy(Gray3_9FA5AE)) + Spacer(modifier = Modifier.height(2.dp)) + Text(text = stringResource(id = R.string.home_new_empty_detail), style = DayoTheme.typography.caption1.copy(Gray4_C5CAD2)) + + Spacer(modifier = Modifier.height(28.dp)) + FilledButton(onClick = { /*TODO*/ }, label = stringResource(id = R.string.home_new_empty_action)) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeScreen.kt new file mode 100644 index 00000000..159e0dab --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/home/HomeScreen.kt @@ -0,0 +1,216 @@ +package daily.dayo.presentation.screen.home + +import androidx.activity.compose.BackHandler +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import daily.dayo.domain.model.Category +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray5_E8EAEE +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.view.DayoTextButton +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.dialog.BottomSheetDialog +import daily.dayo.presentation.view.dialog.getBottomSheetDialogState +import daily.dayo.presentation.viewmodel.HomeViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +const val HOME_DAYOPICK_PAGE_TAB_ID = 0 +const val HOME_NEW_PAGE_TAB_ID = 1 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen( + onPostClick: (Long) -> Unit, + onProfileClick: (String) -> Unit, + onSearchClick: () -> Unit, + coroutineScope: CoroutineScope, + bottomSheetState: BottomSheetScaffoldState, + bottomSheetContent: (@Composable () -> Unit) -> Unit, + homeViewModel: HomeViewModel = hiltViewModel() +) { + var homeTabState by rememberSaveable { mutableIntStateOf(HOME_DAYOPICK_PAGE_TAB_ID) } + var selectedCategory by rememberSaveable { mutableStateOf(Pair(CategoryMenu.All.name, 0)) } // name, index + val onCategoryClick: () -> Unit = { + coroutineScope.launch { bottomSheetState.bottomSheetState.expand() } + } + + val onCategorySelect: (CategoryMenu, Int) -> Unit = { categoryMenu, index -> + selectedCategory = Pair(categoryMenu.name, index) + homeViewModel.setCategory(categoryMenu.category) + coroutineScope.launch { bottomSheetState.bottomSheetState.hide() } + } + + BackHandler(enabled = bottomSheetState.bottomSheetState.isVisible) { + coroutineScope.launch { bottomSheetState.bottomSheetState.hide() } + } + + Scaffold( + topBar = { + TopNavigation( + leftIcon = { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(start = 18.dp) + ) { + DayoTextButton( + onClick = { + homeTabState = HOME_DAYOPICK_PAGE_TAB_ID + }, + text = stringResource(id = R.string.DayoPick), + textStyle = DayoTheme.typography.h3.copy( + color = if (homeTabState == HOME_DAYOPICK_PAGE_TAB_ID) Dark else Gray5_E8EAEE, + fontWeight = FontWeight.ExtraBold + ) + ) + + DayoTextButton( + onClick = { + homeTabState = HOME_NEW_PAGE_TAB_ID + }, + text = stringResource(id = R.string.New), + textStyle = DayoTheme.typography.h3.copy( + color = if (homeTabState == HOME_NEW_PAGE_TAB_ID) Dark else Gray5_E8EAEE, + fontWeight = FontWeight.ExtraBold + ) + ) + } + }, + rightIcon = { + NoRippleIconButton( + onClick = { onSearchClick() }, + iconContentDescription = "search button", + iconPainter = painterResource(id = R.drawable.ic_search), + iconModifier = Modifier + .padding(end = 12.dp) + .size(24.dp), + iconTintColor = Dark + ) + } + ) + } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding) + ) { + when (homeTabState) { + HOME_DAYOPICK_PAGE_TAB_ID -> { + HomeDayoPickScreen( + selectedCategory.first, + onPostClick, + onProfileClick, + onCategoryClick, + homeViewModel + ) + } + + HOME_NEW_PAGE_TAB_ID -> { + HomeNewScreen( + selectedCategory.first, + onPostClick, + onProfileClick, + onCategoryClick, + homeViewModel + ) + } + } + } + } + + bottomSheetContent { + CategoryBottomSheetDialog(onCategorySelect, selectedCategory, coroutineScope, bottomSheetState) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CategoryBottomSheetDialog( + onCategorySelected: (CategoryMenu, Int) -> Unit, + selectedCategory: Pair, + coroutineScope: CoroutineScope, + bottomSheetState: BottomSheetScaffoldState +) { + val categoryMenus = listOf( + CategoryMenu.All, + CategoryMenu.Scheduler, + CategoryMenu.StudyPlanner, + CategoryMenu.PocketBook, + CategoryMenu.SixHoleDiary, + CategoryMenu.Digital, + CategoryMenu.ETC + ) + + BottomSheetDialog( + sheetState = bottomSheetState, + buttons = categoryMenus.mapIndexed { index, category -> + Pair(category.name) { + onCategorySelected(category, index) + } + }, + title = stringResource(id = R.string.filter), + leftIconButtons = categoryMenus.map { + ImageVector.vectorResource(it.defaultIcon) + }, + leftIconCheckedButtons = categoryMenus.map { + ImageVector.vectorResource(it.checkedIcon) + }, + normalColor = Gray2_767B83, + checkedColor = Primary_23C882, + checkedButtonIndex = selectedCategory.second, + closeButtonAction = { coroutineScope.launch { bottomSheetState.bottomSheetState.hide() } } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@Preview(showBackground = true) +private fun PreviewHomeScreen() { + DayoTheme { + HomeScreen( + onPostClick = {}, + onProfileClick = {}, + onSearchClick = {}, + coroutineScope = rememberCoroutineScope(), + bottomSheetState = getBottomSheetDialogState(), + bottomSheetContent = {}, + ) + } +} + + +sealed class CategoryMenu(val name: String, @DrawableRes val defaultIcon: Int, @DrawableRes val checkedIcon: Int, val category: Category) { + object All : CategoryMenu("์ „์ฒด", R.drawable.ic_category_all, R.drawable.ic_category_all_checked, Category.ALL) + object Scheduler : CategoryMenu("์Šค์ผ€์ค„๋Ÿฌ", R.drawable.ic_category_scheduler, R.drawable.ic_category_scheduler_checked, Category.SCHEDULER) + object StudyPlanner : CategoryMenu("์Šคํ„ฐ๋”” ํ”Œ๋ž˜๋„ˆ", R.drawable.ic_category_studyplanner, R.drawable.ic_category_studyplanner_checked, Category.STUDY_PLANNER) + object PocketBook : CategoryMenu("ํฌ์ผ“๋ถ", R.drawable.ic_category_pocketbook, R.drawable.ic_category_pocketbook_checked, Category.POCKET_BOOK) + object SixHoleDiary : CategoryMenu("6๊ณต ๋‹ค์ด์–ด๋ฆฌ", R.drawable.ic_category_sixholediary, R.drawable.ic_category_sixholediary_checked, Category.SIX_DIARY) + object Digital : CategoryMenu("๋ชจ๋ฐ”์ผ ๋‹ค์ด์–ด๋ฆฌ", R.drawable.ic_category_digital, R.drawable.ic_category_digital_checked, Category.GOOD_NOTE) + object ETC : CategoryMenu("๊ธฐํƒ€", R.drawable.ic_category_etc, R.drawable.ic_category_etc_checked, Category.ETC) +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/main/MainNavigator.kt b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainNavigator.kt new file mode 100644 index 00000000..f74bd775 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainNavigator.kt @@ -0,0 +1,235 @@ +package daily.dayo.presentation.screen.main + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavDestination +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import daily.dayo.presentation.screen.home.HomeRoute +import daily.dayo.presentation.screen.home.navigateHome +import daily.dayo.presentation.screen.mypage.navigateBackToFolder +import daily.dayo.presentation.screen.mypage.navigateBlockedUsers +import daily.dayo.presentation.screen.mypage.navigateBookmark +import daily.dayo.presentation.screen.mypage.navigateFolder +import daily.dayo.presentation.screen.mypage.navigateFolderCreate +import daily.dayo.presentation.screen.mypage.navigateFolderEdit +import daily.dayo.presentation.screen.mypage.navigateFolderPostMove +import daily.dayo.presentation.screen.mypage.navigateFollowMenu +import daily.dayo.presentation.screen.mypage.navigateMyPage +import daily.dayo.presentation.screen.mypage.navigateProfileEdit +import daily.dayo.presentation.screen.notice.navigateNotices +import daily.dayo.presentation.screen.notice.navigateNoticeDetail +import daily.dayo.presentation.screen.post.navigatePost +import daily.dayo.presentation.screen.post.navigatePostLikeUsers +import daily.dayo.presentation.screen.profile.navigateProfile +import daily.dayo.presentation.screen.rules.RuleType +import daily.dayo.presentation.screen.rules.navigateRules +import daily.dayo.presentation.screen.search.navigateSearch +import daily.dayo.presentation.screen.search.navigateSearchPostHashtag +import daily.dayo.presentation.screen.search.navigateSearchResult +import daily.dayo.presentation.screen.settings.navigateChangePassword +import daily.dayo.presentation.screen.settings.navigateInformation +import daily.dayo.presentation.screen.settings.navigateSettings +import daily.dayo.presentation.screen.settings.navigateSettingsNotification +import daily.dayo.presentation.screen.settings.navigateWithdraw +import daily.dayo.presentation.screen.write.navigatePostEdit +import daily.dayo.presentation.screen.write.navigateToWritePostWithFolder +import daily.dayo.presentation.screen.write.navigateWrite +import daily.dayo.presentation.screen.write.navigateWriteFolder +import daily.dayo.presentation.screen.write.navigateWriteFolderNew +import daily.dayo.presentation.screen.write.navigateWriteTag + +class MainNavigator( + val navController: NavHostController, +) { + private val currentDestination: NavDestination? + @Composable get() = navController + .currentBackStackEntryAsState().value?.destination + + fun navigateHome() { + navController.navigateHome() + } + + fun navigateToBottomTab(route: String) { + navController.navigate(route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + + fun navigateToBottomTabWithClearStack(route: String) { + // ์ „์ฒด ๋ฐฑ์Šคํƒ์„ ์ •๋ฆฌํ•˜๊ณ  ํƒญ ์ด๋™ + + // ํ˜„์žฌ destination๊ณผ ๋‹ค๋ฅธ ๊ฒฝ์šฐ์—๋งŒ navigation ์ˆ˜ํ–‰ + if (navController.currentDestination?.route != route) { + navController.popBackStack(navController.graph.findStartDestination().id, false) + navController.navigate(route) { + popUpTo(navController.graph.findStartDestination().id) { + inclusive = false + } + launchSingleTop = true + } + } + } + + fun navigatePost(postId: Long) { + navController.navigatePost(postId = postId) + } + + fun navigateProfile(currentMemberId: String?, memberId: String) { + if (currentMemberId != null) { + if (currentMemberId == memberId) { + navController.navigateMyPage() + } else { + navController.navigateProfile(memberId) + } + } + } + + fun navigateSearch() { + navController.navigateSearch() + } + + fun navigateSearchResult(searchKeyword: String) { + navController.navigateSearchResult(searchKeyword) + } + + fun navigateSearchPostHashtag(hashtag: String) { + navController.navigateSearchPostHashtag(hashtag = hashtag) + } + + fun navigateNotices() { + navController.navigateNotices() + } + + fun navigateNoticeDetail(noticeId: Long) { + navController.navigateNoticeDetail(noticeId = noticeId) + } + + fun navigateInformation() { + navController.navigateInformation() + } + + fun navigateRules(ruleType: RuleType) { + navController.navigateRules(ruleType) + } + + fun navigateSettings() { + navController.navigateSettings() + } + + fun navigateChangePassword() { + navController.navigateChangePassword() + } + + fun navigateSettingsNotification() { + navController.navigateSettingsNotification() + } + + fun navigateProfileEdit() { + navController.navigateProfileEdit() + } + + fun navigateWithdraw() { + navController.navigateWithdraw() + } + + fun navigateBlockedUsers() { + navController.navigateBlockedUsers() + } + + fun navigateBookmark() { + navController.navigateBookmark() + } + + fun navigateFolderCreate() { + navController.navigateFolderCreate() + } + + fun navigateFolderEdit(folderId: Long) { + navController.navigateFolderEdit(folderId) + } + + fun navigateFolderPostMove(folderId: Long) { + navController.navigateFolderPostMove(folderId) + } + + fun navigateFollowMenu(memberId: String, tabNum: Int) { + navController.navigateFollowMenu(memberId, tabNum) + } + + fun navigateFolder(folderId: Long) { + navController.navigateFolder(folderId) + } + + fun navigateBackToFolder(folderId: Long) { + navController.navigateBackToFolder(folderId) + } + + fun navigateWrite() { + navController.navigateWrite() + } + + fun navigatePostEdit(postId: Long) { + navController.navigatePostEdit(postId) + } + + fun navigateToWritePostWithFolder(folderId: Long) { + navController.navigateToWritePostWithFolder(folderId) + } + + fun navigateWriteTag() { + navController.navigateWriteTag() + } + + fun navigateWriteFolder() { + navController.navigateWriteFolder() + } + + fun navigateWriteFolderNew() { + navController.navigateWriteFolderNew() + } + + fun popBackStack() { + navController.popBackStack() + } + + fun popBackStackIfNotHome() { + if (!isSameCurrentDestination(HomeRoute.route)) { + navController.popBackStack() + } + } + + private fun isSameCurrentDestination(route: String) = + navController.currentDestination?.route == route + + fun navigatePostLikeUsers(postId: Long) { + navController.navigatePostLikeUsers(postId = postId) + } + + fun navigateUp() { + navController.navigateUp() + } + + @Composable + fun shouldShowBottomBar(): Boolean { + val currentRoute = currentDestination?.route ?: return false + return currentRoute in Screen + } + + fun isWriteGraph(): Boolean { + return navController.currentDestination?.route?.startsWith("write") == true + } +} + +@Composable +internal fun rememberMainNavigator( + navController: NavHostController = rememberNavController(), +): MainNavigator = remember(navController) { + MainNavigator(navController) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt new file mode 100644 index 00000000..49a43dc1 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/main/MainScreen.kt @@ -0,0 +1,402 @@ +package daily.dayo.presentation.screen.main + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.BottomNavigation +import androidx.compose.material.BottomNavigationItem +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetValue +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import daily.dayo.presentation.R +import daily.dayo.presentation.screen.feed.FeedRoute +import daily.dayo.presentation.screen.feed.feedNavGraph +import daily.dayo.presentation.screen.home.HomeRoute +import daily.dayo.presentation.screen.home.homeNavGraph +import daily.dayo.presentation.screen.mypage.MyPageRoute +import daily.dayo.presentation.screen.mypage.myPageNavGraph +import daily.dayo.presentation.screen.notice.noticeNavGraph +import daily.dayo.presentation.screen.notification.NotificationRoute +import daily.dayo.presentation.screen.notification.notificationNavGraph +import daily.dayo.presentation.screen.post.postNavGraph +import daily.dayo.presentation.screen.profile.profileNavGraph +import daily.dayo.presentation.screen.search.searchNavGraph +import daily.dayo.presentation.screen.settings.settingsNavGraph +import daily.dayo.presentation.screen.write.WriteRoute +import daily.dayo.presentation.screen.write.writeNavGraph +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray7_F6F6F7 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.dialog.getBottomSheetDialogState +import daily.dayo.presentation.viewmodel.NoticeViewModel +import daily.dayo.presentation.viewmodel.ProfileViewModel +import kotlinx.coroutines.launch + +@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class) +@Composable +internal fun MainScreen( + onAdRequest: (onRewardSuccess: () -> Unit) -> Unit, + navigator: MainNavigator = rememberMainNavigator(), + profileViewModel: ProfileViewModel = hiltViewModel() +) { + val currentMemberId = profileViewModel.currentMemberId + val coroutineScope = rememberCoroutineScope() + val noticeViewModel = hiltViewModel() + + val snackBarHostState = remember { SnackbarHostState() } + var bottomSheetContent by remember { mutableStateOf<(@Composable () -> Unit)?>(null) } + val bottomSheetState = getBottomSheetDialogState() + val bottomSheetDimAlpha by remember { + derivedStateOf { if (bottomSheetState.bottomSheetState.currentValue == SheetValue.Expanded) 0.6f else 0f } + } + val animatedDimAlpha by animateFloatAsState(targetValue = bottomSheetDimAlpha) + + SharedTransitionLayout { + BottomSheetScaffold( + scaffoldState = bottomSheetState, + sheetDragHandle = null, + sheetContent = { + Box(modifier = Modifier.navigationBarsPadding()) { + bottomSheetContent?.invoke() + } + }, + sheetPeekHeight = 0.dp, + snackbarHost = { + SnackbarHost( + hostState = snackBarHostState, + modifier = Modifier.navigationBarsPadding() + ) + } + ) { + Box { + Column(modifier = Modifier.navigationBarsPadding()) { + NavHost( + modifier = Modifier.weight(1f), + navController = navigator.navController, + startDestination = Screen.Home.route, + ) { + homeNavGraph( + onPostClick = { navigator.navigatePost(it) }, + onProfileClick = { memberId -> + navigator.navigateProfile( + currentMemberId, + memberId + ) + }, + onSearchClick = { navigator.navigateSearch() }, + coroutineScope = coroutineScope, + bottomSheetState = bottomSheetState, + bottomSheetContent = { content -> + bottomSheetContent = content + }, + ) + feedNavGraph( + snackBarHostState = snackBarHostState, + onEmptyViewClick = { navigator.navigateHome() }, + onPostClick = { navigator.navigatePost(it) }, + onProfileClick = { memberId -> + navigator.navigateProfile( + currentMemberId, + memberId + ) + }, + onPostLikeUsersClick = { navigator.navigatePostLikeUsers(it) }, + onPostHashtagClick = { navigator.navigateSearchPostHashtag(it) }, + bottomSheetState = bottomSheetState, + bottomSheetContent = { content -> bottomSheetContent = content } + ) + postNavGraph( + snackBarHostState = snackBarHostState, + onPostEditClick = { navigator.navigatePostEdit(it) }, + onProfileClick = { memberId -> + navigator.navigateProfile( + currentMemberId, + memberId + ) + }, + onPostLikeUsersClick = { navigator.navigatePostLikeUsers(it) }, + onPostHashtagClick = { navigator.navigateSearchPostHashtag(it) }, + onBackClick = { navigator.popBackStack() } + ) + searchNavGraph( + onBackClick = { navigator.popBackStack() }, + onSearch = { navigator.navigateSearchResult(it) }, + onPostClick = { navigator.navigatePost(it) }, + onProfileClick = { memberId -> + navigator.navigateProfile( + currentMemberId, + memberId + ) + }, + navController = navigator.navController, + ) + writeNavGraph( + snackBarHostState = snackBarHostState, + navController = navigator.navController, + navigateToWritePost = { navigator.navigatePost(it) }, + onBackClick = { navigator.navigateUp() }, + onTagClick = { navigator.navigateWriteTag() }, + onWriteFolderClick = { navigator.navigateWriteFolder() }, + onWriteFolderNewClick = { navigator.navigateWriteFolderNew() }, + onAdRequest = onAdRequest, + bottomSheetState = bottomSheetState, + bottomSheetContent = { content -> + bottomSheetContent = content + } + ) + myPageNavGraph( + navController = navigator.navController, + onBackClick = { navigator.popBackStack() }, + onSettingsClick = { navigator.navigateSettings() }, + onFollowButtonClick = { memberId, tabNum -> + navigator.navigateFollowMenu( + memberId, + tabNum + ) + }, + onProfileClick = { memberId -> + navigator.navigateProfile( + currentMemberId, + memberId + ) + }, + onProfileEditClick = { navigator.navigateProfileEdit() }, + onBookmarkClick = { navigator.navigateBookmark() }, + onFolderClick = { folderId -> navigator.navigateFolder(folderId) }, + onFolderCreateClick = { navigator.navigateFolderCreate() }, + onFolderEditClick = { folderId -> + navigator.navigateFolderEdit( + folderId + ) + }, + onWritePostWithFolderClick = { folderId -> + navigator.navigateToWritePostWithFolder( + folderId + ) + }, + onPostClick = { postId -> navigator.navigatePost(postId) }, + onPostMoveClick = { folderId -> + navigator.navigateFolderPostMove( + folderId + ) + }, + onAdRequest = onAdRequest, + navigateBackToFolder = { folderId -> + navigator.navigateBackToFolder( + folderId + ) + } + ) + profileNavGraph( + snackBarHostState = snackBarHostState, + onFollowMenuClick = { memberId, tabNum -> + navigator.navigateFollowMenu( + memberId, + tabNum + ) + }, + onFolderClick = { folderId -> navigator.navigateFolder(folderId) }, + onPostClick = { postId -> navigator.navigatePost(postId) }, + onBackClick = { navigator.popBackStack() } + ) + notificationNavGraph( + onPostClick = { navigator.navigatePost(it) }, + onProfileClick = { memberId -> + navigator.navigateProfile( + currentMemberId, + memberId + ) + }, + onNoticeClick = { noticeId -> + navigator.navigateNoticeDetail( + noticeId + ) + }, + ) + settingsNavGraph( + coroutineScope = coroutineScope, + snackBarHostState = snackBarHostState, + onProfileEditClick = { navigator.navigateProfileEdit() }, + onWithdrawClick = { navigator.navigateWithdraw() }, + onBlockUsersClick = { navigator.navigateBlockedUsers() }, + onPasswordChangeClick = { navigator.navigateChangePassword() }, + onSettingNotificationClick = { navigator.navigateSettingsNotification() }, + onNoticesClick = { navigator.navigateNotices() }, + onInformationClick = { navigator.navigateInformation() }, + onRulesClick = { ruleType -> navigator.navigateRules(ruleType) }, + onBackClick = { navigator.popBackStack() }, + bottomSheetState = bottomSheetState, + bottomSheetContent = { content -> + bottomSheetContent = content + }, + onNavigateToHome = { navigator.navigateToBottomTabWithClearStack(Screen.Home.route) }, + onNavigateToMyPage = { navigator.navigateToBottomTabWithClearStack(Screen.MyPage.route) }, + ) + noticeNavGraph( + noticeViewModel = noticeViewModel, + onBackClick = { navigator.popBackStack() }, + onNoticeDetailClick = { noticeId -> + navigator.navigateNoticeDetail( + noticeId + ) + }, + sharedTransitionScope = this@SharedTransitionLayout, + ) + } + + // bottom navigation + MainBottomNavigation( + visible = navigator.shouldShowBottomBar(), + navController = navigator.navController, + modifier = Modifier.fillMaxWidth() + ) + } + + if (animatedDimAlpha > 0f) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Dark.copy(alpha = animatedDimAlpha)) + .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { + coroutineScope.launch { bottomSheetState.bottomSheetState.hide() } + } + ) + } + } + } + } +} + +@Composable +fun MainBottomNavigation( + visible: Boolean, + navController: NavController, + modifier: Modifier +) { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + val items = listOf( + Screen.Home, + Screen.Feed, + Screen.Write, + Screen.Notification, + Screen.MyPage + ) + + if (visible) { + Column { + Divider(color = Gray7_F6F6F7, thickness = 1.dp) + + BottomNavigation( + backgroundColor = White_FFFFFF, + contentColor = Gray2_767B83, + elevation = 0.dp, + modifier = modifier + ) { + items.forEach { screen -> + val selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true + BottomNavigationItem( + icon = { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = ImageVector.vectorResource(id = if (selected) screen.selectedIcon else screen.defaultIcon), + contentDescription = stringResource(id = screen.resourceId), + modifier = Modifier.size(if (screen.route != Screen.Write.route) 24.dp else 36.dp) + ) + + Spacer(Modifier.height(2.dp)) + + if (screen.route != Screen.Write.route) { + Text(text = stringResource(screen.resourceId), style = DayoTheme.typography.caption5) + } + } + }, + modifier = Modifier.padding(vertical = 8.dp), + selected = selected, + selectedContentColor = Dark, + onClick = { + navController.navigate(screen.route) { + if (screen.route != Screen.Write.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + } + launchSingleTop = + if (screen.route == Screen.Write.route) false else true + restoreState = true + } + } + ) + } + } + } + } +} + +sealed class Screen(val route: String, @StringRes val resourceId: Int, @DrawableRes val defaultIcon: Int, @DrawableRes val selectedIcon: Int) { + object Home : Screen(HomeRoute.route, R.string.home, R.drawable.ic_home, R.drawable.ic_home_filled) + object Feed : Screen(FeedRoute.route, R.string.feed, R.drawable.ic_feed, R.drawable.ic_feed_filled) + object Write : Screen(WriteRoute.route, R.string.write, R.drawable.ic_write, R.drawable.ic_write_filled) + object Notification : Screen(NotificationRoute.route, R.string.notification, R.drawable.ic_notification, R.drawable.ic_notification_filled) + object MyPage : Screen(MyPageRoute.route, R.string.my_page, R.drawable.ic_my_page, R.drawable.ic_my_page_filled) + + companion object { + operator fun contains(route: String): Boolean { + return listOf(Home, Feed, Notification, MyPage).map { it.route }.contains(route) + } + } +} + +@Preview +@Composable +private fun PreviewMainBottomNavigation() { + MainBottomNavigation( + visible = true, + navController = rememberNavController(), + modifier = Modifier + ) +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/FollowScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/FollowScreen.kt new file mode 100644 index 00000000..23ac7635 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/FollowScreen.kt @@ -0,0 +1,465 @@ +package daily.dayo.presentation.screen.mypage + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.White +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import daily.dayo.domain.model.Follow +import daily.dayo.domain.model.Profile +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.view.DayoOutlinedButton +import daily.dayo.presentation.view.FilledButton +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.viewmodel.FollowUiState +import daily.dayo.presentation.viewmodel.FollowViewModel +import daily.dayo.presentation.viewmodel.ProfileViewModel +import kotlinx.coroutines.launch + +@Composable +internal fun FollowScreen( + memberId: String, + tabNum: Int, + onProfileClick: (String) -> Unit, + onBackClick: () -> Unit, + profileViewModel: ProfileViewModel = hiltViewModel(), + followViewModel: FollowViewModel = hiltViewModel() +) { + val currentMemberId = profileViewModel.currentMemberId + val profileInfo by profileViewModel.profileInfo.observeAsState() + val followerUiState by followViewModel.followerUiState.observeAsState(FollowUiState.Loading) + val followingUiState by followViewModel.followingUiState.observeAsState(FollowUiState.Loading) + val pagerState = rememberPagerState(initialPage = tabNum, pageCount = { 2 }) + val onFollowClick: (Follow) -> Unit = { follow -> + followViewModel.toggleFollow(follow, pagerState.currentPage == FOLLOWER_TAB_ID) + } + + LaunchedEffect(memberId) { + profileViewModel.requestOtherProfile(memberId) + // ํŒ”๋กœ์›Œ, ํŒ”๋กœ์ž‰ ์ˆ˜๋ฅผ ์•Œ๊ธฐ ์œ„ํ•จ, pagerState์— ๋”ฐ๋ฅธ ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์ดˆ๊ธฐ ํƒญ์ด ์•„๋‹Œ ์ •๋ณด๋งŒ ํ˜ธ์ถœ + when (tabNum) { + FOLLOWER_TAB_ID -> followViewModel.requestFollowingList(memberId) + FOLLOWING_TAB_ID -> followViewModel.requestFollowerList(memberId) + } + } + + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.currentPage } + .collect { page -> + when (page) { + FOLLOWER_TAB_ID -> followViewModel.requestFollowerList(memberId) + FOLLOWING_TAB_ID -> followViewModel.requestFollowingList(memberId) + } + } + } + + if (currentMemberId != null) { + FollowScreen( + currentMemberId, + profileInfo?.data, + followerUiState, + followingUiState, + pagerState, + onProfileClick, + onFollowClick, + onBackClick + ) + } +} + +@Composable +private fun FollowScreen( + currentMemberId: String, + profileInfo: Profile?, + followerUiState: FollowUiState, + followingUiState: FollowUiState, + pagerState: PagerState, + onProfileClick: (String) -> Unit, + onFollowClick: (Follow) -> Unit, + onBackClick: () -> Unit +) { + Scaffold( + topBar = { + TopNavigation( + leftIcon = { + IconButton( + onClick = onBackClick, + modifier = Modifier.indication( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back_sign), + contentDescription = stringResource(id = R.string.back_sign), + tint = Gray1_50545B + ) + } + }, + title = profileInfo?.nickname ?: "", + titleAlignment = TopNavigationAlign.CENTER + ) + }, + containerColor = White + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding), + ) { + val followerCount = when (followerUiState) { + is FollowUiState.Success -> followerUiState.count + is FollowUiState.Loading, + is FollowUiState.Error -> 0 + } + + val followingCount = when (followingUiState) { + is FollowUiState.Success -> followingUiState.count + is FollowUiState.Loading, + is FollowUiState.Error -> 0 + } + + FollowTab(pagerState, followerCount, followingCount) + + HorizontalPager( + state = pagerState, + flingBehavior = PagerDefaults.flingBehavior(state = pagerState), + pageContent = { page -> + when (page) { + FOLLOWER_TAB_ID -> FollowContent(page, currentMemberId, followerUiState, onProfileClick, onFollowClick) + FOLLOWING_TAB_ID -> FollowContent(page, currentMemberId, followingUiState, onProfileClick, onFollowClick) + } + } + ) + } + } +} + +@Composable +private fun FollowContent( + tabNum: Int, + currentMemberId: String, + followUiState: FollowUiState, + onProfileClick: (String) -> Unit, + onFollowClick: (Follow) -> Unit +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(DayoTheme.colorScheme.background), + contentPadding = PaddingValues(vertical = 20.dp, horizontal = 18.dp) + ) { + when (followUiState) { + is FollowUiState.Loading -> { + + } + + is FollowUiState.Success -> { + if (followUiState.data.isEmpty()) { + item { + Box( + modifier = Modifier.fillParentMaxSize(), + contentAlignment = Alignment.Center + ) { + FollowerEmpty(tabNum) + } + } + } else { + items(items = followUiState.data, key = { it.memberId }) { follow -> + FollowUserInfo(follow, follow.memberId == currentMemberId, onProfileClick, onFollowClick) + } + } + } + + is FollowUiState.Error -> { + item { + Box( + modifier = Modifier.fillParentMaxSize(), + contentAlignment = Alignment.Center + ) { + FollowerEmpty(tabNum) + } + } + } + } + } +} + +@Composable +private fun FollowerEmpty(tabNum: Int) { + Column( + modifier = Modifier.fillMaxSize(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Bottom + ) { + Image( + painter = painterResource(id = R.drawable.ic_follow_empty), + contentDescription = stringResource(id = R.string.follower_empty_description) + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = stringResource( + if (tabNum == FOLLOWER_TAB_ID) R.string.follower_empty_description + else R.string.following_empty_description + ), + modifier = Modifier.padding(bottom = 2.dp), + color = Gray3_9FA5AE, + textAlign = TextAlign.Center, + overflow = TextOverflow.Visible, + style = DayoTheme.typography.b3, + ) + + Text( + text = stringResource( + if (tabNum == FOLLOWER_TAB_ID) R.string.follower_empty_sub_description + else R.string.following_empty_sub_description + ), + color = Gray4_C5CAD2, + textAlign = TextAlign.Center, + overflow = TextOverflow.Visible, + style = DayoTheme.typography.caption2, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + } +} + +@Composable +private fun FollowUserInfo( + follow: Follow, + isMine: Boolean, + onProfileClick: (String) -> Unit, + onFollowClick: (Follow) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + val context = LocalContext.current + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + RoundImageView( + context = context, + imageUrl = "${BuildConfig.BASE_URL}/images/${follow.profileImg}", + roundSize = 18.dp, + modifier = Modifier + .size(36.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { onProfileClick(follow.memberId) } + ) + + Text( + text = follow.nickname, + modifier = Modifier + .wrapContentWidth() + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { onProfileClick(follow.memberId) }, + color = Dark, + style = DayoTheme.typography.b6 + ) + } + + if (!isMine) { + if (follow.isFollow) { + DayoOutlinedButton( + onClick = { onFollowClick(follow) }, + label = stringResource(id = R.string.follow_already), + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_check_sign_gray), + contentDescription = stringResource(R.string.follow_already_icon_description), + modifier = Modifier.size(20.dp) + ) + } + ) + } else { + FilledButton( + onClick = { onFollowClick(follow) }, + label = stringResource(id = R.string.follow_yet), + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_plus_sign_green), + contentDescription = stringResource(R.string.follow_yet_icon_description), + modifier = Modifier.size(20.dp) + ) + }, + isTonal = true + ) + } + } + } +} + +@Composable +private fun FollowTab(pagerState: PagerState, followerCount: Int, followingCount: Int) { + val coroutineScope = rememberCoroutineScope() + val pages = listOf( + stringResource(id = R.string.follower), + stringResource(id = R.string.following) + ) + + TabRow( + selectedTabIndex = pagerState.currentPage, + containerColor = White, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]), + color = Primary_23C882, + ) + }, + divider = { Divider(color = Color.Transparent, thickness = 0.dp) }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 18.dp) + .padding(top = 4.dp) + ) { + pages.forEachIndexed { index, title -> + Tab( + text = { + Text( + text = "$title " + if (index == FOLLOWER_TAB_ID) "$followerCount" else "$followingCount", + style = DayoTheme.typography.b5 + ) + }, + selected = pagerState.currentPage == index, + selectedContentColor = Primary_23C882, + unselectedContentColor = Gray2_767B83, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + } + ) + } + } +} + +@Preview +@Composable +private fun PreviewEmptyFollowScreen() { + FollowScreen( + currentMemberId = "", + profileInfo = DEFAULT_PROFILE, + followerUiState = FollowUiState.Success(), + followingUiState = FollowUiState.Success(), + pagerState = rememberPagerState(initialPage = FOLLOWER_TAB_ID, pageCount = { 2 }), + onProfileClick = {}, + onFollowClick = {}, + onBackClick = {} + ) +} + +@Preview +@Composable +private fun PreviewFollowScreen() { + FollowScreen( + currentMemberId = "", + profileInfo = DEFAULT_PROFILE, + followerUiState = FollowUiState.Success( + 2, listOf( + Follow( + isFollow = true, + memberId = "", + nickname = "๋‹ค์š”", + profileImg = "" + ), + Follow( + isFollow = false, + memberId = "", + nickname = "๋‹ค์š”๋‹ค์š”", + profileImg = "" + ) + ) + ), + followingUiState = FollowUiState.Success(), + pagerState = rememberPagerState(initialPage = FOLLOWER_TAB_ID, pageCount = { 2 }), + onProfileClick = {}, + onFollowClick = {}, + onBackClick = {} + ) +} + +private const val FOLLOWER_TAB_ID = 0 +private const val FOLLOWING_TAB_ID = 1 +private val DEFAULT_PROFILE = Profile( + memberId = null, + email = "", + nickname = "nickname", + profileImg = "", + postCount = 10, + followerCount = 10, + followingCount = 10, + follow = null, +) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageEditScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageEditScreen.kt new file mode 100644 index 00000000..5cdd68a4 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageEditScreen.kt @@ -0,0 +1,505 @@ +package daily.dayo.presentation.screen.mypage + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.os.Environment +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.launch +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Text +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.domain.model.Profile +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Resource +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.createLoadingDialog +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.hideLoadingDialog +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.resizeDialogFragment +import daily.dayo.presentation.common.dialog.LoadingAlertDialog.showLoadingDialog +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.view.BadgeRoundImageView +import daily.dayo.presentation.view.DayoTextField +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.view.dialog.BottomSheetDialog +import daily.dayo.presentation.view.dialog.getBottomSheetDialogState +import daily.dayo.presentation.viewmodel.ProfileSettingViewModel +import kotlinx.coroutines.launch +import java.io.File +import java.io.FileOutputStream +import java.util.regex.Pattern + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun MyPageEditScreen( + onBackClick: () -> Unit, + profileSettingViewModel: ProfileSettingViewModel = hiltViewModel() +) { + val context = LocalContext.current + val focusManager = LocalFocusManager.current + val bottomSheetState = getBottomSheetDialogState() + val coroutineScope = rememberCoroutineScope() + val alertDialog = remember { mutableStateOf(createLoadingDialog(context)) } + + val profileUiState by profileSettingViewModel.profileInfo.observeAsState(Resource.loading(null)) + val isNicknameDuplicate by profileSettingViewModel.isNicknameDuplicate.collectAsStateWithLifecycle(false) + val updateSuccess by profileSettingViewModel.updateSuccess.collectAsStateWithLifecycle(false) + + val profileInfo = remember { mutableStateOf(null) } + val modifiedProfileImage = remember { mutableStateOf("") } + val nickNameErrorMessage = remember { mutableStateOf("") } + var showProfileGallery by remember { mutableStateOf(false) } + var showProfileCapture by remember { mutableStateOf(false) } + val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + if (uri != null) { + modifiedProfileImage.value = uri.toString() + } + } + val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap -> + if (bitmap != null) { + modifiedProfileImage.value = bitmapToUri(context, bitmap).toString() + } + } + + LaunchedEffect(profileUiState) { + profileInfo.value = when (profileUiState.status) { + Status.SUCCESS -> profileUiState.data + Status.LOADING, Status.ERROR -> null + } + } + + LaunchedEffect(profileInfo.value?.nickname, isNicknameDuplicate) { + profileInfo.value?.nickname?.let { nickname -> + profileSettingViewModel.requestCheckNicknameDuplicate(nickname) + } + + nickNameErrorMessage.value = when { + isNicknameDuplicate -> context.getString(R.string.my_profile_edit_nickname_message_duplicate_fail) + else -> verifyNickname(profileInfo.value?.nickname ?: "", context) + } + } + + LaunchedEffect(profileInfo.value?.profileImg, modifiedProfileImage) { + profileInfo.value?.profileImg?.let { profileImg -> + modifiedProfileImage.value = "${BuildConfig.BASE_URL}/images/${profileImg}" + } + } + + LaunchedEffect(updateSuccess) { + if (updateSuccess) { + hideLoadingDialog(alertDialog.value) + onBackClick.invoke() + } + } + + if (showProfileGallery) { + ProfileGallery(context, galleryLauncher) + showProfileGallery = false + } + + if (showProfileCapture) { + ProfileCapture(context, cameraLauncher) + showProfileCapture = false + } + + MyPageEditScreen( + profileInfo = profileInfo, + bottomSheetState = bottomSheetState, + modifiedProfileImage = modifiedProfileImage.value, + nickNameErrorMessage = nickNameErrorMessage.value, + onClickProfileSelect = { + coroutineScope.launch { + showProfileGallery = true + bottomSheetState.bottomSheetState.hide() + } + }, + onClickProfileCapture = { + coroutineScope.launch { + showProfileCapture = true + bottomSheetState.bottomSheetState.hide() + } + }, + onClickProfileReset = { + modifiedProfileImage.value = "" + coroutineScope.launch { + bottomSheetState.bottomSheetState.hide() + } + }, + onBackClick = onBackClick, + onConfirmClick = { + if (profileInfo.value?.nickname != null) { + showLoadingDialog(alertDialog.value) + resizeDialogFragment(context, alertDialog.value, 0.8f) + profileSettingViewModel.requestUpdateMyProfile( + nickname = profileInfo.value?.nickname!!, + profileImg = uriToFile(context, modifiedProfileImage.value), + isReset = modifiedProfileImage.value.isEmpty() + ) + } + focusManager.clearFocus() + } + ) +} + +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) +@Composable +private fun MyPageEditScreen( + profileInfo: MutableState, + modifiedProfileImage: String, + nickNameErrorMessage: String, + bottomSheetState: BottomSheetScaffoldState, + onClickProfileSelect: () -> Unit, + onClickProfileCapture: () -> Unit, + onClickProfileReset: () -> Unit, + onBackClick: () -> Unit, + onConfirmClick: () -> Unit, +) { + val scrollState = rememberScrollState() + val coroutineScope = rememberCoroutineScope() + + Scaffold( + topBar = { + MyPageEditTopNavigation( + confirmEnabled = nickNameErrorMessage.isEmpty(), + onBackClick = onBackClick, + onConfirmClick = onConfirmClick + ) + }, + bottomBar = { + ProfileImageBottomSheetDialog( + bottomSheetState, + onClickProfileSelect, + onClickProfileCapture, + onClickProfileReset + ) + }, + content = { contentPadding -> + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize() + .verticalScroll(scrollState) + .padding(contentPadding) + .padding(vertical = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val placeholderResId = remember { R.drawable.ic_profile_default_user_profile } + BadgeRoundImageView( + context = LocalContext.current, + imageUrl = modifiedProfileImage, + imageDescription = "my page profile image", + placeholderResId = placeholderResId, + contentModifier = Modifier + .size(100.dp) + .aspectRatio(1f) + .clip(RoundedCornerShape(percent = 50)) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + coroutineScope.launch { bottomSheetState.bottomSheetState.expand() } + } + ) + ) + + Spacer(modifier = Modifier.height(36.dp)) + + DayoTextField( + value = profileInfo.value?.nickname ?: "", + onValueChange = { textValue -> + profileInfo.value = profileInfo.value?.copy( + nickname = textValue + ) + }, + label = stringResource(id = R.string.nickname), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 18.dp), + isError = nickNameErrorMessage.isNotEmpty(), + errorMessage = nickNameErrorMessage + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Column(modifier = Modifier.padding(horizontal = 18.dp)) { + Text( + text = stringResource(id = R.string.email), + style = DayoTheme.typography.caption3.copy( + color = Gray4_C5CAD2, + fontWeight = FontWeight.SemiBold + ) + ) + + Text( + text = profileInfo.value?.email ?: "", + modifier = Modifier.padding(vertical = 8.dp), + style = DayoTheme.typography.b4.copy( + color = Gray2_767B83, + fontWeight = FontWeight.SemiBold + ) + ) + + Divider( + modifier = Modifier.fillMaxWidth(), + thickness = 1.dp, + color = Gray6_F0F1F3 + ) + } + } + } + ) +} + +@Composable +private fun MyPageEditTopNavigation( + confirmEnabled: Boolean, + onBackClick: () -> Unit, + onConfirmClick: () -> Unit +) { + TopNavigation( + title = stringResource(id = R.string.my_profile_edit_title), + leftIcon = { + IconButton(onClick = onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_back_sign), + contentDescription = "back", + tint = Gray1_50545B + ) + } + }, + rightIcon = { + Text( + modifier = Modifier + .padding(vertical = 12.dp) + .padding(start = 24.dp, end = 18.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + enabled = confirmEnabled, + onClick = onConfirmClick + ), + text = stringResource(id = R.string.confirm), + style = DayoTheme.typography.b3.copy(color = Dark), + ) + }, + titleAlignment = TopNavigationAlign.CENTER + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ProfileImageBottomSheetDialog( + bottomSheetState: BottomSheetScaffoldState, + onClickProfileSelect: () -> Unit, + onClickProfileCapture: () -> Unit, + onClickProfileReset: () -> Unit, +) { + BottomSheetDialog( + sheetState = bottomSheetState, + buttons = listOf( + Pair(stringResource(id = R.string.my_profile_edit_image_select_gallery)) { + onClickProfileSelect() + }, Pair(stringResource(id = R.string.image_option_camera)) { + onClickProfileCapture() + }, Pair(stringResource(id = R.string.my_profile_edit_image_reset)) { + onClickProfileReset() + }), + isFirstButtonColored = true + ) +} + +@Composable +fun ProfileGallery( + context: Context, + galleryLauncher: ManagedActivityResultLauncher +) { + val imagePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_IMAGES + } else { + Manifest.permission.WRITE_EXTERNAL_STORAGE + } + + var hasImagePermission by remember { + mutableStateOf( + ContextCompat.checkSelfPermission( + context, + imagePermission + ) == PackageManager.PERMISSION_GRANTED + ) + } + + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted -> + hasImagePermission = isGranted + } + + LaunchedEffect(hasImagePermission) { + if (hasImagePermission) { + galleryLauncher.launch("image/*") + } else { + permissionLauncher.launch(imagePermission) + } + } +} + +@Composable +fun ProfileCapture( + context: Context, + cameraLauncher: ManagedActivityResultLauncher +) { + var hasCameraPermission by remember { + mutableStateOf( + ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + ) + } + + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted -> + hasCameraPermission = isGranted + } + + LaunchedEffect(hasCameraPermission) { + if (hasCameraPermission) { + cameraLauncher.launch() + } else { + permissionLauncher.launch(Manifest.permission.CAMERA) + } + } +} + +private fun verifyNickname(nickname: String, context: Context): String { + return if (nickname.length < 2) context.getString(R.string.my_profile_edit_nickname_message_length_fail_min) + else if (nickname.length > 10) context.getString(R.string.my_profile_edit_nickname_message_length_fail_max) + else if (Pattern.matches("^[ใ„ฑ-ใ…Ž|ใ…-ใ…ฃ๊ฐ€-ํžฃa-zA-Z0-9]+$", nickname).not()) context.getString(R.string.my_profile_edit_nickname_message_format_fail) + else "" +} + +fun uriToFile(context: Context, uri: String): File? { + if (uri.isEmpty()) return null + if (uri.toUri().scheme == "http" || uri.toUri().scheme == "https") return null + + val inputStream = context.contentResolver.openInputStream(uri.toUri()) + return if (inputStream != null) { + val tempFile = File(context.cacheDir, "profile_image_${System.currentTimeMillis()}.jpg") + try { + val outputStream = FileOutputStream(tempFile) + inputStream.use { input -> + outputStream.use { output -> + input.copyTo(output) + } + } + tempFile + } catch (e: Exception) { + e.printStackTrace() + null + } finally { + inputStream.close() + } + } else { + null + } +} + +fun bitmapToUri(context: Context, bitmap: Bitmap): Uri? { + val file = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "profile_image_${System.currentTimeMillis()}.jpg") + return try { + FileOutputStream(file).use { outputStream -> + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + } + Uri.fromFile(file) + } catch (e: Exception) { + e.printStackTrace() + null + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +internal fun PreviewMyPageEditScreen() { + DayoTheme { + MyPageEditScreen( + profileInfo = remember { + mutableStateOf( + Profile( + null, + "princedj@gmail.com", + "๋™์ค€์™•์ž๋‹ค์š”", + "", + 0, + 0, + 0, + false + ) + ) + }, + modifiedProfileImage = "", + nickNameErrorMessage = "", + bottomSheetState = getBottomSheetDialogState(), + onClickProfileSelect = {}, + onClickProfileCapture = {}, + onClickProfileReset = { }, + onBackClick = {}, + onConfirmClick = {} + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt new file mode 100644 index 00000000..1fba5465 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MyPageScreen.kt @@ -0,0 +1,430 @@ +package daily.dayo.presentation.screen.mypage + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import daily.dayo.domain.model.Folder +import daily.dayo.domain.model.Profile +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_AD_START_COUNT +import daily.dayo.presentation.common.constant.FolderConstants.MAX_FOLDER_COUNT +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.Gray7_F6F6F7 +import daily.dayo.presentation.theme.PrimaryL3_F2FBF7 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.FolderView +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.viewmodel.FolderViewModel +import daily.dayo.presentation.viewmodel.ProfileViewModel + +@Composable +fun MyPageScreen( + onSettingsClick: () -> Unit, + onFollowButtonClick: (String, Int) -> Unit, + onProfileEditClick: () -> Unit, + onBookmarkClick: () -> Unit, + onFolderClick: (Long) -> Unit, + onFolderCreateClick: () -> Unit, + onAdRequest: (onRewardSuccess: () -> Unit) -> Unit, + profileViewModel: ProfileViewModel = hiltViewModel(), + folderViewModel: FolderViewModel = hiltViewModel() +) { + val profileInfo = profileViewModel.profileInfo.observeAsState() + val folderList = folderViewModel.folderList.observeAsState() + val folderCount = folderList.value?.data?.size ?: 0 + val shouldShowAd = folderCount + 1 in FOLDER_AD_START_COUNT..MAX_FOLDER_COUNT + + LaunchedEffect(Unit) { + profileViewModel.requestMyProfile() + folderViewModel.requestAllMyFolderList() + } + + Scaffold( + topBar = { MyPageTopNavigation(onSettingsClick) }, + content = { contentPadding -> + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize() + .padding(contentPadding) + .padding(horizontal = 20.dp), + ) { + item(span = { GridItemSpan(2) }) { + MyPageProfile(profileInfo.value?.data, onFollowButtonClick) + } + + item(span = { GridItemSpan(2) }) { + MyPageMenu(onProfileEditClick, onBookmarkClick) + } + + item(span = { GridItemSpan(2) }) { + MyPageDiaryHeader( + isCreateFolderEnabled = folderList.value?.data?.size?.let { it < MAX_FOLDER_COUNT } ?: false, + onFolderCreateClick = { + // ๊ด‘๊ณ  ๋ณด๊ธฐ + if (shouldShowAd) { + onAdRequest { + onFolderCreateClick() + } + } else { + onFolderCreateClick() + } + } + ) + } + + when (folderList.value?.status) { + Status.SUCCESS -> { + folderList.value?.data?.let { folders -> + items(folders) { folder -> + MyPageDiary(folder, onFolderClick) + } + } + } + + Status.LOADING -> Unit + Status.ERROR -> Unit + else -> Unit + } + } + } + ) +} + +@Composable +private fun MyPageProfile( + profile: Profile?, + onFollowButtonClick: (String, Int) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(DayoTheme.colorScheme.background) + .padding(top = 8.dp, bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // profile image + RoundImageView( + context = LocalContext.current, + imageUrl = "${BuildConfig.BASE_URL}/images/${profile?.profileImg}", + imageDescription = "my page profile image", + roundSize = 24.dp, + modifier = Modifier + .size(48.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { } + ) + ) + + // nickname, email + Column( + modifier = Modifier + .weight(1f) + .padding(start = 8.dp) + ) { + Text( + text = profile?.nickname ?: "", + style = DayoTheme.typography.h1.copy( + color = Dark, + fontWeight = FontWeight.SemiBold + ) + ) + + Text( + text = profile?.email ?: "", + style = DayoTheme.typography.caption5.copy( + color = Gray4_C5CAD2 + ) + ) + } + + // follower + Column( + modifier = Modifier.clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { profile?.memberId?.let { onFollowButtonClick(it, FOLLOWER) } } + ), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_follower), + contentDescription = stringResource(id = R.string.follower), + modifier = Modifier.size(12.dp) + ) + + Text( + text = stringResource(id = R.string.follower), + style = DayoTheme.typography.caption4.copy(color = Gray1_50545B) + ) + } + Text( + text = "${profile?.followerCount ?: "0"}", + style = DayoTheme.typography.b6.copy(color = Gray1_50545B) + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + + // following + Column( + modifier = Modifier.clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { profile?.memberId?.let { onFollowButtonClick(it, FOLLOWING) } } + ), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.ic_following), + contentDescription = stringResource(id = R.string.following), + modifier = Modifier.size(12.dp) + ) + + Text( + text = stringResource(id = R.string.following), + style = DayoTheme.typography.caption4.copy(color = Gray1_50545B) + ) + } + + Text( + text = "${profile?.followingCount ?: "0"}", + style = DayoTheme.typography.b6.copy(color = Gray1_50545B) + ) + } + + Spacer(modifier = Modifier.width(20.dp)) + } +} + +@Composable +private fun MyPageMenu( + onProfileEditClick: () -> Unit, + onBookmarkClick: () -> Unit +) { + Row( + modifier = Modifier.padding(bottom = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // edit + androidx.compose.material3.TextButton( + onClick = onProfileEditClick, + shape = RoundedCornerShape(size = 12.dp), + border = BorderStroke(width = 1.dp, color = Gray6_F0F1F3), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = White_FFFFFF, + contentColor = Gray2_767B83 + ), + modifier = Modifier + .height(36.dp) + .weight(1f), + content = { + Text( + text = stringResource(id = R.string.my_profile_edit_title), + style = DayoTheme.typography.b6.copy(Gray1_50545B), + ) + } + ) + + // bookmark + IconButton( + onClick = { onBookmarkClick() }, + modifier = Modifier + .background(color = DayoTheme.colorScheme.background, shape = RoundedCornerShape(12.dp)) + .border( + border = BorderStroke(width = 1.dp, color = Gray6_F0F1F3), + shape = RoundedCornerShape(size = 12.dp) + ) + .size(36.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_bookmark_default), + contentDescription = stringResource(id = R.string.bookmark), + tint = Gray1_50545B + ) + } + } +} + +@Composable +private fun MyPageDiaryHeader( + isCreateFolderEnabled: Boolean, + onFolderCreateClick: () -> Unit +) { + Row( + modifier = Modifier + .background(color = DayoTheme.colorScheme.background) + .fillMaxWidth() + .padding(bottom = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.my_profile_my_diary), + style = DayoTheme.typography.b3.copy(Dark) + ) + + // ์ƒˆ ํด๋” ์ƒ์„ฑ ๋ฒ„ํŠผ + Button( + onClick = onFolderCreateClick, + colors = ButtonDefaults.buttonColors( + containerColor = PrimaryL3_F2FBF7, + contentColor = Primary_23C882, + disabledContainerColor = Gray7_F6F6F7, + disabledContentColor = Gray4_C5CAD2 + ), + shape = RoundedCornerShape(8.dp), + contentPadding = PaddingValues(8.dp), + modifier = Modifier.wrapContentSize(), + enabled = isCreateFolderEnabled + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = stringResource(id = R.string.my_profile_new_folder) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringResource(id = R.string.my_profile_new_folder), + style = DayoTheme.typography.b6.copy( + if (isCreateFolderEnabled) Primary_23C882 else Gray4_C5CAD2 + ) + ) + } + } +} + +@Composable +private fun MyPageDiary(folder: Folder, onFolderClick: (Long) -> Unit) { + FolderView( + folder = folder, + onClickFolder = onFolderClick, + modifier = Modifier.padding(bottom = 12.dp) + ) +} + +@Composable +private fun MyPageTopNavigation(onSettingsClick: () -> Unit) { + TopNavigation( + leftIcon = { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(start = 18.dp) + ) { + Text( + text = stringResource(id = R.string.my_page), + style = DayoTheme.typography.h1.copy( + color = Gray1_50545B, + fontWeight = FontWeight.SemiBold + ) + ) + } + }, + rightIcon = { + NoRippleIconButton( + onClick = onSettingsClick, + iconContentDescription = "setting button", + iconPainter = painterResource(id = R.drawable.ic_setting), + iconModifier = Modifier + .padding(end = 18.dp) + .size(24.dp), + iconTintColor = Dark + ) + } + ) +} + +@Preview +@Composable +private fun PreviewMyPageTopNavigation() { + MyPageTopNavigation({}) +} + +@Preview +@Composable +private fun PreviewMyPageProfile() { + MyPageProfile(profile = null, onFollowButtonClick = { memberId, tabNum -> }) +} + +@Preview +@Composable +private fun PreviewMyPageMenu() { + MyPageMenu({}, {}) +} + +@Preview +@Composable +private fun PreviewMyPageDiaryHeader() { + Column { + MyPageDiaryHeader(isCreateFolderEnabled = true, onFolderCreateClick = {}) + MyPageDiaryHeader(isCreateFolderEnabled = false, onFolderCreateClick = {}) + } +} + +private const val FOLLOWER = 0 +private const val FOLLOWING = 1 diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MypageNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MypageNavigation.kt new file mode 100644 index 00000000..130a9208 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/mypage/MypageNavigation.kt @@ -0,0 +1,223 @@ +package daily.dayo.presentation.screen.mypage + +import androidx.compose.runtime.remember +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import daily.dayo.presentation.screen.bookmark.BookmarkScreen +import daily.dayo.presentation.screen.folder.FolderCreateScreen +import daily.dayo.presentation.screen.folder.FolderEditScreen +import daily.dayo.presentation.screen.folder.FolderPostMoveScreen +import daily.dayo.presentation.screen.folder.FolderScreen +import daily.dayo.presentation.screen.settings.BlockedUsersScreen + +fun NavController.navigateMyPage() { + navigate(MyPageRoute.route) { + launchSingleTop = true + } +} + +fun NavController.navigateProfileEdit() { + navigate(MyPageRoute.profileEdit()) +} + +fun NavController.navigateBlockedUsers() { + navigate(MyPageRoute.blockedUsers()) +} + +fun NavController.navigateBookmark() { + navigate(MyPageRoute.bookmark()) +} + +fun NavController.navigateFolderCreate() { + navigate(MyPageRoute.folderCreate()) +} + +fun NavController.navigateFolderEdit(folderId: Long) { + navigate(MyPageRoute.folderEdit(folderId)) +} + +fun NavController.navigateFolderPostMove(folderId: Long) { + navigate(MyPageRoute.folderPostMove(folderId)) +} + +fun NavController.navigateFollowMenu(memberId: String, tabNum: Int) { + navigate(MyPageRoute.follow(memberId, "$tabNum")) +} + +fun NavController.navigateFolder(folderId: Long) { + navigate(MyPageRoute.folder(folderId)) +} + +fun NavController.navigateBackToFolder(folderId: Long) { + navigate(MyPageRoute.folder(folderId)) { + popUpTo(MyPageRoute.folder(folderId)) { + inclusive = true + } + launchSingleTop = true + } +} + +fun NavGraphBuilder.myPageNavGraph( + navController: NavController, + onBackClick: () -> Unit, + onSettingsClick: () -> Unit, + onFollowButtonClick: (String, Int) -> Unit, + onProfileClick: (String) -> Unit, + onProfileEditClick: () -> Unit, + onBookmarkClick: () -> Unit, + onFolderClick: (Long) -> Unit, + onFolderCreateClick: () -> Unit, + onFolderEditClick: (Long) -> Unit, + onWritePostWithFolderClick: (Long) -> Unit, + onPostClick: (Long) -> Unit, + onPostMoveClick: (Long) -> Unit, + onAdRequest: (onRewardSuccess: () -> Unit) -> Unit, + navigateBackToFolder: (Long) -> Unit +) { + composable(MyPageRoute.route) { + MyPageScreen( + onSettingsClick = onSettingsClick, + onFollowButtonClick = onFollowButtonClick, + onProfileEditClick = onProfileEditClick, + onBookmarkClick = onBookmarkClick, + onFolderClick = onFolderClick, + onFolderCreateClick = onFolderCreateClick, + onAdRequest = onAdRequest + ) + } + + composable( + route = MyPageRoute.follow("{memberId}", "{tabNum}"), + arguments = listOf( + navArgument("memberId") { + type = NavType.StringType + }, + navArgument("tabNum") { + type = NavType.IntType + } + ) + ) { navBackStackEntry -> + val memberId = navBackStackEntry.arguments?.getString("memberId") ?: "" + val tabNum = navBackStackEntry.arguments?.getInt("tabNum") ?: 0 + FollowScreen( + memberId = memberId, + tabNum = tabNum, + onProfileClick = onProfileClick, + onBackClick = onBackClick + ) + } + + composable(MyPageRoute.profileEdit()) { + MyPageEditScreen( + onBackClick = onBackClick + ) + } + + composable(MyPageRoute.blockedUsers()) { + BlockedUsersScreen( + onBackClick = onBackClick, + ) + } + + composable(MyPageRoute.bookmark()) { + BookmarkScreen( + onBackClick = onBackClick + ) + } + + composable(MyPageRoute.folderCreate()) { + FolderCreateScreen( + onBackClick = onBackClick + ) + } + + composable( + route = MyPageRoute.folderRoute, + arguments = listOf( + navArgument("folderId") { + type = NavType.LongType + } + ) + ) { navBackStackEntry -> + navBackStackEntry.arguments?.getLong("folderId")?.let { folderId -> + FolderScreen( + folderId = folderId, + onPostClick = onPostClick, + onFolderEditClick = { onFolderEditClick(folderId) }, + onPostMoveClick = { onPostMoveClick(folderId) }, + onWritePostWithFolderClick = { onWritePostWithFolderClick(folderId) }, + onBackClick = onBackClick, + folderViewModel = hiltViewModel(navBackStackEntry) + ) + } + } + + composable( + route = MyPageRoute.folderEditRoute, + arguments = listOf( + navArgument("folderId") { + type = NavType.LongType + } + ) + ) { navBackStackEntry -> + navBackStackEntry.arguments?.getLong("folderId")?.let { folderId -> + FolderEditScreen( + folderId = folderId, + onBackClick = onBackClick + ) + } + } + + composable( + route = MyPageRoute.folderPostMoveRoute, + arguments = listOf( + navArgument("folderId") { + type = NavType.LongType + } + ) + ) { navBackStackEntry -> + navBackStackEntry.arguments?.getLong("folderId")?.let { folderId -> + val parentStackEntry = remember(navBackStackEntry) { + navController.getBackStackEntry(MyPageRoute.folder(folderId)) + } + + FolderPostMoveScreen( + currentFolderId = folderId, + navigateToCreateNewFolder = onFolderCreateClick, + navigateBackToFolder = { navigateBackToFolder(folderId) }, + onAdRequest = onAdRequest, + onBackClick = onBackClick, + folderViewModel = hiltViewModel(parentStackEntry) + ) + } + } +} + +object MyPageRoute { + const val route = "myPage" + const val folderRoute = "$route/folder/{folderId}" + const val folderEditRoute = "$route/folder/edit/{folderId}" + const val folderPostMoveRoute = "$route/folder/move/{folderId}" + + // profile edit + fun profileEdit() = "$route/edit" + + // blocked users + fun blockedUsers() = "$route/blockedUsers" + + // follow + fun follow(memberId: String, tabNum: String) = "$route/follow/$memberId/$tabNum" + + // bookmark + fun bookmark() = "$route/bookmark" + + // folder + fun folder(folderId: Long) = "$route/folder/$folderId" + fun folderCreate() = "$route/folder/create" + fun folderEdit(folderId: Long) = "$route/folder/edit/$folderId" + fun folderPostMove(folderId: Long) = "$route/folder/move/$folderId" +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/notice/NoticeDetailScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/notice/NoticeDetailScreen.kt new file mode 100644 index 00000000..d838117a --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/notice/NoticeDetailScreen.kt @@ -0,0 +1,182 @@ +package daily.dayo.presentation.screen.notice + +import android.webkit.WebView +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Divider +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.viewmodel.NoticeViewModel + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun NoticeDetailScreen( + noticeId: Long, + onBackClick: () -> Unit = {}, + noticeViewModel: NoticeViewModel = hiltViewModel(), + animatedVisibilityScope: AnimatedVisibilityScope, + sharedElementScope: SharedTransitionScope, +) { + val scrollState = rememberScrollState() + val noticeDetail by noticeViewModel.detailNotice.collectAsStateWithLifecycle() + val notice = noticeViewModel.selectedNotice.collectAsStateWithLifecycle().value + + LaunchedEffect(Unit) { + noticeViewModel.requestDetailNotice(noticeId) + } + + Scaffold( + topBar = { + NoticeDetailActionbarLayout(onBackClick = onBackClick) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .padding(innerPadding) + .fillMaxSize() + .verticalScroll(scrollState), + ) { + NoticeDetail( + noticeId = noticeId, + title = notice?.title ?: "๊ณต์ง€์‚ฌํ•ญ", + contents = noticeDetail?.data ?: "", + uploadDate = notice?.uploadDate ?: "0000.00.00", + sharedTransitionScope = sharedElementScope, + animatedVisibilityScope = animatedVisibilityScope, + ) + } + } +} + +@Preview +@Composable +fun NoticeDetailActionbarLayout( + onBackClick: () -> Unit = {}, +) { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = { onBackClick() }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + ) +} + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun NoticeDetail( + noticeId: Long, + title: String = "๊ณต์ง€์‚ฌํ•ญ", + contents: String = "๊ณต์ง€์‚ฌํ•ญ ๋‚ด์šฉ", + uploadDate: String = "0000.00.00", + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope, +) { + with(sharedTransitionScope) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 20.dp, end = 20.dp, top = 4.dp) + .background(DayoTheme.colorScheme.background) + ) { + NoticeDetailHeader( + titleModifier = Modifier.sharedBounds( + sharedContentState = rememberSharedContentState(key = "title_$noticeId"), + animatedVisibilityScope = animatedVisibilityScope, + ), + uploadDateModifier = Modifier.sharedBounds( + sharedContentState = rememberSharedContentState(key = "uploadDate_$noticeId"), + animatedVisibilityScope = animatedVisibilityScope, + ), + title = title, + uploadDate = uploadDate, + ) + Divider( + modifier = Modifier.padding(top = 20.dp, bottom = 18.dp), + color = Gray6_F0F1F3, + thickness = 1.dp, + ) + + AndroidView( + factory = { context -> + WebView(context).apply { + settings.javaScriptEnabled = true + loadDataWithBaseURL(null, contents, "text/html", "UTF-8", null) + } + }, + update = { webView -> + webView.loadDataWithBaseURL(null, contents, "text/html", "UTF-8", null) + }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + ) + } + } +} + +@Composable +fun NoticeDetailHeader( + titleModifier: Modifier, + uploadDateModifier: Modifier, + title: String = "๊ณต์ง€์‚ฌํ•ญ", + uploadDate: String = "0000.00.00", +) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + ) { + Text( + modifier = uploadDateModifier, + text = uploadDate, + style = DayoTheme.typography.caption4, + color = Gray3_9FA5AE, + softWrap = false, + overflow = TextOverflow.Visible + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + modifier = titleModifier, + text = title, + style = DayoTheme.typography.b1, + color = Dark, + softWrap = false, + overflow = TextOverflow.Visible + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/notice/NoticeNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/notice/NoticeNavigation.kt new file mode 100644 index 00000000..b0784d9b --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/notice/NoticeNavigation.kt @@ -0,0 +1,65 @@ +package daily.dayo.presentation.screen.notice + +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.material3.SnackbarHostState +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import daily.dayo.presentation.viewmodel.NoticeViewModel +import kotlinx.coroutines.CoroutineScope + +fun NavController.navigateNotices() { + navigate(NoticeRoute.route) +} + +fun NavController.navigateNoticeDetail(noticeId: Long) { + navigate(NoticeRoute.noticeDetail(noticeId)) +} + +@OptIn(ExperimentalSharedTransitionApi::class) +fun NavGraphBuilder.noticeNavGraph( + noticeViewModel: NoticeViewModel, + onBackClick: () -> Unit, + onNoticeDetailClick: (Long) -> Unit, + sharedTransitionScope: SharedTransitionScope, +) { + composable(NoticeRoute.route) { + NoticesScreen( + onBackClick = onBackClick, + onNoticeDetailClick = onNoticeDetailClick, + noticeViewModel = noticeViewModel, + sharedElementScope = sharedTransitionScope, + animatedVisibilityScope = this, + ) + } + + composable( + route = NoticeRoute.noticeDetailRoute, + arguments = listOf( + navArgument("noticeId") { + type = NavType.LongType + } + ) + ) { navBackStackEntry -> + navBackStackEntry.arguments?.getLong("noticeId")?.let { noticeId -> + NoticeDetailScreen( + noticeId = noticeId, + onBackClick = onBackClick, + noticeViewModel = noticeViewModel, + sharedElementScope = sharedTransitionScope, + animatedVisibilityScope = this, + ) + } + } +} + +object NoticeRoute { + const val route = "notice" + + const val noticeDetailRoute = "$route/{noticeId}" + + fun noticeDetail(noticeId: Long) = "$route/$noticeId" +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/notice/NoticesScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/notice/NoticesScreen.kt new file mode 100644 index 00000000..c103a4fc --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/notice/NoticesScreen.kt @@ -0,0 +1,155 @@ +package daily.dayo.presentation.screen.notice + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import daily.dayo.domain.model.Notice +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.viewmodel.NoticeViewModel + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun NoticesScreen( + sharedElementScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope, + onBackClick: () -> Unit = {}, + onNoticeDetailClick: (Long) -> Unit = {}, + noticeViewModel: NoticeViewModel = hiltViewModel(), +) { + val notices = noticeViewModel.noticeList.collectAsLazyPagingItems() + + LaunchedEffect(Unit) { + noticeViewModel.requestAllNoticeList() + } + + Scaffold( + topBar = { + NoticesActionbarLayout(onBackClick = onBackClick) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .padding(innerPadding) + .fillMaxSize(), + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(vertical = 8.dp), + ) { + items( + count = notices.itemCount, + key = notices.itemKey() + ) { index -> + val notice = notices[index] + notice?.let { + Notice( + notice = it, + onNoticeDetailClick = { + noticeViewModel.selectNotice(notice) + onNoticeDetailClick(notice.noticeId) + }, + sharedElementScope = sharedElementScope, + animatedVisibilityScope = animatedVisibilityScope, + ) + } + } + } + } + } +} + +@Preview +@Composable +fun NoticesActionbarLayout( + onBackClick: () -> Unit = {}, +) { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = { onBackClick() }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + title = stringResource(R.string.notice_actionbar_title), + ) +} + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun Notice( + notice: Notice = Notice( + noticeId = Long.MAX_VALUE, + title = "๊ณต์ง€์‚ฌํ•ญ", + uploadDate = "0000.00.00" + ), + onNoticeDetailClick: () -> Unit = {}, + sharedElementScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope +) { + with(sharedElementScope) { + Column( + modifier = Modifier + .clickable { onNoticeDetailClick() } + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 20.dp, vertical = 12.dp), + ) { + Text( + text = notice.uploadDate, + modifier = Modifier + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "uploadDate_${notice.noticeId}"), + animatedVisibilityScope = animatedVisibilityScope, + ) + .wrapContentSize(), + style = DayoTheme.typography.caption4, + color = Gray3_9FA5AE, + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = notice.title, + modifier = Modifier + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "title_${notice.noticeId}"), + animatedVisibilityScope = animatedVisibilityScope, + ) + .wrapContentSize(), + style = DayoTheme.typography.b6, + color = Dark, + ) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/notification/NotificationNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/notification/NotificationNavigation.kt new file mode 100644 index 00000000..bce0a827 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/notification/NotificationNavigation.kt @@ -0,0 +1,22 @@ +package daily.dayo.presentation.screen.notification + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable + +fun NavGraphBuilder.notificationNavGraph( + onPostClick: (Long) -> Unit, + onProfileClick: (String) -> Unit, + onNoticeClick: (Long) -> Unit, +) { + composable(NotificationRoute.route) { + NotificationScreen( + onPostClick = onPostClick, + onProfileClick = onProfileClick, + onNoticeClick = onNoticeClick, + ) + } +} + +object NotificationRoute { + const val route = "notification" +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/notification/NotificationScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/notification/NotificationScreen.kt new file mode 100644 index 00000000..d90c2e2d --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/notification/NotificationScreen.kt @@ -0,0 +1,485 @@ +package daily.dayo.presentation.screen.notification + +import android.content.Context +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Divider +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import coil.size.Size +import daily.dayo.domain.model.Notification +import daily.dayo.domain.model.Topic +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.TimeChangerUtil +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray7_F6F6F7 +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.viewmodel.NotificationViewModel +import kotlinx.coroutines.flow.flowOf + +@OptIn(ExperimentalMaterialApi::class) +@Composable +@Preview +fun NotificationScreen( + notificationViewModel: NotificationViewModel = hiltViewModel(), + onPostClick: (Long) -> Unit = {}, + onProfileClick: (String) -> Unit = {}, + onNoticeClick: (Long) -> Unit = {}, +) { + val notifications = notificationViewModel.alarmList.collectAsLazyPagingItems() + val refreshing by notificationViewModel.isRefreshing.collectAsStateWithLifecycle() + val pullRefreshState = + rememberPullRefreshState(refreshing, { notificationViewModel.loadNotifications() }) + + LaunchedEffect(Unit) { + notificationViewModel.loadNotifications() + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + NotificationTopNavigation() + }) { innerPadding -> + NotificationContent( + innerPadding = innerPadding, + context = LocalContext.current, + notifications = notifications, + markAlarmAsChecked = { alarmId -> + notificationViewModel.markAlarmAsChecked(alarmId) + }, + pullRefreshState = pullRefreshState, + isRefreshing = refreshing, + onPostClick = onPostClick, + onProfileClick = onProfileClick, + onNoticeClick = onNoticeClick, + ) + } +} + +@Composable +@Preview +fun NotificationTopNavigation() { + TopNavigation(title = stringResource(R.string.notification)) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun NotificationContent( + innerPadding: PaddingValues, + isRefreshing: Boolean = false, + pullRefreshState: PullRefreshState, + context: Context = LocalContext.current, + notifications: LazyPagingItems = flowOf( + PagingData.from( + listOf( + Notification( + alarmId = 0, + topic = Topic.HEART, + check = false, + content = "", + createdTime = "", + image = "", + nickname = "", + memberId = "", + postId = 0, + profileImage = "", + ) + ) + ) + ).collectAsLazyPagingItems(), + markAlarmAsChecked: (Int) -> Unit = {}, + onPostClick: (Long) -> Unit = {}, + onProfileClick: (String) -> Unit = {}, + onNoticeClick: (Long) -> Unit = {}, +) { + Box( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .pullRefresh(pullRefreshState) + ) { + if (notifications.itemCount < 1 && + notifications.loadState.refresh is LoadState.NotLoading && + notifications.loadState.append.endOfPaginationReached + ) { + EmptyNotifications() + } else { + // checkedItems์™€ unCheckedItems๋ฅผ ๋ณด์—ฌ์ค„๋•Œ, alaramId๋งŒ์œผ๋กœ ๊ตฌ๋ถ„ํ•˜๋ฉด, check ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ ๋˜์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๊ฐ€ ๋‚  ์ˆ˜ ์žˆ์Œ + val (checkedItems, uncheckedItems) = notifications.itemSnapshotList.items.partition { it.check == true } + + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + if (uncheckedItems.isNotEmpty()) { + item { + Spacer( + modifier = Modifier + .height(4.dp) + .fillMaxWidth() + ) + } + + notifications( + notifications = uncheckedItems, + keyPrefix = "unchecked", + context = context, + onNotificationClick = { notification -> + notification.alarmId?.let { alarmId -> + markAlarmAsChecked(alarmId) + } + performNotificationNavigation( + notification = notification, + onPostClick = onPostClick, + onProfileClick = onProfileClick, + onNoticeClick = onNoticeClick, + ) + }, + onProfileClick = onProfileClick, + ) + } + + item { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(12.dp) + ) + } + + if (uncheckedItems.isNotEmpty() && checkedItems.isNotEmpty()) { + item { + Divider( + color = Gray7_F6F6F7, + thickness = 1.dp, + modifier = Modifier + .padding(horizontal = 20.dp) + .height(1.dp) + .fillMaxWidth() + ) + Spacer(modifier = Modifier.size(16.dp)) + } + } + + if (checkedItems.isNotEmpty()) { + item { + Text( + text = stringResource(R.string.notification_seen_title), + style = DayoTheme.typography.b3.copy(color = Dark), + modifier = Modifier.padding(horizontal = 20.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + } + + notifications( + notifications = checkedItems, + keyPrefix = "checked", + context = context, + onNotificationClick = { notification -> + performNotificationNavigation( + notification = notification, + onPostClick = onPostClick, + onProfileClick = onProfileClick, + onNoticeClick = onNoticeClick, + ) + }, + onProfileClick = onProfileClick, + ) + } + } + + // refresh indicator + PullRefreshIndicator( + isRefreshing, + pullRefreshState, + Modifier.align(Alignment.TopCenter) + ) + } + } +} + +fun LazyListScope.notifications( + notifications: List, + keyPrefix: String, + context: Context, + onNotificationClick: (Notification) -> Unit, + onProfileClick: (String) -> Unit, +) { + itemsIndexed( + items = notifications, + key = { idx, notification -> "$keyPrefix-${notification.alarmId ?: idx}" }, + ) { idx, notification -> + Box(modifier = Modifier.animateItem()) { + NotificationView( + notification = notification, + context = context, + onClick = { onNotificationClick(notification) }, + onProfileClick = onProfileClick, + ) + } + } +} + +@Composable +@Preview +fun EmptyNotifications() { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_notification_empty), + contentDescription = "" + ) + Spacer(modifier = Modifier.size(20.dp)) + Text( + text = stringResource(R.string.notification_empty_title), + style = DayoTheme.typography.b3.copy(color = Gray3_9FA5AE) + ) + Spacer(modifier = Modifier.size(2.dp)) + Text( + text = stringResource(R.string.notification_empty_description), + style = DayoTheme.typography.caption2.copy(color = Gray4_C5CAD2) + ) + } +} + +@Composable +@Preview +fun NotificationView( + notification: Notification = Notification( + alarmId = 0, + topic = Topic.HEART, + check = false, + content = "", + createdTime = "", + image = "", + nickname = "", + memberId = "", + postId = 0, + profileImage = "", + ), + context: Context = LocalContext.current, + onClick: () -> Unit = {}, + onProfileClick: (String) -> Unit = {}, +) { + var textLayoutResult by remember { mutableStateOf(null) } + val notificationMessage = buildAnnotatedString { + // 1. ๋‹‰๋„ค์ž„ ํฌํ•จ๋œ ๋ฉ”์‹œ์ง€ ์ธ์ง€ ์ฒดํฌ + if (!notification.nickname.isNullOrBlank()) { + pushStringAnnotation( + tag = "nickname", + annotation = notification.nickname ?: "" + ) + withStyle( + style = DayoTheme.typography.caption1.copy(color = Dark) + .toSpanStyle() + ) { + append(notification.nickname ?: "") + } + pop() + } + + // 2. ๋ณธ๋ฌธ + withStyle( + style = DayoTheme.typography.caption2.copy(color = Dark).toSpanStyle() + ) { + append(notification.content ?: "") + } + } + + Row( + modifier = Modifier + .clickable { onClick() } + .background(DayoTheme.colorScheme.background) + .fillMaxWidth() + .wrapContentHeight() + .padding(vertical = 12.dp, horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Row( + modifier = Modifier.weight(1f), + ) { + RoundImageView( + imageUrl = "${BuildConfig.BASE_URL}/images/${notification.profileImage}", + context = context, + modifier = Modifier + .size(28.dp) + .clickable { + notification.memberId?.let { id -> + onProfileClick(id) + } + }, + imageDescription = "notification thumbnail", + imageSize = Size.ORIGINAL, + roundSize = 14.dp, + placeholderResId = R.drawable.ic_profile_default_user_profile, + ) + Spacer( + modifier = Modifier + .width(8.dp) + .fillMaxHeight() + ) + Column( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + ) { + Text( + text = notificationMessage, + style = DayoTheme.typography.caption2.copy(color = Dark), // ๊ธฐ๋ณธ ํ…์ŠคํŠธ ์Šคํƒ€์ผ + modifier = Modifier.pointerInput(Unit) { + detectTapGestures { offset -> + textLayoutResult?.let { layoutResult -> + val position = layoutResult.getOffsetForPosition(offset) + val annotation = notificationMessage.getStringAnnotations( + tag = "nickname", + start = position, + end = position + ).firstOrNull() + + if (annotation != null) { + notification.memberId?.let { id -> + onProfileClick(id) + } + } else { + onClick() + } + } + } + }, + onTextLayout = { layoutResult -> + textLayoutResult = layoutResult + }, + ) + notification.createdTime?.let { + Text( + modifier = Modifier.clickable( + interactionSource = null, + indication = null, + onClick = { onClick() } + ), + text = TimeChangerUtil.timeChange(context, notification.createdTime ?: ""), + style = DayoTheme.typography.caption4.copy(color = Gray3_9FA5AE) + ) + } + } + } + + if (!notification.image.isNullOrBlank()) { + Row(modifier = Modifier.requiredSize(width = (16 + 56).dp, height = 56.dp)) { + Spacer( + modifier = Modifier + .width(16.dp) + .fillMaxHeight() + ) + RoundImageView( + imageUrl = "${BuildConfig.BASE_URL}/images/${notification.image!!}", + context = context, + modifier = Modifier + .size(56.dp), + roundSize = 10.dp, + imageDescription = "notification thumbnail", + ) + } + } + } +} + +fun performNotificationNavigation( + notification: Notification, + onPostClick: (Long) -> Unit, + onProfileClick: (String) -> Unit, + onNoticeClick: (Long) -> Unit, +) { + notification.topic?.let { topic -> + when (topic) { + Topic.HEART -> { + notification.postId?.let { id -> + onPostClick(id) + } + } + + Topic.COMMENT -> { + notification.postId?.let { id -> + onPostClick(id) + } + } + + Topic.NOTICE -> { + // TODO: Navigate to Notice Post + // onNoticeClick(0L) + } + + Topic.FOLLOW -> { + notification.memberId?.let { id -> + onProfileClick(id) + } + } + + Topic.MENTION -> { + notification.postId?.let { id -> + onPostClick(id) + } + } + + null -> { + Unit + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/post/PostLikeUsersScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostLikeUsersScreen.kt new file mode 100644 index 00000000..3e60073b --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostLikeUsersScreen.kt @@ -0,0 +1,275 @@ +package daily.dayo.presentation.screen.post + +import androidx.compose.foundation.background +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import coil.compose.AsyncImage +import coil.request.ImageRequest +import daily.dayo.domain.model.LikeUser +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.view.DayoOutlinedButton +import daily.dayo.presentation.view.FilledButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.viewmodel.FollowViewModel +import daily.dayo.presentation.viewmodel.PostViewModel +import daily.dayo.presentation.viewmodel.ProfileViewModel +import kotlinx.coroutines.flow.flowOf +import java.text.DecimalFormat + +@Composable +fun PostLikeUsersScreen( + postId: Long, + onProfileClick: (String) -> Unit, + onBackClick: () -> Unit, + postViewModel: PostViewModel = hiltViewModel(), + profileViewModel: ProfileViewModel = hiltViewModel(), + followViewModel: FollowViewModel = hiltViewModel() +) { + val currentMemberId = profileViewModel.currentMemberId + val likeCount = postViewModel.postLikeCountUiState.collectAsStateWithLifecycle() + val likeUserList = postViewModel.postLikeUsers.collectAsLazyPagingItems() + val onFollowClick: (LikeUser) -> Unit = { user -> + if (!user.follow) { + followViewModel.requestFollow( + followerId = user.memberId, + isFollower = false + ) + } else { + followViewModel.requestUnfollow( + followerId = user.memberId, + isFollower = true + ) + } + } + + val followSuccess by followViewModel.followingFollowSuccess.observeAsState() + val unFollowSuccess by followViewModel.followerUnfollowSuccess.observeAsState() + + LaunchedEffect(likeCount, followSuccess, unFollowSuccess) { + postViewModel.requestPostDetail(postId = postId) + postViewModel.requestPostLikeUsers(postId = postId) + } + + if (currentMemberId != null) { + PostLikeUsersScreen( + currentMemberId = currentMemberId, + likeCount = likeCount.value, + likeUsers = likeUserList, + onProfileClick = onProfileClick, + onFollowClick = onFollowClick, + onBackClick = onBackClick + ) + } else { + // TODO ์—๋Ÿฌ ์ฒ˜๋ฆฌ + } +} + +@Composable +private fun PostLikeUsersScreen( + currentMemberId: String, + likeCount: Int, + likeUsers: LazyPagingItems, + onProfileClick: (String) -> Unit, + onFollowClick: (LikeUser) -> Unit, + onBackClick: () -> Unit +) { + Scaffold( + topBar = { + TopNavigation( + leftIcon = { + IconButton( + onClick = { onBackClick() }, + modifier = Modifier + .indication(interactionSource = remember { MutableInteractionSource() }, indication = null) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back_sign), + contentDescription = "back sign", + tint = Dark + ) + } + }, + title = stringResource(id = R.string.like), + titleAlignment = TopNavigationAlign.CENTER + ) + } + ) { innerPadding -> + LazyColumn( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize() + .padding(innerPadding), + contentPadding = PaddingValues(horizontal = 18.dp) + ) { + // like count + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val dec = DecimalFormat("#,###") + Text( + text = " ${dec.format(likeCount)} ", + style = DayoTheme.typography.caption1.copy(Primary_23C882), + modifier = Modifier.padding(vertical = 12.dp) + ) + Text( + text = "๊ฐœ์˜ ์ข‹์•„์š”", + style = DayoTheme.typography.caption1.copy(Gray2_767B83), + modifier = Modifier.padding(vertical = 12.dp) + ) + } + } + + // users + items( + count = likeUsers.itemCount, + key = likeUsers.itemKey() + ) { index -> + likeUsers[index]?.let { user -> + LikeUserItem( + likeUser = user, + isMine = user.memberId == currentMemberId, + onProfileClick = onProfileClick, + onFollowClick = { onFollowClick(user) } + ) + } + } + } + } +} + + +@Composable +private fun LikeUserItem( + likeUser: LikeUser, + isMine: Boolean, + onProfileClick: (String) -> Unit, + onFollowClick: (LikeUser) -> Unit +) { + Surface( + color = colorResource(id = R.color.white_FFFFFF), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 20.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.wrapContentHeight() + ) { + // profile + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data("${BuildConfig.BASE_URL}/images/${likeUser.profileImg}") + .build(), + contentDescription = "${likeUser.nickname} + profile", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(36.dp) + .clip(shape = CircleShape) + .align(Alignment.CenterVertically) + .clickableSingle( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onProfileClick(likeUser.memberId) } + ) + ) + + // nickname + Text( + text = likeUser.nickname, + style = DayoTheme.typography.b6.copy(Dark), + modifier = Modifier.clickableSingle( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onProfileClick(likeUser.memberId) } + ) + ) + + Spacer(modifier = Modifier.weight(1f)) + + if (!isMine) { + if (!likeUser.follow) { + FilledButton( + onClick = { onFollowClick(likeUser) }, + modifier = Modifier.height(36.dp), + label = stringResource(id = R.string.follow_yet), + icon = { Icon(Icons.Filled.Add, stringResource(id = R.string.follow_yet)) }, + isTonal = true + ) + } else { + DayoOutlinedButton( + onClick = { onFollowClick(likeUser) }, + modifier = Modifier.height(36.dp), + label = stringResource(id = R.string.follow_already), + icon = { Icon(Icons.Filled.Check, stringResource(id = R.string.follow_already)) }) + } + } + } + } +} + +@Preview +@Composable +private fun PreviewPostLikeUsersScreen() { + val likeUsers = flowOf(PagingData.empty()).collectAsLazyPagingItems() + PostLikeUsersScreen( + currentMemberId = "", + likeCount = 1, + likeUsers = likeUsers, + onProfileClick = { }, + onFollowClick = { }, + onBackClick = { } + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/post/PostNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostNavigation.kt new file mode 100644 index 00000000..537f5090 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostNavigation.kt @@ -0,0 +1,68 @@ +package daily.dayo.presentation.screen.post + +import androidx.compose.material3.SnackbarHostState +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument + +fun NavController.navigatePost(postId: Long) { + navigate(PostRoute.postDetail(postId)) +} + +fun NavController.navigatePostLikeUsers(postId: Long) { + navigate(PostRoute.postLikeUsers(postId)) +} + +fun NavGraphBuilder.postNavGraph( + snackBarHostState: SnackbarHostState, + onPostEditClick: (Long) -> Unit, + onProfileClick: (String) -> Unit, + onPostLikeUsersClick: (Long) -> Unit, + onPostHashtagClick: (String) -> Unit, + onBackClick: () -> Unit +) { + composable( + route = PostRoute.postDetailRoute, + arguments = listOf( + navArgument("postId") { + type = NavType.LongType + } + ) + ) { navBackStackEntry -> + navBackStackEntry.arguments?.getLong("postId")?.let { postId -> + PostScreen( + postId = postId, + snackBarHostState = snackBarHostState, + onPostEditClick = onPostEditClick, + onProfileClick = onProfileClick, + onPostLikeUsersClick = onPostLikeUsersClick, + onPostHashtagClick = onPostHashtagClick, + onBackClick = onBackClick + ) + } + } + + composable( + route = PostRoute.postLikeUsersRoute, + arguments = listOf( + navArgument("postId") { + type = NavType.LongType + } + ) + ) { navBackStackEntry -> + navBackStackEntry.arguments?.getLong("postId")?.let { postId -> + PostLikeUsersScreen(postId = postId, onBackClick = { onBackClick() }, onProfileClick = onProfileClick) + } + } +} + +object PostRoute { + private const val route: String = "post" + const val postDetailRoute = "$route/{postId}" + const val postLikeUsersRoute = "$route/likeUsers/{postId}" + + fun postDetail(postId: Long) = "$route/$postId" + fun postLikeUsers(postId: Long) = "$route/likeUsers/$postId" +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/post/PostScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostScreen.kt new file mode 100644 index 00000000..a6225d2e --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/post/PostScreen.kt @@ -0,0 +1,426 @@ +package daily.dayo.presentation.screen.post + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import daily.dayo.domain.model.Comment +import daily.dayo.domain.model.Comments +import daily.dayo.domain.model.PostDetail +import daily.dayo.domain.model.SearchUser +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Event +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.view.CommentListView +import daily.dayo.presentation.view.CommentMentionSearchView +import daily.dayo.presentation.view.CommentReplyDescriptionView +import daily.dayo.presentation.view.CommentTextField +import daily.dayo.presentation.view.DEFAULT_POST +import daily.dayo.presentation.view.DetailPostView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.dialog.CommentReportDialog +import daily.dayo.presentation.view.dialog.DEFAULT_COMMENTS +import daily.dayo.presentation.viewmodel.PostViewModel +import daily.dayo.presentation.viewmodel.ReportViewModel +import daily.dayo.presentation.viewmodel.SearchViewModel +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import java.text.DecimalFormat + +@Composable +fun PostScreen( + postId: Long, + snackBarHostState: SnackbarHostState, + onPostEditClick: (Long) -> Unit, + onProfileClick: (String) -> Unit, + onPostLikeUsersClick: (Long) -> Unit, + onPostHashtagClick: (String) -> Unit, + onBackClick: () -> Unit, + postViewModel: PostViewModel = hiltViewModel(), + searchViewModel: SearchViewModel = hiltViewModel(), + reportViewModel: ReportViewModel = hiltViewModel() +) { + val postState = postViewModel.postDetail.observeAsState() + var post by remember { mutableStateOf(DEFAULT_POST) } + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val keyboardController = LocalSoftwareKeyboardController.current + + // post option + val onPostModifyClick: (Long) -> Unit = { onPostEditClick(postId) } + val onPostDeleteClick: (Long) -> Unit = { postViewModel.requestDeletePost(postId) } + val postDeleteSuccess by postViewModel.postDeleteSuccess.collectAsStateWithLifecycle(false) + + // comment + val commentState = postViewModel.postComments.observeAsState() + val commentText = remember { mutableStateOf(TextFieldValue("")) } + val showMentionSearchView = remember { mutableStateOf(false) } + val commentFocusRequester = FocusRequester() + + // comment option + val onClickCommentDelete: (Long) -> Unit = { commentId -> + postViewModel.requestDeletePostComment(commentId) + } + val postCommentDeleteSuccess by postViewModel.postCommentDeleteSuccess.observeAsState(Event(false)) + if (postCommentDeleteSuccess.getContentIfNotHandled() == true) { + postViewModel.requestPostComment(postId) + SideEffect { + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.comment_delete_message)) + } + } + } + var showReportDialog by remember { mutableStateOf(false) } + var reportCommentId by remember { mutableStateOf(null) } + val onClickCommentReport: (Long) -> Unit = { commentId -> + reportCommentId = commentId + showReportDialog = true + } + + // search follow user + val userResults = searchViewModel.searchFollowUserList.collectAsLazyPagingItems() + val userSearchKeyword = remember { mutableStateOf("") } + val mentionedMemberIds = remember { mutableStateListOf() } + LaunchedEffect(userSearchKeyword.value) { + searchViewModel.searchFollowUser(userSearchKeyword.value) + } + val onClickFollowUser: (SearchUser) -> Unit = { mentionUser -> + with(commentText.value) { + val cursorPos = selection.start + val start = text.lastIndexOf('@', cursorPos - 1) + val end = text.indexOf(' ', start).let { if (it == -1) text.length else it } + val newText = text.replaceRange(start, end, "@${mentionUser.nickname} ") + commentText.value = TextFieldValue( + text = newText, + selection = TextRange(start + mentionUser.nickname.length + 2) + ) + userSearchKeyword.value = "" + mentionedMemberIds.add(mentionUser) + showMentionSearchView.value = false + } + } + + // create comment + val replyCommentState = remember { mutableStateOf?>(null) } // parent comment Id, reply comment + val onClickPostComment: () -> Unit = { + if (replyCommentState.value == null) { + if (commentText.value.text.isNotBlank()) { + postViewModel.requestCreatePostComment(commentText.value.text, postId, mentionedMemberIds) + } + } else { + postViewModel.requestCreatePostCommentReply(replyCommentState.value!!, commentText.value.text, postId, mentionedMemberIds) + } + } + val onClickCommentReply: (Pair?) -> Unit = { reply -> + // set reply comment state + replyCommentState.value = reply + + // show mention user name + val replyUsername = "@${replyCommentState.value?.second?.nickname} " + commentText.value = TextFieldValue(text = replyUsername, selection = TextRange(replyUsername.length)) + commentFocusRequester.requestFocus() + } + val commentEnabled = if (replyCommentState.value == null) { + commentText.value.text.isNotBlank() + } else { + val replyUsername = "@${replyCommentState.value?.second?.nickname} " + commentText.value.text.drop(replyUsername.length).isNotBlank() + } + + // clear comment + val clearComment = { + commentText.value = TextFieldValue("") + replyCommentState.value = null + mentionedMemberIds.clear() + } + val onClickCancelReply: () -> Unit = { + clearComment() + } + val postCommentCreateSuccess by postViewModel.postCommentCreateSuccess.observeAsState(Event(false)) + if (postCommentCreateSuccess.getContentIfNotHandled() == true) { + clearComment() + keyboardController?.hide() + postViewModel.requestPostComment(postId) + } + + LaunchedEffect(Unit) { + postViewModel.requestPostDetail(postId) + postViewModel.requestPostComment(postId) + } + + LaunchedEffect(postState.value) { + post = when (postState.value?.status) { + Status.SUCCESS -> postState.value?.data ?: DEFAULT_POST + else -> DEFAULT_POST + } + } + + LaunchedEffect(postDeleteSuccess) { + if (postDeleteSuccess) { + onBackClick() + } + } + + PostScreen( + postId = postId, + post = post, + comments = when (commentState.value?.status) { + Status.SUCCESS -> commentState.value?.data ?: DEFAULT_COMMENTS + else -> DEFAULT_COMMENTS + }, + currentMemberId = postViewModel.getCurrentUserInfo().memberId, + commentEnabled = commentEnabled, + snackBarHostState = snackBarHostState, + commentText = commentText, + replyCommentState = replyCommentState, + userSearchKeyword = userSearchKeyword, + showMentionSearchView = showMentionSearchView, + userResults = userResults, + commentFocusRequester = commentFocusRequester, + onClickPostComment = onClickPostComment, + onClickProfile = onProfileClick, + onClickPost = { }, + onPostModifyClick = onPostModifyClick, + onPostDeleteClick = onPostDeleteClick, + onClickLikePost = { + postViewModel.toggleLikePost(postId = postId, currentHeart = post.heart) + }, + onClickBookmark = { + postViewModel.toggleBookmarkPostDetail(postId = postId, currentBookmark = post.bookmark) + }, + onClickReport = { reason -> + reportViewModel.requestSavePostReport(reason, postId) + }, + onPostLikeUsersClick = onPostLikeUsersClick, + onPostHashtagClick = onPostHashtagClick, + onClickCommentReply = onClickCommentReply, + onClickCommentDelete = onClickCommentDelete, + onClickCommentReport = onClickCommentReport, + onClickFollowUser = onClickFollowUser, + onClickCancelReply = onClickCancelReply, + onBackClick = onBackClick + ) + + reportCommentId?.let { commentId -> + if (showReportDialog) { + CommentReportDialog( + onClickCancel = { showReportDialog = !showReportDialog }, + onClickConfirm = { reason -> + reportViewModel.requestSaveCommentReport(reason, commentId) + showReportDialog = !showReportDialog + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.comment_report_message)) + } + } + ) + } + } +} + +@Composable +private fun PostScreen( + postId: Long, + post: PostDetail, + comments: Comments, + currentMemberId: String?, + commentEnabled: Boolean, + snackBarHostState: SnackbarHostState, + commentText: MutableState, + replyCommentState: MutableState?>, + userSearchKeyword: MutableState, + showMentionSearchView: MutableState, + userResults: LazyPagingItems, + commentFocusRequester: FocusRequester, + onClickPostComment: () -> Unit, + onClickProfile: (String) -> Unit, + onClickPost: () -> Unit, + onPostModifyClick: (Long) -> Unit, + onPostDeleteClick: (Long) -> Unit, + onClickLikePost: () -> Unit, + onClickBookmark: () -> Unit, + onClickReport: (String) -> Unit, + onPostLikeUsersClick: (Long) -> Unit, + onPostHashtagClick: (String) -> Unit, + onClickCommentReply: (Pair) -> Unit, + onClickCommentDelete: (Long) -> Unit, + onClickCommentReport: (Long) -> Unit, + onClickFollowUser: (SearchUser) -> Unit, + onClickCancelReply: () -> Unit, + onBackClick: () -> Unit +) { + Scaffold( + topBar = { + TopNavigation( + leftIcon = { + IconButton(onClick = onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_x_sign), + contentDescription = stringResource(id = R.string.back_sign), + tint = Dark + ) + } + } + ) + } + ) { innerPadding -> + val coroutineScope = rememberCoroutineScope() + val lazyListState = rememberLazyListState() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + LazyColumn( + state = lazyListState, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + item { + DetailPostView( + postId = postId, + post = post, + commentCount = comments.count, + currentMemberId = currentMemberId, + snackBarHostState = snackBarHostState, + onClickProfile = onClickProfile, + onClickPost = onClickPost, + onPostModifyClick = onPostModifyClick, + onPostDeleteClick = onPostDeleteClick, + onClickLikePost = onClickLikePost, + onClickComment = { + coroutineScope.launch { + commentFocusRequester.requestFocus() + lazyListState.animateScrollToItem(lazyListState.layoutInfo.totalItemsCount - 1) + } + }, + onClickBookmark = onClickBookmark, + onClickReport = onClickReport, + onPostLikeUsersClick = onPostLikeUsersClick, + onPostHashtagClick = onPostHashtagClick, + modifier = Modifier.fillMaxWidth() + ) + } + + item { + Spacer(Modifier.height(12.dp)) + Row(Modifier.padding(horizontal = 18.dp)) { + val dec = DecimalFormat("#,###") + Text(text = " ${dec.format(comments.count)} ", style = DayoTheme.typography.caption1, color = if (comments.count != 0) Primary_23C882 else Gray4_C5CAD2) + Text(text = stringResource(id = R.string.post_comment_count_message), style = DayoTheme.typography.caption1.copy(Gray2_767B83)) + } + } + + item { + Spacer(Modifier.height(12.dp)) + CommentListView( + postComments = comments, + onClickProfile = onClickProfile, + onClickReply = onClickCommentReply, + onClickDelete = onClickCommentDelete, + onClickReport = onClickCommentReport, + currentMemberId = currentMemberId, + modifier = Modifier.padding(horizontal = 18.dp) + ) + } + } + if (showMentionSearchView.value) CommentMentionSearchView(userResults, onClickFollowUser) + if (replyCommentState.value != null) CommentReplyDescriptionView(replyCommentState, onClickCancelReply) + CommentTextField( + enabled = commentEnabled, + commentText = commentText, + replyCommentState = replyCommentState, + userSearchKeyword = userSearchKeyword, + showMentionSearchView = showMentionSearchView, + focusRequester = commentFocusRequester, + onClickPostComment = onClickPostComment + ) + } + } +} + +@Preview +@Composable +private fun PreviewPostScreen() { + val commentText = remember { mutableStateOf(TextFieldValue("")) } + val replyCommentState = remember { mutableStateOf?>(null) } // parent comment Id, reply comment + val userSearchKeyword = remember { mutableStateOf("") } + val showMentionSearchView = remember { mutableStateOf(false) } + val userResults = flowOf(PagingData.empty()).collectAsLazyPagingItems() + + DayoTheme { + PostScreen( + postId = 0L, + post = DEFAULT_POST, + comments = DEFAULT_COMMENTS, + currentMemberId = "", + commentEnabled = commentText.value.text.isNotBlank(), + snackBarHostState = SnackbarHostState(), + commentText = commentText, + replyCommentState = replyCommentState, + userSearchKeyword = userSearchKeyword, + showMentionSearchView = showMentionSearchView, + userResults = userResults, + commentFocusRequester = FocusRequester(), + onClickPostComment = { }, + onClickProfile = { }, + onClickPost = { }, + onPostModifyClick = { }, + onPostDeleteClick = { }, + onClickLikePost = { }, + onClickBookmark = { }, + onClickReport = { }, + onPostLikeUsersClick = { }, + onPostHashtagClick = { }, + onClickCommentReply = { }, + onClickCommentDelete = { }, + onClickCommentReport = { }, + onClickFollowUser = { }, + onClickCancelReply = { }, + onBackClick = { } + ) + } +} + diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileNavigation.kt new file mode 100644 index 00000000..60b237c5 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileNavigation.kt @@ -0,0 +1,43 @@ +package daily.dayo.presentation.screen.profile + +import androidx.compose.material3.SnackbarHostState +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument + +fun NavController.navigateProfile(memberId: String) { + navigate(ProfileRoute.profile(memberId)) +} + +fun NavGraphBuilder.profileNavGraph( + snackBarHostState: SnackbarHostState, + onFollowMenuClick: (String, Int) -> Unit, + onFolderClick: (Long) -> Unit, + onPostClick: (Long) -> Unit, + onBackClick: () -> Unit +) { + composable( + route = ProfileRoute.profile("{memberId}"), + arguments = listOf( + navArgument("memberId") { + type = NavType.StringType + } + ) + ) { navBackStackEntry -> + val memberId = navBackStackEntry.arguments?.getString("memberId") ?: "" + ProfileScreen( + externalSnackBarHostState = snackBarHostState, + memberId = memberId, + onFollowMenuClick = onFollowMenuClick, + onFolderClick = onFolderClick, + onBackClick = onBackClick + ) + } +} + +object ProfileRoute { + const val route = "profile" + fun profile(memberId: String) = "$route/$memberId" +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileScreen.kt new file mode 100644 index 00000000..e72175f6 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/profile/ProfileScreen.kt @@ -0,0 +1,493 @@ +package daily.dayo.presentation.screen.profile + +import android.content.Context +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import daily.dayo.domain.model.Folder +import daily.dayo.domain.model.Profile +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.view.DayoOutlinedButton +import daily.dayo.presentation.view.FilledButton +import daily.dayo.presentation.view.FolderView +import daily.dayo.presentation.view.ProfileDropdownMenu +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.dialog.ConfirmDialog +import daily.dayo.presentation.view.dialog.UserReportDialog +import daily.dayo.presentation.viewmodel.FolderViewModel +import daily.dayo.presentation.viewmodel.ProfileViewModel +import daily.dayo.presentation.viewmodel.ReportViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun ProfileScreen( + externalSnackBarHostState: SnackbarHostState, + memberId: String, + onFollowMenuClick: (String, Int) -> Unit, + onFolderClick: (Long) -> Unit, + onBackClick: () -> Unit, + profileViewModel: ProfileViewModel = hiltViewModel(), + folderViewModel: FolderViewModel = hiltViewModel(), + reportViewModel: ReportViewModel = hiltViewModel() +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val snackBarHostState = remember { SnackbarHostState() } + val lifecycleOwner = LocalLifecycleOwner.current + val profileInfo = profileViewModel.profileInfo.observeAsState() + val folderList = folderViewModel.folderList.observeAsState() + val onFollowClick: () -> Unit = { + profileViewModel.toggleFollow(memberId, profileInfo.value?.data?.follow ?: false) + } + val onClickUserReport: (String) -> Unit = { reason -> + profileInfo.value?.data?.memberId?.let { reportViewModel.requestSaveMemberReport(reason, it) } + } + val onClickUserBlockSuccess by profileViewModel.blockSuccess.collectAsStateWithLifecycle() + val onClickUserBlock: (String) -> Unit = { memberId -> + profileInfo.value?.data?.memberId?.let { profileViewModel.requestBlockMember(it) } + } + + LaunchedEffect(onClickUserBlockSuccess) { + when (onClickUserBlockSuccess) { + Status.SUCCESS -> { + coroutineScope.launch { + externalSnackBarHostState.showSnackbar(context.getString(R.string.other_profile_block_success_message)) + } + onBackClick() + } + Status.ERROR -> { + } + else -> {} + } + } + + LaunchedEffect(Unit) { + profileViewModel.requestOtherProfile(memberId) + folderViewModel.requestUserFolderList(memberId) + + launch { + profileViewModel.followSuccess + .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) + .collect { followSuccess -> + if (followSuccess) { + profileViewModel.requestOtherProfile(memberId) + } + } + } + + launch { + profileViewModel.unfollowSuccess + .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) + .collect { unfollowSuccess -> + if (unfollowSuccess) { + profileViewModel.requestOtherProfile(memberId) + } + } + } + } + + ProfileScreen( + context = context, + coroutineScope = coroutineScope, + snackBarHostState = snackBarHostState, + profile = profileInfo.value?.data ?: DEFAULT_PROFILE, + folderList = folderList.value?.data ?: emptyList(), + onFollowClick = onFollowClick, + onFollowMenuClick = onFollowMenuClick, + onFolderClick = onFolderClick, + onClickUserReport = onClickUserReport, + onClickUserBlock = onClickUserBlock, + onBackClick = onBackClick + ) +} + +@Composable +private fun ProfileScreen( + context: Context, + coroutineScope: CoroutineScope, + snackBarHostState: SnackbarHostState, + profile: Profile, + folderList: List, + onFollowClick: () -> Unit, + onFollowMenuClick: (String, Int) -> Unit, + onFolderClick: (Long) -> Unit, + onClickUserReport: (String) -> Unit, + onClickUserBlock: (String) -> Unit, + onBackClick: () -> Unit, +) { + var showReportDialog by remember { mutableStateOf(false) } + var showBlockDialog by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + ProfileTopNavigation( + onUserReportClick = { showReportDialog = true }, + onUserBlockClick = { showBlockDialog = true }, + onBackClick = onBackClick, + ) + }, + snackbarHost = { SnackbarHost(snackBarHostState) }, + content = { contentPadding -> + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize() + .padding(contentPadding) + .padding(horizontal = 20.dp), + ) { + item(span = { GridItemSpan(2) }) { + UserProfile(profile, onFollowMenuClick) + } + + item(span = { GridItemSpan(2) }) { + Column { + Spacer(Modifier.height(8.dp)) + UserFollowButton(profile.follow ?: false, onFollowClick) + Spacer(Modifier.height(20.dp)) + } + } + + items(folderList) { folder -> + UserDiary(folder, onFolderClick) + } + } + } + ) + + if (showReportDialog) { + UserReportDialog( + onClickCancel = { showReportDialog = false }, + onClickConfirm = { reason -> + onClickUserReport(reason) + showReportDialog = false + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.report_user_alert_message)) + } + } + ) + } + + if (showBlockDialog) { + ConfirmDialog( + title = stringResource(id = R.string.other_profile_block_message), + description = stringResource(id = R.string.other_profile_block_explanation_message), + onClickCancel = { showBlockDialog = false }, + onClickConfirm = { + onClickUserBlock(profile.memberId ?: "") + showBlockDialog = false + }, + ) + } +} + +@Composable +private fun UserFollowButton( + isFollow: Boolean, + onFollowClick: () -> Unit +) { + if (isFollow) { + DayoOutlinedButton( + onClick = onFollowClick, + label = stringResource(id = R.string.follow_already), + modifier = Modifier.fillMaxWidth(), + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_check_sign_gray), + contentDescription = stringResource(R.string.follow_already_icon_description), + modifier = Modifier.size(20.dp) + ) + } + ) + } else { + FilledButton( + onClick = onFollowClick, + label = stringResource(id = R.string.follow_yet), + modifier = Modifier.fillMaxWidth(), + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_plus_sign_green), + contentDescription = stringResource(R.string.follow_yet_icon_description), + modifier = Modifier.size(20.dp) + ) + }, + isTonal = true + ) + } +} + +@Composable +private fun UserProfile( + profile: Profile, + onFollowMenuClick: (String, Int) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(DayoTheme.colorScheme.background) + .padding(top = 8.dp, bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // profile image + RoundImageView( + context = LocalContext.current, + imageUrl = "${BuildConfig.BASE_URL}/images/${profile.profileImg}", + imageDescription = "profile image", + roundSize = 24.dp, + modifier = Modifier + .size(48.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { } + ) + ) + + // nickname, email + Column( + modifier = Modifier + .weight(1f) + .padding(start = 8.dp) + ) { + Text( + text = profile.nickname, + style = DayoTheme.typography.h1.copy( + color = Dark, + fontWeight = FontWeight.SemiBold + ) + ) + + Text( + text = profile.email, + style = DayoTheme.typography.caption5.copy( + color = Gray4_C5CAD2 + ) + ) + } + + // follower + Column( + modifier = Modifier.clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { profile.memberId?.let { onFollowMenuClick(it, FOLLOWER) } } + ), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_follower), + contentDescription = stringResource(id = R.string.follower), + modifier = Modifier.size(12.dp) + ) + + Text( + text = stringResource(id = R.string.follower), + style = DayoTheme.typography.caption4.copy(color = Gray1_50545B) + ) + } + Text( + text = "${profile.followerCount}", + style = DayoTheme.typography.b6.copy(color = Gray1_50545B) + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + + // following + Column( + modifier = Modifier.clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { profile.memberId?.let { onFollowMenuClick(it, FOLLOWING) } } + ), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.ic_following), + contentDescription = stringResource(id = R.string.following), + modifier = Modifier.size(12.dp) + ) + + Text( + text = stringResource(id = R.string.following), + style = DayoTheme.typography.caption4.copy(color = Gray1_50545B) + ) + } + + Text( + text = "${profile.followingCount}", + style = DayoTheme.typography.b6.copy(color = Gray1_50545B) + ) + } + + Spacer(modifier = Modifier.width(20.dp)) + } +} + +@Composable +private fun UserDiary(folder: Folder, onFolderClick: (Long) -> Unit) { + FolderView( + folder = folder, + onClickFolder = onFolderClick, + modifier = Modifier.padding(bottom = 12.dp) + ) +} + +@Composable +private fun ProfileTopNavigation( + onUserReportClick: () -> Unit, + onUserBlockClick: () -> Unit, + onBackClick: () -> Unit, +) { + var showProfileOption by remember { mutableStateOf(false) } + + TopNavigation( + leftIcon = { + IconButton(onClick = onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_x_sign), + contentDescription = stringResource(id = R.string.back_sign), + tint = Dark + ) + } + }, + rightIcon = { + Box { + IconButton( + onClick = { showProfileOption = true } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_option_horizontal), + contentDescription = "profile option", + modifier = Modifier.padding(8.dp), + tint = Gray2_767B83, + ) + } + } + + ProfileDropdownMenu( + expanded = showProfileOption, + onDismissRequest = { showProfileOption = !showProfileOption }, + onUserReportClick = { + onUserReportClick() + showProfileOption = !showProfileOption + }, + onUserBlockClick = { + onUserBlockClick() + showProfileOption = !showProfileOption + } + ) + } + ) +} + +@Preview +@Composable +private fun PreviewProfileScreen() { + ProfileScreen( + context = LocalContext.current, + coroutineScope = rememberCoroutineScope(), + snackBarHostState = remember { SnackbarHostState() }, + profile = DEFAULT_PROFILE, + folderList = emptyList(), + onFollowClick = { }, + onFollowMenuClick = { _, _ -> }, + onFolderClick = { }, + onClickUserReport = { }, + onClickUserBlock = { }, + onBackClick = { }, + ) +} + +@Preview +@Composable +private fun PreviewUserFollowButton() { + Column { + UserFollowButton( + isFollow = true, + onFollowClick = {} + ) + + UserFollowButton( + isFollow = false, + onFollowClick = {} + ) + } +} + +private const val FOLLOWER = 0 +private const val FOLLOWING = 1 +private val DEFAULT_PROFILE = Profile( + memberId = null, + email = "", + nickname = "nickname", + profileImg = "", + postCount = 10, + followerCount = 10, + followingCount = 10, + follow = null, +) + diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/rules/RuleNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/rules/RuleNavigation.kt new file mode 100644 index 00000000..063d3afd --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/rules/RuleNavigation.kt @@ -0,0 +1,21 @@ +package daily.dayo.presentation.screen.rules + +import androidx.navigation.NavController + +enum class RuleType(val koreanName: String, val fileName: String) { + PRIVACY_POLICY("๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ", "privacy"), + TERMS_AND_CONDITIONS("์ด์šฉ์•ฝ๊ด€", "terms") +} + +fun NavController.navigateRules(ruleType: RuleType) { + when (ruleType) { + RuleType.PRIVACY_POLICY -> this.navigate(RuleRoute.privacyPolicy) + RuleType.TERMS_AND_CONDITIONS -> this.navigate(RuleRoute.termsAndConditions) + } +} + +object RuleRoute { + const val route = "rule" + const val privacyPolicy = "${route}/privacy_policy" + const val termsAndConditions = "${route}/terms_and_conditions" +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/rules/RuleScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/rules/RuleScreen.kt new file mode 100644 index 00000000..292e35a7 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/rules/RuleScreen.kt @@ -0,0 +1,97 @@ +package daily.dayo.presentation.screen.rules + +import android.view.KeyEvent +import android.view.View +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.viewinterop.AndroidView +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign + +@Composable +internal fun RuleRoute( + onBackClick: () -> Unit = {}, + ruleType: RuleType +) { + RuleScreen(onBackClick = onBackClick, ruleType = ruleType) +} + +@Preview +@Composable +fun RuleScreen( + onBackClick: () -> Unit = {}, + ruleType: RuleType = RuleType.PRIVACY_POLICY +) { + Surface( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize() + ) { + + Scaffold( + topBar = { + RuleActionbarLayout( + onBackClick = onBackClick, + ruleType = ruleType + ) + } + ) { paddingValues -> + AndroidView( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + factory = { context -> + WebView(context).apply { + webViewClient = WebViewClient() + settings.javaScriptEnabled = false + overScrollMode = View.OVER_SCROLL_NEVER + loadUrl("${BuildConfig.BASE_URL}/${ruleType.fileName}.html") + + setOnKeyListener( + View.OnKeyListener { v, keyCode, event -> + if (event.action != KeyEvent.ACTION_DOWN) return@OnKeyListener true + if (keyCode == KeyEvent.KEYCODE_BACK) { + onBackClick() + return@OnKeyListener true + } + false + } + ) + } + } + ) + } + } +} + +@Composable +fun RuleActionbarLayout( + onBackClick: () -> Unit = {}, + ruleType: RuleType +) { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = { onBackClick() }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + title = ruleType.koreanName, + titleAlignment = TopNavigationAlign.CENTER + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchNavigation.kt new file mode 100644 index 00000000..77492aff --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchNavigation.kt @@ -0,0 +1,71 @@ +package daily.dayo.presentation.screen.search + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument + +fun NavController.navigateSearch() { + navigate(SearchRoute.route) +} + +fun NavController.navigateSearchResult(searchKeyword: String) { + navigate(SearchRoute.resultSearch(searchKeyword)) +} + +fun NavController.navigateSearchPostHashtag(hashtag: String) { + navigate(SearchRoute.searchHashtag(hashtag)) +} + +fun NavGraphBuilder.searchNavGraph( + onBackClick: () -> Unit, + onSearch: (String) -> Unit, + onProfileClick: (String) -> Unit, + onPostClick: (Long) -> Unit, + navController: NavController, +) { + composable(route = SearchRoute.route) { + SearchRoute( + onBackClick = onBackClick, + onSearch = onSearch + ) + } + + composable( + route = SearchRoute.resultSearch("{searchKeyword}"), + arguments = listOf(navArgument("searchKeyword") { type = NavType.StringType }) + ) { backStackEntry -> + val searchKeyword = backStackEntry.arguments?.getString("searchKeyword") ?: "" + SearchResultRoute( + searchKeyword = searchKeyword, + onBackClick = onBackClick, + onPostClick = onPostClick, + onClickProfile = onProfileClick, + navController = navController, + ) + } + + composable( + route = SearchRoute.searchHashtag("{hashtag}"), + arguments = listOf( + navArgument("hashtag") { + type = NavType.StringType + } + ) + ) { navBackStackEntry -> + val hashtag = navBackStackEntry.arguments?.getString("hashtag") ?: "" + SearchPostHashtagScreen( + hashtag = hashtag, + onBackClick = onBackClick, + onPostClick = onPostClick + ) + } +} + +object SearchRoute { + const val route = "search" + + fun resultSearch(searchKeyword: String) = "$route/$searchKeyword" + fun searchHashtag(hashtag: String) = "$route/hashtag/$hashtag" +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchPostHashtagScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchPostHashtagScreen.kt new file mode 100644 index 00000000..d6ab802a --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchPostHashtagScreen.kt @@ -0,0 +1,165 @@ +package daily.dayo.presentation.screen.search + +import androidx.compose.foundation.background +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.viewmodel.SearchViewModel + +@Composable +fun SearchPostHashtagScreen( + hashtag: String, + onBackClick: () -> Unit, + onPostClick: (Long) -> Unit, + searchViewModel: SearchViewModel = hiltViewModel() +) { + val isLatest by rememberSaveable { mutableStateOf(true) } // TODO api ์ˆ˜์ • ํ›„ ๊ตฌํ˜„ + val hashtagPosts = searchViewModel.searchTagList.collectAsLazyPagingItems() + val hashtagPostsCount by searchViewModel.searchTagTotalCount.collectAsStateWithLifecycle(0) + + LaunchedEffect(Unit) { + with(searchViewModel) { + searchHashtag(hashtag = hashtag) + } + } + + Scaffold( + topBar = { + TopNavigation( + leftIcon = { + IconButton( + onClick = onBackClick, + modifier = Modifier + .indication(interactionSource = remember { MutableInteractionSource() }, indication = null) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back_sign), + contentDescription = "back sign", + tint = Gray1_50545B + ) + } + }, + title = "#$hashtag", + titleAlignment = TopNavigationAlign.CENTER + ) + } + ) { innerPadding -> + LazyVerticalGrid( + columns = GridCells.Fixed(2), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 18.dp), + modifier = Modifier + .fillMaxSize() + .background(DayoTheme.colorScheme.background) + .padding(innerPadding) + ) { + // description + item(span = { GridItemSpan(2) }) { + SearchResultDescription(hashtagPostsCount, isLatest) + } + + // posts + items( + count = hashtagPosts.itemCount, + key = hashtagPosts.itemKey() + ) { index -> + val item = hashtagPosts[index] + item.let { post -> + RoundImageView( + context = LocalContext.current, + imageUrl = "${BuildConfig.BASE_URL}/images/${post?.thumbnailImage}", + imageDescription = "searched Image", + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { post?.postId?.let { onPostClick(it) } } + ) + .padding(bottom = 4.dp) + ) + } + } + } + } +} + +@Composable +private fun SearchResultDescription(resultCount: Int, isLatest: Boolean) { + Row(modifier = Modifier.fillMaxWidth()) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(vertical = 12.dp) + .weight(1f) + ) { + Text( + style = DayoTheme.typography.caption1.copy(color = Primary_23C882), + text = "$resultCount", + modifier = Modifier.padding(end = 2.dp) + ) + Text( + style = DayoTheme.typography.caption1.copy(color = Gray2_767B83), + text = "๊ฐœ์˜ ํƒœ๊ทธ" + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + modifier = Modifier + .padding(vertical = 12.dp) + .weight(1f) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_swap_vertical), + contentDescription = if (isLatest) "์ตœ์‹ ์ˆœ" else "์˜ค๋ž˜๋œ์ˆœ", + tint = Gray1_50545B + ) + Text( + style = DayoTheme.typography.caption1.copy(color = Gray2_767B83), + text = if (isLatest) "์ตœ์‹ ์ˆœ" else "์˜ค๋ž˜๋œ์ˆœ" + ) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt new file mode 100644 index 00000000..70ad81a5 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchResultScreen.kt @@ -0,0 +1,652 @@ +package daily.dayo.presentation.screen.search + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Divider +import androidx.compose.material3.Surface +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.White +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import com.skydoves.landscapist.ImageOptions +import com.skydoves.landscapist.glide.GlideImage +import daily.dayo.domain.model.Search +import daily.dayo.domain.model.SearchHistoryType +import daily.dayo.domain.model.SearchUser +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Event +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.toSp +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray5_E8EAEE +import daily.dayo.presentation.theme.PrimaryL3_F2FBF7 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.viewmodel.AccountViewModel +import daily.dayo.presentation.viewmodel.FollowViewModel +import daily.dayo.presentation.viewmodel.SearchViewModel +import kotlinx.coroutines.launch + +@Composable +internal fun SearchResultRoute( + searchKeyword: String, + onBackClick: () -> Unit, + onPostClick: (Long) -> Unit, + onClickProfile: (String) -> Unit, + navController: NavController, + searchViewModel: SearchViewModel = hiltViewModel(), + followViewModel: FollowViewModel = hiltViewModel(), + accountViewModel: AccountViewModel = hiltViewModel(), +) { + BackHandler { + onBackClick() + } + + val searchKeywordResultsTag = searchViewModel.searchTagList.collectAsLazyPagingItems() + val searchKeywordResultsUser = searchViewModel.searchUserList.collectAsLazyPagingItems() + val searchKeywordResultsTagTotalCount by searchViewModel.searchTagTotalCount.collectAsStateWithLifecycle( + 0 + ) + val searchKeywordResultsUserTotalCount by searchViewModel.searchUserTotalCount.collectAsStateWithLifecycle( + 0 + ) + val followSuccess by followViewModel.followingFollowSuccess.observeAsState(Event(true)) + val unFollowSuccess by followViewModel.followerUnfollowSuccess.observeAsState(Event(true)) + val currentUserMemberId = accountViewModel.getCurrentUserInfo().memberId + + LaunchedEffect(Unit) { + with(searchViewModel) { + searchKeyword(keyword = searchKeyword, SearchHistoryType.TAG) + searchKeyword(keyword = searchKeyword, SearchHistoryType.USER) + } + } + + SearchResultScreen( + searchKeyword = searchKeyword, + searchKeywordResultsTag = searchKeywordResultsTag, + searchKeywordResultsTagTotalCount = searchKeywordResultsTagTotalCount, + searchKeywordResultsUser = searchKeywordResultsUser, + searchKeywordResultsUserTotalCount = searchKeywordResultsUserTotalCount, + onBackClick = onBackClick, + onPostClick = onPostClick, + onSearchClick = { newKeyword -> + navController.navigate(SearchRoute.resultSearch(newKeyword)) { + launchSingleTop = true + popUpTo(SearchRoute.route) { inclusive = false } // ์ด์ „ ๊ฒฐ๊ณผ ํ™”๋ฉด ์Šคํƒ์—์„œ ์ œ๊ฑฐ + restoreState = true + } + }, + onFollowClick = { memberId, isFollower -> + followViewModel.requestFollow( + memberId, + isFollower + ) + }, + onUnFollowClick = { memberId, isFollower -> + followViewModel.requestUnfollow( + memberId, + isFollower + ) + }, + onFollowSuccess = followSuccess.peekContent(), + onUnFollowSuccess = unFollowSuccess.peekContent(), + currentUserMemberId = currentUserMemberId ?: "", + onClickProfile = onClickProfile + ) +} + +@Composable +@Preview +internal fun SearchResultRoutePreview() { + SearchResultScreen( + searchKeyword = "", onBackClick = { }, + searchKeywordResultsTag = null, + searchKeywordResultsUser = null, + searchKeywordResultsTagTotalCount = 0, + searchKeywordResultsUserTotalCount = 0, + onSearchClick = { _ -> }, + onPostClick = { }, + onFollowClick = { _, _ -> }, + onUnFollowClick = { _, _ -> }, + currentUserMemberId = "", + onClickProfile = { } + ) +} + +@Composable +fun SearchResultScreen( + searchKeyword: String, + onBackClick: () -> Unit, + onPostClick: (Long) -> Unit, + searchKeywordResultsTag: LazyPagingItems?, + searchKeywordResultsTagTotalCount: Int, + searchKeywordResultsUser: LazyPagingItems?, + searchKeywordResultsUserTotalCount: Int, + onSearchClick: (String) -> Unit, + onFollowClick: (String, Boolean) -> Unit, + onUnFollowClick: (String, Boolean) -> Unit, + onFollowSuccess: Boolean = true, + onUnFollowSuccess: Boolean = true, + currentUserMemberId: String, + onClickProfile: (String) -> Unit +) { + val pages = listOf("ํƒœ๊ทธ", "์‚ฌ์šฉ์ž") + val coroutineScope = rememberCoroutineScope() + val pagerState = rememberPagerState { 2 } + + Surface( + color = colorResource(id = R.color.white_FFFFFF), + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier.fillMaxSize() + ) { + SearchActionbarLayout( + initialKeyword = searchKeyword, + onBackClick = onBackClick, + onSearchClick = { keyword -> + onSearchClick(keyword) + } + ) + + TabRow( + selectedTabIndex = pagerState.currentPage, + containerColor = White, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]), + color = Primary_23C882, + ) + }, + divider = { Divider(color = Color.Transparent, thickness = 0.dp) }, + modifier = Modifier + .fillMaxWidth() + .padding(18.dp, 0.dp, 18.dp, 0.dp), + ) { + pages.forEachIndexed { index, title -> + Tab( + text = { + Text( + text = title, + style = TextStyle( + fontSize = 14.dp.toSp(), + fontFamily = FontFamily(Font(R.font.pretendard_medium)), + fontWeight = FontWeight(500) + ) + ) + }, + selected = pagerState.currentPage == index, + selectedContentColor = Primary_23C882, + unselectedContentColor = Gray2_767B83, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + }, + ) + } + } + HorizontalPager( + modifier = Modifier, + state = pagerState, + pageSpacing = 0.dp, + userScrollEnabled = false, + reverseLayout = false, + contentPadding = PaddingValues(0.dp), + beyondViewportPageCount = 0, + pageSize = PageSize.Fill, + flingBehavior = PagerDefaults.flingBehavior(state = pagerState), + key = null, + pageContent = { page -> + if (searchKeywordResultsTag?.loadState?.refresh is LoadState.NotLoading || + searchKeywordResultsUser?.loadState?.refresh is LoadState.NotLoading + ) { + when (page) { + 0 -> { + if (searchKeywordResultsTagTotalCount != 0) { + Column { + SearchResultsCount(resultCount = searchKeywordResultsTagTotalCount) + SearchResultTagView( + searchKeywordResultsTag = searchKeywordResultsTag, + onPostClick = onPostClick + ) + } + } else { + SearchResultEmpty() + } + } + + 1 -> { + if (searchKeywordResultsUserTotalCount != 0) { + Column { + SearchResultsCount(resultCount = searchKeywordResultsUserTotalCount) + SearchResultsUserView( + searchKeywordResultsUser = searchKeywordResultsUser, + onFollowClick = onFollowClick, + onUnFollowClick = onUnFollowClick, + onFollowSuccess = onFollowSuccess, + onUnFollowSuccess = onUnFollowSuccess, + currentUserMemberId = currentUserMemberId, + onClickProfile = onClickProfile + ) + } + } else { + SearchResultEmpty() + } + } + + else -> {} + } + } + } + ) + } + } +} + +@Composable +@Preview +fun SearchResultEmpty() { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_empty), + contentDescription = "search tag empty" + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(R.string.search_result_empty_title), + style = DayoTheme.typography.b3 + .copy( + color = Gray3_9FA5AE, + textAlign = TextAlign.Center + ), + ) + Text( + modifier = Modifier.padding(vertical = 2.dp), + text = stringResource(id = R.string.search_result_empty_description), + style = DayoTheme.typography.caption1 + .copy( + color = Gray3_9FA5AE, + textAlign = TextAlign.Center + ), + ) + } +} + +@Composable +@Preview +fun SearchResultsCount(resultCount: Int = 0) { + Surface( + color = colorResource(id = R.color.white_FFFFFF), + modifier = Modifier + .fillMaxWidth() + .height(44.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(0.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 18.dp, vertical = 12.dp) + ) { + Text( + style = TextStyle( + fontSize = 13.sp, + fontFamily = FontFamily(Font(R.font.pretendard_medium)), + fontWeight = FontWeight(500), + color = Primary_23C882 + ), + text = "$resultCount", + modifier = Modifier.padding(end = 2.dp) + ) + Text( + style = TextStyle( + fontSize = 13.sp, + fontFamily = FontFamily(Font(R.font.pretendard_medium)), + fontWeight = FontWeight(500) + ), + text = "๊ฐœ์˜ ๊ฒ€์ƒ‰๊ฒฐ๊ณผ" + ) + } + } +} + +@Composable +fun SearchResultTagView( + searchKeywordResultsTag: LazyPagingItems?, + onPostClick: (Long) -> Unit +) { + val imageInteractionSource = remember { MutableInteractionSource() } + Box( + modifier = Modifier + .fillMaxSize() + ) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 18.dp), + modifier = Modifier + .wrapContentHeight() + ) { + searchKeywordResultsTag?.let { posts -> + items( + count = posts.itemCount, + key = posts.itemKey() + ) { index -> + val item = posts[index] + item?.let { post -> + RoundImageView( + context = LocalContext.current, + imageUrl = "${BuildConfig.BASE_URL}/images/${post.thumbnailImage}", + imageDescription = "searched Image", + modifier = Modifier + .matchParentSize() + .clickableSingle( + interactionSource = imageInteractionSource, + indication = null, + onClick = { onPostClick(post.postId) } + ) + ) + } + } + } + } + } +} + +@Composable +fun SearchResultsUserView( + searchKeywordResultsUser: LazyPagingItems?, + onFollowClick: (String, Boolean) -> Unit, + onUnFollowClick: (String, Boolean) -> Unit, + onFollowSuccess: Boolean = true, + onUnFollowSuccess: Boolean = true, + currentUserMemberId: String, + onClickProfile: (String) -> Unit +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(DayoTheme.colorScheme.background) + ) { + searchKeywordResultsUser?.let { users -> + + items(users.itemCount) { index -> + val item = users[index] + SearchResultUserView( + user = item!!, + onFollowSuccess = onFollowSuccess, + onUnFollowSuccess = onUnFollowSuccess, + currentUserMemberId = currentUserMemberId, + onFollowClick = onFollowClick, + onUnFollowClick = onUnFollowClick, + onClickProfile = onClickProfile + ) + } + } + } +} + +@Composable +@Preview +fun SearchResultUserViewPreview() { + SearchResultUserView( + user = SearchUser( + memberId = "1", + profileImg = "profileImg", + nickname = "nickname", + isFollow = false + ), + onFollowClick = { _, _ -> }, + onUnFollowClick = { _, _ -> }, + onFollowSuccess = true, + onUnFollowSuccess = true, + currentUserMemberId = "1", + onClickProfile = { } + ) +} + +@Composable +fun SearchResultUserView( + user: SearchUser, + onFollowSuccess: Boolean, onUnFollowSuccess: Boolean, + onFollowClick: (String, Boolean) -> Unit, + onUnFollowClick: (String, Boolean) -> Unit, + currentUserMemberId: String, + onClickProfile: (String) -> Unit +) { + Surface( + color = colorResource(id = R.color.white_FFFFFF), + modifier = Modifier.fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickableSingle { onClickProfile(user.memberId) } + .padding(horizontal = 18.dp, vertical = 8.dp) + ) { + SearchResultUserImageLayout(user = user, onClickProfile = onClickProfile) + SearchResultUserNicknameLayout(userNickname = user.nickname) // TODO NICKNAME์„ ๋ฐ›์•„์˜ค๋„๋กํ•ด์•ผ ํ•จ + Spacer(modifier = Modifier.weight(1f)) + if (user.memberId != currentUserMemberId) { + SearchResultUserFollowLayout( + user = user, + onFollowClick, + onUnFollowClick, + onFollowSuccess, + onUnFollowSuccess + ) + } + } + } +} + +@Composable +private fun SearchResultUserImageLayout(user: SearchUser, onClickProfile: (String) -> Unit) { + val imageInteractionSource = remember { MutableInteractionSource() } + GlideImage( + imageModel = { "${BuildConfig.BASE_URL}/images/${user.profileImg}" }, + imageOptions = ImageOptions( + contentDescription = "image description", + contentScale = ContentScale.Crop, + ), + modifier = Modifier + .padding(1.dp) + .height(36.dp) + .aspectRatio(1f) + .clip(CircleShape) + .clickableSingle( + interactionSource = imageInteractionSource, + indication = null, + onClick = { onClickProfile(user.memberId) } + ) + ) +} + +@Composable +private fun SearchResultUserNicknameLayout(userNickname: String) { + Text( + text = userNickname, + style = DayoTheme.typography.b6.copy(color = Gray1_50545B), + modifier = Modifier + .fillMaxHeight() + .padding(start = 12.dp, top = 8.dp, bottom = 8.dp) + ) +} + +@Composable +@Preview +private fun SearchResultUserFollowLayoutPreviewFalse() { + SearchResultUserFollowLayout( + user = SearchUser( + memberId = "1", + profileImg = "profileImg", + nickname = "nickname", + isFollow = false + ), + onFollowClick = { _, _ -> }, + onUnFollowClick = { _, _ -> }, + followSuccess = true, + unFollowSuccess = true + ) +} + +@Composable +@Preview +private fun SearchResultUserFollowLayoutPreviewTrue() { + SearchResultUserFollowLayout( + user = SearchUser( + memberId = "1", + profileImg = "profileImg", + nickname = "nickname", + isFollow = true + ), + onFollowClick = { _, _ -> }, + onUnFollowClick = { _, _ -> }, + followSuccess = true, + unFollowSuccess = true + ) +} + +@Composable +private fun SearchResultUserFollowLayout( + user: SearchUser, + onFollowClick: (String, Boolean) -> Unit, + onUnFollowClick: (String, Boolean) -> Unit, + followSuccess: Boolean, + unFollowSuccess: Boolean +) { + val followInteractionSource = remember { MutableInteractionSource() } + val followIsPressed by followInteractionSource.collectIsPressedAsState() + var followState by rememberSaveable { mutableStateOf(user.isFollow) } + + var isFollowButton by remember { + mutableStateOf((!followState and !followIsPressed) or (followState and followIsPressed)) + } + LaunchedEffect(followIsPressed, followState) { + isFollowButton = (!followState and !followIsPressed) or (followState and followIsPressed) + } + + NoRippleIconButton( + onClick = { + if (followState) { + onUnFollowClick(user.memberId, false) + if (unFollowSuccess) { + followState = false + } + } else { + onFollowClick(user.memberId, false) + if (followSuccess) { + followState = true + } + } + }, + iconContentDescription = "follow button", + iconPainter = if (isFollowButton) painterResource(R.drawable.ic_plus_sign_green) + else painterResource(R.drawable.ic_check_sign_gray), + iconButtonModifier = Modifier + .width(85.dp) + .height(36.dp) + .border( + width = 1.dp, + color = if (isFollowButton) PrimaryL3_F2FBF7 else Gray5_E8EAEE, + shape = RoundedCornerShape(32.dp) + ) + .background( + color = if (isFollowButton) PrimaryL3_F2FBF7 else White_FFFFFF, + shape = RoundedCornerShape(32.dp) + ), + iconModifier = Modifier + .padding(1.dp) + .width(20.dp) + .height(20.dp), + iconTintColor = if (isFollowButton) Primary_23C882 else Gray2_767B83, + interactionSource = followInteractionSource + ) { + Text( + text = if (isFollowButton) stringResource(id = R.string.follow_yet) + else stringResource(id = R.string.follow_already), + style = DayoTheme.typography.b6.copy( + color = if (isFollowButton) Primary_23C882 else Gray2_767B83, + textAlign = TextAlign.Center + ), + modifier = Modifier + .height(20.dp), + ) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchScreen.kt new file mode 100644 index 00000000..8be57468 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/search/SearchScreen.kt @@ -0,0 +1,446 @@ +package daily.dayo.presentation.screen.search + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.domain.model.SearchHistory +import daily.dayo.domain.model.SearchHistoryDetail +import daily.dayo.domain.model.SearchHistoryType +import daily.dayo.presentation.R +import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.toSp +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.viewmodel.SearchViewModel +import kotlinx.coroutines.launch + +@Composable +internal fun SearchRoute( + onBackClick: () -> Unit, + onSearch: (String) -> Unit, + viewmodel: SearchViewModel = hiltViewModel() +) { + BackHandler { + onBackClick() + } + + val searchHistory by viewmodel.searchHistory.collectAsStateWithLifecycle() + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + viewmodel.getSearchKeywordRecent() + } + + searchHistory?.let { + SearchScreen( + searchHistory = it, + onBackClick = onBackClick, + onSearchClick = { keyword -> + onSearch(keyword) + }, + onKeywordDeleteClick = { keyword, type -> + coroutineScope.launch { + viewmodel.deleteSearchKeywordRecent( + keyword, + type + ) + } + }, + onHistoryClearClick = { + coroutineScope.launch { + viewmodel.clearSearchKeywordRecent() + } + } + ) + } +} + +@Composable +fun SearchScreen( + searchHistory: SearchHistory, + onBackClick: () -> Unit, + onSearchClick: (String) -> Unit, + onKeywordDeleteClick: (String, SearchHistoryType) -> Unit, + onHistoryClearClick: () -> Unit +) { + Surface( + color = colorResource(id = R.color.white_FFFFFF), + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + Column { + SearchActionbarLayout( + onBackClick = onBackClick, + onSearchClick = onSearchClick + ) + SetSearchHistoryLayout( + onKeywordClick = onSearchClick, + onKeywordDeleteClick = onKeywordDeleteClick, + onHistoryClearClick = onHistoryClearClick, + searchHistory = searchHistory + ) + } + } +} + +@Composable +fun SearchActionbarLayout( + initialKeyword: String = "", + onBackClick: () -> Unit, + onSearchClick: (String) -> Unit +) { + Surface( + color = colorResource(id = R.color.white_FFFFFF), + modifier = Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth() + ) { + var textFieldValue by remember { mutableStateOf(TextFieldValue(initialKeyword)) } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxSize() + .padding(start = 6.dp, end = 14.dp, top = 2.dp, bottom = 2.dp), + ) { + NoRippleIconButton( + onClick = { onBackClick() }, + iconContentDescription = "Previous Page", + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .weight(1F) + .padding(vertical = 4.dp) + .background( + colorResource(id = R.color.gray_7_F6F6F7), + RoundedCornerShape(8.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + BasicTextField( + value = textFieldValue, + onValueChange = { textFieldValue = it }, + singleLine = true, + modifier = Modifier + .padding(start = 8.dp, top = 4.dp, bottom = 4.dp) + .wrapContentHeight(align = Alignment.CenterVertically) + .fillMaxWidth() + .defaultMinSize(minWidth = 1.dp, minHeight = 1.dp), + textStyle = TextStyle( + fontSize = 16.dp.toSp(), + fontFamily = FontFamily(Font(R.font.pretendard_medium)), + fontWeight = FontWeight(500), + color = colorResource(id = R.color.gray_1_313131), + ), + decorationBox = { innerTextField -> + if (textFieldValue.text.isEmpty()) { + Text( + text = stringResource(id = R.string.hint_search_keyword_input), + style = TextStyle( + fontSize = 14.dp.toSp(), + lineHeight = 20.dp.toSp(), + fontFamily = FontFamily(Font(R.font.pretendard_medium)), + fontWeight = FontWeight(500), + color = colorResource(id = R.color.gray_3_9FA5AE), + ) + ) + } + innerTextField() + }, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions( + onSearch = { + val trimmedBlankText = trimBlankText(textFieldValue.text) + if (trimmedBlankText.isNotEmpty()) { + onSearchClick(trimmedBlankText) + } + } + ) + ) + NoRippleIconButton( + onClick = { textFieldValue = TextFieldValue() }, + iconContentDescription = "Input Clear Button", + iconPainter = painterResource(id = R.drawable.ic_x_sign_circle_gray), + iconTintColor = Color.Unspecified, + iconButtonModifier = Modifier + .align(Alignment.CenterEnd) + .alpha(if (textFieldValue.text.isNotEmpty()) 1f else 0f), + ) + } + } + } +} + +@Composable +@Preview +private fun SearchHistoryGuideLayout() { + Surface( + color = colorResource(id = R.color.white_FFFFFF), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 18.dp, vertical = 8.dp) + ) { + Text( + text = "์ตœ๊ทผ ๊ฒ€์ƒ‰", + style = TextStyle( + fontSize = 14.dp.toSp(), + lineHeight = 20.dp.toSp(), + fontFamily = FontFamily(Font(R.font.pretendard_medium)), + fontWeight = FontWeight(500), + color = colorResource(id = R.color.gray_2_767B83), + ), + modifier = Modifier + .padding(start = 2.dp, top = 8.dp, bottom = 8.dp) + ) + } +} + +@Composable +private fun SetClearSearchHistoryLayout(onHistoryClearClick: () -> Unit) { + Surface( + color = colorResource(id = R.color.white_FFFFFF), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 8.dp) + ) { + Button( + modifier = Modifier + .wrapContentSize() + .background(DayoTheme.colorScheme.background) + .padding(horizontal = 12.dp, vertical = 4.dp), + colors = ButtonDefaults.buttonColors( + containerColor = colorResource(id = R.color.white_FFFFFF), + contentColor = colorResource(id = R.color.gray_2_767B83), + ), + shape = RoundedCornerShape(100.dp), + border = BorderStroke(1.dp, colorResource(id = R.color.gray_2_767B83)), + onClick = { onHistoryClearClick() } + ) { + Text( + text = stringResource(id = R.string.search_history_clear), + style = TextStyle( + fontSize = 12.dp.toSp(), + lineHeight = 18.dp.toSp(), + fontFamily = FontFamily(Font(R.font.pretendard_medium)), + fontWeight = FontWeight(500), + color = colorResource(id = R.color.gray_2_767B83), + ), + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 0.dp) + ) + } + } +} + +@Composable +private fun SetSearchHistoryLayout( + onKeywordClick: (String) -> Unit, + onKeywordDeleteClick: (String, SearchHistoryType) -> Unit, + onHistoryClearClick: () -> Unit, + searchHistory: SearchHistory +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(DayoTheme.colorScheme.background) + ) { + item { + SearchHistoryGuideLayout() + } + + if (searchHistory.data.isNotEmpty()) { + items(searchHistory.data) { + SearchHistoryLayout( + onKeywordClick = onKeywordClick, + onKeywordDeleteClick = onKeywordDeleteClick, + searchHistoryDetail = it + ) + } + item { + SetClearSearchHistoryLayout(onHistoryClearClick = onHistoryClearClick) + } + } else { + item { + Box( + modifier = Modifier + .fillParentMaxHeight() + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + SearchHistoryEmpty() + } + } + } + } +} + +@Composable +@Preview +fun SearchHistoryEmpty() { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_empty), + contentDescription = "search history empty" + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(R.string.search_history_empty_title), + style = DayoTheme.typography.b3, + color = Gray3_9FA5AE, + ) + Text( + modifier = Modifier.padding(vertical = 2.dp), + text = stringResource(id = R.string.search_history_empty_description), + style = DayoTheme.typography.caption2, + color = Gray4_C5CAD2, + ) + } +} + +@Composable +private fun SearchHistoryLayout( + onKeywordClick: (String) -> Unit, + onKeywordDeleteClick: (String, SearchHistoryType) -> Unit, + searchHistoryDetail: SearchHistoryDetail +) { + Surface( + color = colorResource(id = R.color.white_FFFFFF), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .clickableSingle { + onKeywordClick(searchHistoryDetail.history) + } + .padding(horizontal = 18.dp, vertical = 4.dp) + ) { + SearchHistoryDetailLayout(searchedText = searchHistoryDetail.history) + Spacer( + modifier = Modifier + .padding(end = 20.dp) + .weight(1f) + ) + SearchHistoryDeleteButton( + onKeywordDeleteClick = onKeywordDeleteClick, + searchHistoryDetail = searchHistoryDetail + ) + } + } +} + +@Composable +@Preview +private fun PreviewSearchHistoryLayout() { + SearchHistoryLayout( + onKeywordClick = { _ -> }, + onKeywordDeleteClick = { _, _ -> }, + searchHistoryDetail = SearchHistoryDetail( + history = "๊ฒ€์ƒ‰์–ด", + searchId = 0, + searchHistoryType = SearchHistoryType.USER + ) + ) +} + +@Composable +private fun SearchHistoryDetailLayout(searchedText: String) { + Text( + text = searchedText, + style = TextStyle( + fontSize = 16.dp.toSp(), + fontFamily = FontFamily(Font(R.font.pretendard_medium)), + fontWeight = FontWeight(500), + color = colorResource(id = R.color.gray_1_313131), + ), + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp) + ) +} + +@Composable +private fun SearchHistoryDeleteButton( + onKeywordDeleteClick: (String, SearchHistoryType) -> Unit, + searchHistoryDetail: SearchHistoryDetail +) { + NoRippleIconButton( + onClick = { + with(searchHistoryDetail) { + onKeywordDeleteClick(history, searchHistoryType) + } + }, + iconContentDescription = "History Delete Button", + iconPainter = painterResource(id = R.drawable.ic_x_sign) + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/AppInformationScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/AppInformationScreen.kt new file mode 100644 index 00000000..cb4b2e68 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/AppInformationScreen.kt @@ -0,0 +1,173 @@ +package daily.dayo.presentation.screen.settings + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.R +import daily.dayo.presentation.screen.rules.RuleType +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign + +@Composable +fun AppInformationScreen( + onBackClick: () -> Unit = {}, + onRulesClick: (RuleType) -> Unit = {}, +) { + Scaffold( + topBar = { AppInformationTopBar(onBackClick = onBackClick) }, + ) { paddingValues -> + AppInformationContent( + modifier = Modifier.padding(paddingValues), + onRulesClick = onRulesClick, + ) + } +} + +@Composable +fun AppInformationTopBar( + onBackClick: () -> Unit, +) { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = { onBackClick() }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + title = stringResource(R.string.information_title), + titleAlignment = TopNavigationAlign.CENTER + ) +} + +@Composable +fun AppInformationContent( + modifier: Modifier = Modifier, + onRulesClick: (RuleType) -> Unit = {}, +) { + val context = LocalContext.current + val appVersion = getAppVersion(context) + + Column( + modifier = modifier + .fillMaxSize() + .padding(vertical = 8.dp, horizontal = 20.dp), + ) { + AppInformationItem( + title = stringResource(R.string.rules_terms_and_conditions), + onClick = { onRulesClick(RuleType.TERMS_AND_CONDITIONS) }, + ) + Spacer(modifier = Modifier.height(2.dp)) + AppInformationItem( + title = stringResource(R.string.rules_privacy_policy_title), + onClick = { onRulesClick(RuleType.PRIVACY_POLICY) }, + ) + Spacer(modifier = Modifier.height(2.dp)) + AppInformationItem( + title = stringResource(R.string.app_version), + description = "DAYO $appVersion", + ) + } +} + +@Composable +fun AppInformationItem( + title: String, + description: String? = null, + onClick: () -> Unit = {}, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .clickable { onClick() }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = DayoTheme.typography.b4, + color = Dark, + ) + Spacer(modifier = Modifier.weight(1f)) + if (!description.isNullOrBlank()) { + Text( + text = description, + style = DayoTheme.typography.b4, + color = DayoTheme.colorScheme.primary, + modifier = Modifier + .padding(end = 4.dp) + .wrapContentSize(), + ) + } else { + Icon( + modifier = Modifier + .padding(end = 4.dp) + .size(20.dp), + painter = painterResource(id = R.drawable.ic_chevron_r), + contentDescription = null, + tint = DayoTheme.colorScheme.outline, + ) + } + } +} + +private fun getAppVersion(context: Context): String { + return try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.getPackageInfo( + context.packageName, + PackageManager.PackageInfoFlags.of(0) + ).versionName + } else { + context.packageManager.getPackageInfo(context.packageName, 0).versionName + } ?: "" + } catch (e: PackageManager.NameNotFoundException) { + "" + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewAppInformationContent() { + DayoTheme { + AppInformationContent() + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewSettingsListItem() { + DayoTheme { + Column { + AppInformationItem(title = "์ด์šฉ์•ฝ๊ด€") + AppInformationItem(title = "๊ฐœ์ธ์ •๋ณด์ฒ˜๋ฆฌ๋ฐฉ์นจ") + AppInformationItem(title = "์•ฑ ๋ฒ„์ „", description = "DAYO 1.0.0") + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt new file mode 100644 index 00000000..b4861f2e --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/BlockedUsersScreen.kt @@ -0,0 +1,265 @@ +package daily.dayo.presentation.screen.settings + +import android.content.Context +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.size.Size +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.view.DayoOutlinedButton +import daily.dayo.presentation.view.FilledRoundedCornerButton +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.viewmodel.ProfileSettingViewModel +import daily.dayo.presentation.viewmodel.ProfileViewModel +import kotlinx.coroutines.launch + +@Composable +fun BlockedUsersScreen( + onBackClick: () -> Unit, + profileViewModel: ProfileViewModel = hiltViewModel(), + profileSettingViewModel: ProfileSettingViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val snackBarHostState = remember { SnackbarHostState() } + val blockedUsers by profileSettingViewModel.blockList.collectAsStateWithLifecycle() + val unblockSuccess by profileViewModel.unblockSuccess.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + profileSettingViewModel.requestBlockList() + } + + LaunchedEffect(unblockSuccess) { + unblockSuccess?.let { state -> + when (state) { + Status.SUCCESS -> { + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.other_profile_unblock_success_message)) + } + profileSettingViewModel.requestBlockList() + } + + Status.ERROR -> { + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.other_profile_unblock_fail_message)) + } + } + + Status.LOADING -> { + + } + } + } + } + + Scaffold( + topBar = { BlockedUsersActionbarLayout(onBackClick = onBackClick) }, + snackbarHost = { SnackbarHost(snackBarHostState) }, + content = { innerPadding -> + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .padding(innerPadding) + .fillMaxSize() + .padding(top = 12.dp, start = 20.dp, end = 20.dp) + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(vertical = 16.dp) + ) { + if (blockedUsers.status != Status.ERROR) { + blockedUsers.data.orEmpty().let { blockedUsers -> + if (blockedUsers.isEmpty()) { + item { + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 164.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_blocked_users_empty), + contentDescription = null, + modifier = Modifier + .width(136.dp) + .wrapContentHeight() + .padding(6.5.dp) + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(R.string.blocked_users_empty_description), + color = Gray3_9FA5AE, + style = DayoTheme.typography.b3, + modifier = Modifier + .wrapContentSize() + ) + } + } + } else { + itemsIndexed( + blockedUsers, + key = { _, user -> user.memberId } + ) { _, user -> + // Nickname์ด null์ธ ๊ฒฝ์šฐ๋Š” ์—†์„ ๊ฒƒ ๊ฐ™์ง€๋งŒ, null์ผ ๊ฒฝ์šฐ ๋ณด์ด์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌ + user.nickname?.let { nickname -> + BlockedUser( + userId = user.memberId, + imageFileName = user.profileImg, + nickName = nickname, + onUnblockClick = { userId -> + coroutineScope.launch { + profileViewModel.requestUnblockMember(userId) + } + }, + context = context, + ) + } + } + } + } + } else { + item { + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 164.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_blocked_users_empty), + contentDescription = null, + modifier = Modifier + .width(136.dp) + .wrapContentHeight() + .padding(6.5.dp) + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(R.string.blocked_users_error_description), + color = Gray3_9FA5AE, + style = DayoTheme.typography.b3, + modifier = Modifier + .wrapContentSize() + ) + Spacer(modifier = Modifier.height(20.dp)) + FilledRoundedCornerButton( + modifier = Modifier + .padding(horizontal = 20.dp) + .wrapContentSize(), + onClick = { profileSettingViewModel.requestBlockList() }, + label = stringResource(R.string.re_try) + ) + } + } + } + } + } + } + ) +} + +@Preview +@Composable +fun BlockedUser( + userId: String = "", + imageFileName: String = "", + nickName: String = "", + onUnblockClick: (String) -> Unit = {}, + context: Context = LocalContext.current, +) { + Row( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxWidth() + .height(38.dp) + .padding(bottom = 2.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + RoundImageView( + imageUrl = "${BuildConfig.BASE_URL}/images/${imageFileName}", + context = context, + modifier = Modifier + .size(36.dp) + .aspectRatio(1f), + imageDescription = "User Profile Image", + imageSize = Size(36, 36), + roundSize = 18.dp, + placeholderResId = R.drawable.ic_profile_default_user_profile, + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = nickName, + style = DayoTheme.typography.b6.copy(color = Dark), + maxLines = 1, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Spacer(modifier = Modifier.weight(1f)) + DayoOutlinedButton( + label = stringResource(R.string.blocked_users_unblock), + onClick = { onUnblockClick(userId) }, + modifier = Modifier.height(36.dp), + ) + } +} + + +@Preview +@Composable +fun BlockedUsersActionbarLayout( + onBackClick: () -> Unit = {}, +) { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = { onBackClick() }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + title = stringResource(R.string.blocked_users_title), + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/ChangePasswordScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/ChangePasswordScreen.kt new file mode 100644 index 00000000..ede8c5ad --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/ChangePasswordScreen.kt @@ -0,0 +1,344 @@ +package daily.dayo.presentation.screen.settings + +import android.content.Context +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import daily.dayo.presentation.R +import daily.dayo.presentation.common.ReplaceUnicode.trimBlankText +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.screen.account.SetPasswordView +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.FilledRoundedCornerButton +import daily.dayo.presentation.view.Loading +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.viewmodel.AccountViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +const val PASSWORD_PERMIT_FORMAT = "^[a-z0-9]{8,16}$" + +@Composable +@Preview +fun ChangePasswordScreen( + coroutineScope: CoroutineScope = rememberCoroutineScope(), + snackBarHostState: SnackbarHostState = remember { SnackbarHostState() }, + accountViewModel: AccountViewModel = hiltViewModel(), + onBackClick: () -> Unit = {}, +) { + val context = LocalContext.current + val keyboardController = LocalSoftwareKeyboardController.current + var changePasswordStep by remember { mutableStateOf(ChangePasswordStep.CUR_PASSWORD_INPUT) } + val setChangePasswordStep: (ChangePasswordStep) -> Unit = { changePasswordStep = it } + val changePasswordStatus by accountViewModel.changePasswordSuccess.collectAsState() + val checkCurrentPasswordStatus by accountViewModel.checkCurrentPasswordSuccess.collectAsState() + + val isNextButtonEnabled = remember { mutableStateOf(false) } + val isNextButtonClickable = remember { mutableStateOf(false) } + + // Current Password + val currentPasswordState = remember { mutableStateOf("") } + val isCurrentPasswordPassFormatError = remember { mutableStateOf(false) } + + // New Password + val newPasswordState = remember { mutableStateOf("") } + val isNewPasswordPassFormatError = remember { mutableStateOf(false) } + val newPasswordConfirmState = remember { mutableStateOf("") } + val isNewPasswordMatchError = remember { mutableStateOf(false) } + + + LaunchedEffect(changePasswordStatus) { + if (changePasswordStep == ChangePasswordStep.NEW_PASSWORD_CONFIRM && + changePasswordStatus == Status.SUCCESS + ) { + onBackClick() + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.change_password_success_message)) + } + } + } + + LaunchedEffect(checkCurrentPasswordStatus) { + if (checkCurrentPasswordStatus == true) { + setChangePasswordStep(ChangePasswordStep.NEW_PASSWORD_INPUT) + } else if (checkCurrentPasswordStatus == false) { + isCurrentPasswordPassFormatError.value = true + } + } + + ChangePasswordScaffold( + context = context, + onBackClick = { + when (changePasswordStep) { + ChangePasswordStep.CUR_PASSWORD_INPUT -> { + onBackClick() + } + + ChangePasswordStep.NEW_PASSWORD_INPUT -> { + currentPasswordState.value = "" + isCurrentPasswordPassFormatError.value = false + + newPasswordState.value = "" + isNewPasswordPassFormatError.value = false + setChangePasswordStep(ChangePasswordStep.CUR_PASSWORD_INPUT) + } + + ChangePasswordStep.NEW_PASSWORD_CONFIRM -> { + newPasswordConfirmState.value = "" + isNewPasswordMatchError.value = false + setChangePasswordStep(ChangePasswordStep.NEW_PASSWORD_INPUT) + } + } + }, + changePasswordStep = changePasswordStep, + title = when (changePasswordStep) { + ChangePasswordStep.CUR_PASSWORD_INPUT -> stringResource( + R.string.setting_change_password_current_title + ) + + ChangePasswordStep.NEW_PASSWORD_INPUT, ChangePasswordStep.NEW_PASSWORD_CONFIRM -> stringResource( + R.string.setting_change_password_new_title + ) + }, + isNextButtonEnabled = isNextButtonEnabled.value, + isNextButtonClickable = isNextButtonClickable.value, + onNextClick = { + keyboardController?.hide() + isNextButtonClickable.value = false + + when (changePasswordStep) { + ChangePasswordStep.CUR_PASSWORD_INPUT -> { + accountViewModel.requestCheckCurrentPassword(currentPasswordState.value) + isCurrentPasswordPassFormatError.value = false + } + + ChangePasswordStep.NEW_PASSWORD_INPUT -> { + val passwordFormat = Regex(PASSWORD_PERMIT_FORMAT) + if (passwordFormat.matches(newPasswordState.value)) { + setChangePasswordStep(ChangePasswordStep.NEW_PASSWORD_CONFIRM) + } else { + isNewPasswordPassFormatError.value = true + } + } + + ChangePasswordStep.NEW_PASSWORD_CONFIRM -> { + if (newPasswordState.value == newPasswordConfirmState.value) { + accountViewModel.requestChangePassword( + newPassword = trimBlankText(newPasswordState.value) + ) + } else { + isNewPasswordMatchError.value = true + } + } + } + } + ) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(28.dp) + ) + when (changePasswordStep) { + ChangePasswordStep.CUR_PASSWORD_INPUT -> { + SetPasswordView( + passwordInputViewCondition = changePasswordStep == ChangePasswordStep.CUR_PASSWORD_INPUT, + isNextButtonEnabled = isNextButtonEnabled.value, + setNextButtonEnabled = { isNextButtonEnabled.value = it }, + isNextButtonClickable = isNextButtonClickable.value, + setIsNextButtonClickable = { isNextButtonClickable.value = it }, + password = currentPasswordState.value, + setPassword = { currentPasswordState.value = it }, + isPasswordFormatValid = !isCurrentPasswordPassFormatError.value, + setIsPasswordFormatValid = { isCurrentPasswordPassFormatError.value = !it }, + passwordFailMessage = stringResource(R.string.setting_change_password_current_fail_message), + ) + } + + ChangePasswordStep.NEW_PASSWORD_INPUT, ChangePasswordStep.NEW_PASSWORD_CONFIRM -> { + SetPasswordView( + passwordInputViewCondition = changePasswordStep == ChangePasswordStep.NEW_PASSWORD_INPUT, + passwordConfirmationViewCondition = changePasswordStep == ChangePasswordStep.NEW_PASSWORD_CONFIRM, + isNextButtonEnabled = isNextButtonEnabled.value, + setNextButtonEnabled = { isNextButtonEnabled.value = it }, + isNextButtonClickable = isNextButtonClickable.value, + setIsNextButtonClickable = { isNextButtonClickable.value = it }, + password = newPasswordState.value, + setPassword = { newPasswordState.value = it }, + isPasswordFormatValid = !isNewPasswordPassFormatError.value, + setIsPasswordFormatValid = { isNewPasswordPassFormatError.value = !it }, + passwordConfirmation = newPasswordConfirmState.value, + setPasswordConfirmation = { newPasswordConfirmState.value = it }, + isPasswordMismatch = isNewPasswordMatchError.value, + setIsPasswordMismatch = { isNewPasswordMatchError.value = it }, + ) + + } + } + } + + Loading( + isVisible = changePasswordStatus == Status.LOADING || changePasswordStatus == Status.SUCCESS, + ) +} + +@Preview +@Composable +fun ChangePasswordScaffold( + context: Context = LocalContext.current, + onBackClick: () -> Unit = {}, + changePasswordStep: ChangePasswordStep = ChangePasswordStep.CUR_PASSWORD_INPUT, + title: String = "", + isNextButtonEnabled: Boolean = false, + isNextButtonClickable: Boolean = false, + onNextClick: () -> Unit = {}, + content: @Composable (ColumnScope.() -> Unit) = {}, +) { + BackHandler { onBackClick() } + Scaffold( + topBar = { + ChangePasswordActionbarLayout( + onBackClick = onBackClick, + changePasswordStep = changePasswordStep, + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + ) { + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .padding(horizontal = 18.dp, vertical = 0.dp) + .fillMaxWidth() + .wrapContentSize() + ) { + ChangePasswordTitleLayout(title = title) + content() + } + + Spacer(modifier = Modifier.weight(1f)) + ChangePasswordBottomLayout( + onNextClick = { onNextClick() }, + isChangePasswordButtonEnabled = isNextButtonEnabled, + isChangePasswordButtonClickable = isNextButtonClickable, + ) + } + } +} + + +@Preview +@Composable +fun ChangePasswordActionbarLayout( + onBackClick: () -> Unit = {}, + changePasswordStep: ChangePasswordStep = ChangePasswordStep.CUR_PASSWORD_INPUT, +) { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = { onBackClick() }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + ) +} + +@Preview +@Composable +fun ChangePasswordTitleLayout( + title: String = "", +) { + Spacer(modifier = Modifier.height(15.dp)) + Text( + text = title, + style = DayoTheme.typography.h2.copy(color = Dark), + ) +} + +@Preview +@Composable +fun ChangePasswordBottomLayout( + onNextClick: () -> Unit = {}, + isChangePasswordButtonEnabled: Boolean = false, + isChangePasswordButtonClickable: Boolean = false, +) { + Column( + modifier = Modifier + .imePadding() + .fillMaxWidth() + .wrapContentHeight() + ) { + ChangePasswordNextButton( + onButtonClick = { onNextClick() }, + isChangePasswordButtonEnabled = isChangePasswordButtonEnabled, + isChangePasswordButtonClickable = isChangePasswordButtonClickable + ) + } +} + +@Composable +@Preview +fun ChangePasswordNextButton( + onButtonClick: () -> Unit = {}, + isChangePasswordButtonEnabled: Boolean = false, + isChangePasswordButtonClickable: Boolean = false, +) { + Column( + modifier = Modifier + .padding(horizontal = 18.dp, vertical = 20.dp) + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + FilledRoundedCornerButton( + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + label = stringResource(R.string.next), + textStyle = DayoTheme.typography.b3.copy(color = White_FFFFFF), + onClick = { if (isChangePasswordButtonClickable) onButtonClick() }, + enabled = isChangePasswordButtonEnabled, + ) + } +} + +enum class ChangePasswordStep(val stepNum: Int) { + CUR_PASSWORD_INPUT(1), // ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ + NEW_PASSWORD_INPUT(2), // ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ + NEW_PASSWORD_CONFIRM(3), // ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์ž…๋ ฅ +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingNotificationScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingNotificationScreen.kt new file mode 100644 index 00000000..db329631 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingNotificationScreen.kt @@ -0,0 +1,393 @@ +package daily.dayo.presentation.screen.settings + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.ContentValues.TAG +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.app.NotificationManagerCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.firebase.ktx.Firebase +import com.google.firebase.messaging.ktx.messaging +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.view.dialog.ConfirmDialog +import daily.dayo.presentation.viewmodel.AccountViewModel +import daily.dayo.presentation.viewmodel.SettingNotificationViewModel + +enum class NotificationSettingType { + DEVICE, + NOTICE, + REACTION, +} + +data class NotificationSettingField( + val type: NotificationSettingType = NotificationSettingType.DEVICE, + val title: String, + val description: String = "", + val isChecked: Boolean = false, +) + +@Composable +@Preview +fun SettingNotificationScreen( + onBackClick: () -> Unit = {}, + accountViewModel: AccountViewModel = hiltViewModel(), + settingNotificationViewModel: SettingNotificationViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val reactionNotificationChannel = notificationManager.getNotificationChannel("REACTION") + + var isNotificationEnabledOnDevice by remember { + mutableStateOf(NotificationManagerCompat.from(context).areNotificationsEnabled()) + } + val isNoticeNotificationEnabledOnDevice by accountViewModel.isNoticeNotificationEnabled.collectAsStateWithLifecycle() + var isReactionNotificationEnabledOnDevice by remember { + // ์ฑ„๋„์ด ์—†๊ฑฐ๋‚˜, ์ค‘์š”๋„๊ฐ€ NONE์ด๋ฉด ๊บผ์ง„ ๊ฒƒ + mutableStateOf(reactionNotificationChannel != null && reactionNotificationChannel.importance != NotificationManager.IMPORTANCE_NONE) + } + val isReactionNotificationEnabledOnServer by settingNotificationViewModel.isReactionNotificationEnabled.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + settingNotificationViewModel.requestReceiveAlarm() + } + + LaunchedEffect(isNoticeNotificationEnabledOnDevice) { + with(settingNotificationViewModel) { + if (isNoticeNotificationEnabledOnDevice) registerDeviceToken() else unregisterFcmToken() + } + } + + DisposableEffect(lifecycleOwner) { + // ์‹œ์Šคํ…œ์—์„œ ์•Œ๋ฆผ ์„ค์ • ๋ณ€๊ฒฝ ํ›„ ๋Œ์•„์™”์„ ๋•Œ, ๋ฐ˜์˜๋˜๋„๋ก RESUME์—์„œ ์ฒดํฌ + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + isNotificationEnabledOnDevice = + NotificationManagerCompat.from(context).areNotificationsEnabled() + val updatedChannel = + (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) + .getNotificationChannel("REACTION") + + isReactionNotificationEnabledOnDevice = + updatedChannel != null && updatedChannel.importance != NotificationManager.IMPORTANCE_NONE + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { SettingNotificationTopNavigation(onBackClick = onBackClick) } + ) { innerPadding -> + SettingNotificationContent( + innerPadding = innerPadding, + isNotificationEnabledOnDevice = isNotificationEnabledOnDevice, + isNoticeNotificationOnDevice = isNoticeNotificationEnabledOnDevice, + isReactionNotificationEnabledOnDevice = isReactionNotificationEnabledOnDevice, + isReactionNotificationEnabledOnServer = isReactionNotificationEnabledOnServer, + changeNoticeNotificationSetting = { isChecked -> + accountViewModel.changeNoticeNotificationSetting(isChecked) + + // Subscribe/Unsubscribe Topic (Notice Notification) + val topic = context.getString(R.string.notification_topic_notice) + Firebase.messaging.run { + (if (isChecked) subscribeToTopic(topic) else unsubscribeFromTopic(topic)) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + Log.d(TAG, if (isChecked) "NOTICE ์ˆ˜์‹ " else "NOTICE ์ˆ˜์‹  ๊ฑฐ๋ถ€") + } + } + } + }, + changeReactionNotificationSetting = { + settingNotificationViewModel.requestReceiveChangeReceiveAlarm(it) + }, + reactionNotificationChannel = reactionNotificationChannel + ) + } +} + +@Composable +@Preview +fun MoveToDeviceSettingDialog( + onConfirmClick: () -> Unit = {}, + onDismissClick: () -> Unit = {} +) { + ConfirmDialog( + title = stringResource(R.string.move_to_setting_notification_dialog_title), + description = "", + onClickConfirmText = stringResource(R.string.move), + onClickConfirm = onConfirmClick, + onClickCancelText = stringResource(R.string.cancel), + onClickCancel = onDismissClick, + ) +} + +@Composable +@Preview +fun SettingNotificationTopNavigation( + onBackClick: () -> Unit = {} +) { + TopNavigation( + title = stringResource(R.string.setting_notification_title), + leftIcon = { + NoRippleIconButton( + onClick = { + onBackClick() + }, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + titleAlignment = TopNavigationAlign.CENTER, + ) +} + +@Composable +@Preview +fun SettingNotificationContent( + context: Context = LocalContext.current, + innerPadding: PaddingValues = PaddingValues(0.dp), + isNotificationEnabledOnDevice: Boolean = false, + isNoticeNotificationOnDevice: Boolean = false, + isReactionNotificationEnabledOnDevice: Boolean? = false, + isReactionNotificationEnabledOnServer: Boolean? = false, + changeNoticeNotificationSetting: (Boolean) -> Unit = {}, + changeReactionNotificationSetting: (Boolean) -> Unit = {}, + reactionNotificationChannel: NotificationChannel? = null, +) { + val showMoveToDeviceSettingDialog = remember { mutableStateOf(false) } + val moveDestination = remember { mutableStateOf(SettingMoveDestination.App) } + + val notificationSettingsList = listOf( + NotificationSettingField( + type = NotificationSettingType.DEVICE, + title = stringResource(R.string.setting_notification_device), + description = stringResource(R.string.setting_notification_device_explanation), + isChecked = isNotificationEnabledOnDevice, + ), + NotificationSettingField( + type = NotificationSettingType.NOTICE, + title = stringResource(R.string.setting_notification_notice), + isChecked = isNoticeNotificationOnDevice, + ), + NotificationSettingField( + type = NotificationSettingType.REACTION, + title = stringResource(R.string.setting_notification_reaction), + isChecked = ( + (isReactionNotificationEnabledOnDevice ?: false || reactionNotificationChannel == null) // channel์ด null์ด๋ฉด ์ฑ„๋„ ์ƒ์„ฑ์ด ์•„์ง ์•ˆ๋œ๊ฒƒ -> ์•Œ๋ฆผ์„ ๋ฐ›์•„์•ผ ์ฑ„๋„์ด ์ƒ์„ฑ๋˜๋ฏ€๋กœ true ์ฒ˜๋ฆฌ + && isReactionNotificationEnabledOnServer ?: false) + ) + ) + + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + ) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(12.dp) + ) + + NotificationSettingsList( + settings = notificationSettingsList, + onSwitchChange = { settingField, isChecked -> + when (settingField.type) { + NotificationSettingType.DEVICE -> { + moveDestination.value = SettingMoveDestination.App + showMoveToDeviceSettingDialog.value = true + } + + NotificationSettingType.NOTICE -> { + changeNoticeNotificationSetting(isChecked) + } + + NotificationSettingType.REACTION -> { + val updatedChannel = + (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) + .getNotificationChannel("REACTION") + + if (updatedChannel != null && + updatedChannel.importance == NotificationManager.IMPORTANCE_NONE + ) { + // ์ฑ„๋„์ด ์กด์žฌํ•˜๋Š”๋ฐ, ๊บผ์ ธ์žˆ์œผ๋ฉด ์ง์ ‘ ๋‹ค์‹œ ์ผœ์•ผ ํ•จ + moveDestination.value = SettingMoveDestination.Reaction + showMoveToDeviceSettingDialog.value = true + } else { + changeReactionNotificationSetting(isChecked) + } + } + } + }, + isNotificationChangeEnabled = isNotificationEnabledOnDevice + ) + + if (showMoveToDeviceSettingDialog.value) { + MoveToDeviceSettingDialog( + onConfirmClick = { + val intent = when (moveDestination.value) { + SettingMoveDestination.App -> { + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + } + + SettingMoveDestination.Reaction -> { + Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + putExtra( + Settings.EXTRA_CHANNEL_ID, + context.getString(R.string.notification_channel_id) + ) + } + } + } + context.startActivity(intent) + showMoveToDeviceSettingDialog.value = false + }, + onDismissClick = { showMoveToDeviceSettingDialog.value = false } + ) + } + } +} + +@Composable +fun NotificationSettingsList( + settings: List = emptyList(), + onSwitchChange: (NotificationSettingField, Boolean) -> Unit = { _, _ -> }, + isNotificationChangeEnabled: Boolean = true, // Device ์•Œ๋ฆผ์ด ๊บผ์ ธ์žˆ์œผ๋ฉด ์„ค์ • ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€์ฒ˜๋ฆฌ +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(settings) { setting -> + SettingNotificationItem( + title = setting.title, + description = setting.description, + isChecked = setting.isChecked, + onSwitchChange = { onSwitchChange(setting, it) }, + isNotificationChangeEnabled = + (setting.type == NotificationSettingType.DEVICE || isNotificationChangeEnabled) + ) + } + } +} + + +@Composable +@Preview +fun SettingNotificationItem( + title: String = "", + description: String = "", + onSwitchChange: (Boolean) -> Unit = {}, + isChecked: Boolean = false, + isNotificationChangeEnabled: Boolean = true, +) { + Row( + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 0.dp) + .fillMaxWidth() + .height(48.dp) + ) { + Column( + modifier = Modifier + .align(Alignment.CenterVertically) + .wrapContentSize() + ) { + Text( + text = title, + textAlign = TextAlign.Start, + style = DayoTheme.typography.b4.copy(color = Dark), + modifier = Modifier.wrapContentSize(), + ) + if (description.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = description, + textAlign = TextAlign.Start, + style = DayoTheme.typography.b6.copy(color = Gray2_767B83), + modifier = Modifier.wrapContentSize(), + ) + } + } + Spacer( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + ) + Switch( + checked = isChecked, + onCheckedChange = { onSwitchChange(it) }, + enabled = isNotificationChangeEnabled, + colors = SwitchDefaults.colors( + uncheckedThumbColor = White_FFFFFF, + checkedThumbColor = White_FFFFFF, + checkedTrackColor = Primary_23C882, + uncheckedTrackColor = Gray6_F0F1F3, + uncheckedBorderColor = Gray6_F0F1F3, + checkedBorderColor = Primary_23C882, + ) + ) + } +} + +enum class SettingMoveDestination { + App, + Reaction, +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsNavigation.kt new file mode 100644 index 00000000..4265e3ec --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsNavigation.kt @@ -0,0 +1,120 @@ +package daily.dayo.presentation.screen.settings + +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import daily.dayo.presentation.screen.account.WithdrawScreen +import daily.dayo.presentation.screen.rules.RuleRoute +import daily.dayo.presentation.screen.rules.RuleType +import kotlinx.coroutines.CoroutineScope + +fun NavController.navigateSettings() { + navigate(SettingsRoute.route) +} + +fun NavController.navigateWithdraw() { + navigate(SettingsRoute.withdraw) +} + +fun NavController.navigateChangePassword() { + navigate(SettingsRoute.changePassword) +} + +fun NavController.navigateSettingsNotification() { + navigate(SettingsRoute.notification) +} + +fun NavController.navigateInformation() { + navigate(SettingsRoute.information) +} + +@OptIn(ExperimentalMaterial3Api::class) +fun NavGraphBuilder.settingsNavGraph( + coroutineScope: CoroutineScope, + snackBarHostState: SnackbarHostState, + onProfileEditClick: () -> Unit, + onBlockUsersClick: () -> Unit, + onWithdrawClick: () -> Unit, + onBackClick: () -> Unit, + bottomSheetState: BottomSheetScaffoldState, + bottomSheetContent: (@Composable () -> Unit) -> Unit, + onSettingNotificationClick: () -> Unit, + onPasswordChangeClick: () -> Unit, + onNoticesClick: () -> Unit, + onInformationClick: () -> Unit, + onRulesClick: (RuleType) -> Unit, + onNavigateToHome: () -> Unit = {}, + onNavigateToMyPage: () -> Unit = {}, +) { + composable(SettingsRoute.route) { + SettingsScreen( + onProfileEditClick = onProfileEditClick, + onWithdrawClick = onWithdrawClick, + onBackClick = onBackClick, + onPasswordChangeClick = onPasswordChangeClick, + onBlockUsersClick = onBlockUsersClick, + onSettingNotificationClick = onSettingNotificationClick, + onNoticesClick = onNoticesClick, + onInformationClick = onInformationClick + ) + } + + composable(route = SettingsRoute.notification) { + SettingNotificationScreen( + onBackClick = onBackClick, + ) + } + + composable(SettingsRoute.changePassword) { + ChangePasswordScreen( + coroutineScope = coroutineScope, + snackBarHostState = snackBarHostState, + onBackClick = onBackClick, + ) + } + + composable(SettingsRoute.information) { + AppInformationScreen( + onBackClick = onBackClick, + onRulesClick = onRulesClick, + ) + } + + composable(route = RuleRoute.privacyPolicy) { + RuleRoute( + onBackClick = onBackClick, + ruleType = RuleType.PRIVACY_POLICY + ) + } + + composable(route = RuleRoute.termsAndConditions) { + RuleRoute( + onBackClick = onBackClick, + ruleType = RuleType.TERMS_AND_CONDITIONS + ) + } + + composable(SettingsRoute.withdraw) { + WithdrawScreen( + onBackClick = onBackClick, + bottomSheetState = bottomSheetState, + bottomSheetContent = bottomSheetContent, + snackBarHostState = snackBarHostState, + onNavigateToHome = onNavigateToHome, + onNavigateToMyPage = onNavigateToMyPage + ) + } +} + +object SettingsRoute { + const val route = "settings" + + const val withdraw = "${route}/withdraw" + const val changePassword = "${route}/changePassword" + const val notification = "${route}/notification" + const val information = "${route}/information" +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt new file mode 100644 index 00000000..37e9a64b --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/settings/SettingsScreen.kt @@ -0,0 +1,401 @@ +package daily.dayo.presentation.screen.settings + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color.Companion.White +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.domain.model.Profile +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.activity.LoginActivity +import daily.dayo.presentation.activity.MainActivity +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.Loading +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.view.dialog.ConfirmDialog +import daily.dayo.presentation.viewmodel.AccountViewModel +import daily.dayo.presentation.viewmodel.ProfileViewModel + +@Composable +fun SettingsScreen( + onProfileEditClick: () -> Unit, + onWithdrawClick: () -> Unit, + onBackClick: () -> Unit, + onPasswordChangeClick: () -> Unit, + onBlockUsersClick: () -> Unit, + onSettingNotificationClick: () -> Unit, + onNoticesClick: () -> Unit, + onInformationClick: () -> Unit = {}, + profileViewModel: ProfileViewModel = hiltViewModel(), + accountViewModel: AccountViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val signOutSuccess by accountViewModel.signOutSuccess.collectAsStateWithLifecycle() + val profileInfo = profileViewModel.profileInfo.observeAsState() + val showSignOutDialog = remember { mutableStateOf(false) } + var showInquiryGuideDialog by remember { mutableStateOf(false) } + val inquiryEmail = stringResource(id = R.string.inquiry_email) + + LaunchedEffect(Unit) { + profileViewModel.requestMyProfile() + } + + LaunchedEffect(signOutSuccess) { + if (signOutSuccess == Status.SUCCESS) { + accountViewModel.clearCurrentUser() + val intent = Intent(context, LoginActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP + } + (context as? MainActivity)?.let { + it.startActivity(intent) + it.finish() + } + } + } + + SettingsScreen( + profile = profileInfo.value?.data, + onProfileEditClick = onProfileEditClick, + onBackClick = onBackClick, + onSettingNotificationClick = onSettingNotificationClick, + onPasswordChangeClick = onPasswordChangeClick, + onNoticesClick = onNoticesClick, + onBlockUsersClick = onBlockUsersClick, + onInquiryClick = { + copyInquiryEmail(context, inquiryEmail) + showInquiryGuideDialog = true + }, + onInformationClick = onInformationClick, + onWithdrawClick = onWithdrawClick, + onSignOutClick = { showSignOutDialog.value = true }, + ) + + if (showInquiryGuideDialog) { + ConfirmDialog( + title = stringResource(R.string.setting_contact_message), + description = stringResource(R.string.setting_contact_explanation_message), + onClickCancel = null, + onClickConfirm = { showInquiryGuideDialog = false }, + ) + } + + if (showSignOutDialog.value) { + SignOutDialog( + onConfirmClick = { + accountViewModel.requestSignOut() + showSignOutDialog.value = false + }, + onDismissClick = { showSignOutDialog.value = false }, + ) + } + + Loading( + isVisible = (signOutSuccess == Status.LOADING || + signOutSuccess == Status.SUCCESS), + ) +} + +@Composable +private fun SettingsScreen( + profile: Profile?, + onProfileEditClick: () -> Unit, + onBackClick: () -> Unit, + onSettingNotificationClick: () -> Unit, + onPasswordChangeClick: () -> Unit, + onNoticesClick: () -> Unit = {}, + onBlockUsersClick: () -> Unit, + onInquiryClick: () -> Unit = {}, + onInformationClick: () -> Unit = {}, + onWithdrawClick: () -> Unit, + onSignOutClick: () -> Unit = {}, +) { + Scaffold( + topBar = { + TopNavigation( + leftIcon = { + IconButton( + onClick = onBackClick, + modifier = Modifier.indication( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back_sign), + contentDescription = stringResource(id = R.string.back_sign), + tint = Gray1_50545B + ) + } + }, + title = stringResource(id = R.string.setting_title), + titleAlignment = TopNavigationAlign.CENTER + ) + }, + containerColor = White + ) { contentPadding -> + val scrollState = rememberScrollState() + val context = LocalContext.current + val appVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(0)).versionName + } else { + context.packageManager.getPackageInfo(context.packageName, 0).versionName + } ?: "" + + val settingMenus = listOf( + SettingItem(R.string.setting_menu_change_password, R.drawable.ic_setting_password_change, onClickMenu = onPasswordChangeClick), + SettingItem(R.string.setting_menu_block_user, R.drawable.ic_block, onClickMenu = onBlockUsersClick), + SettingItem(R.string.setting_menu_notification, R.drawable.ic_notification, onClickMenu = onSettingNotificationClick), + null, // Divider + SettingItem(R.string.setting_menu_notice, R.drawable.ic_setting_notice, onClickMenu = onNoticesClick), + SettingItem(R.string.setting_menu_information, R.drawable.ic_setting_information, onClickMenu = onInformationClick, description = appVersion), + SettingItem(R.string.setting_menu_contact, R.drawable.ic_setting_contact, onClickMenu = onInquiryClick), + null // Divider + ) + + Column( + modifier = Modifier + .verticalScroll(scrollState) + .padding(contentPadding) + .padding(top = 16.dp) + .padding(horizontal = 20.dp) + ) { + SettingProfile(profile, onProfileEditClick) + Spacer(modifier = Modifier.height(28.dp)) + settingMenus.forEach { menu -> + menu?.run { + SettingMenu( + titleId = titleId, + iconId = iconId, + onClickMenu = onClickMenu, + description = description + ) + } ?: Divider( + modifier = Modifier.padding(vertical = 12.dp), + color = Gray6_F0F1F3 + ) + } + Text( + text = stringResource(id = R.string.sign_out), + modifier = Modifier + .padding(vertical = 11.5.dp, horizontal = 8.dp) + .clickable { onSignOutClick() }, + color = Primary_23C882, + style = DayoTheme.typography.b6 + ) + Text( + text = stringResource(id = R.string.delete_account), + modifier = Modifier + .padding(vertical = 11.5.dp, horizontal = 8.dp) + .clickable { onWithdrawClick() }, + color = Gray3_9FA5AE, + style = DayoTheme.typography.b6 + ) + } + } +} + +@Composable +private fun SettingProfile( + profile: Profile?, + onProfileEditClick: () -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // profile image + RoundImageView( + context = LocalContext.current, + imageUrl = "${BuildConfig.BASE_URL}/images/${profile?.profileImg}", + imageDescription = stringResource(id = R.string.setting_my_profile_image_description), + roundSize = 28.dp, + modifier = Modifier.size(56.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // nickname + Text( + text = profile?.nickname ?: "", + color = Dark, + style = DayoTheme.typography.b2 + ) + + // email + Text( + text = profile?.email ?: "", + color = Gray3_9FA5AE, + style = DayoTheme.typography.b6 + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // edit profile + TextButton( + onClick = onProfileEditClick, + shape = RoundedCornerShape(size = 12.dp), + border = BorderStroke(width = 1.dp, color = Gray6_F0F1F3), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = White_FFFFFF, + contentColor = Gray2_767B83 + ), + modifier = Modifier.height(36.dp), + content = { + Text( + text = stringResource(id = R.string.my_profile_edit_title), + modifier = Modifier.padding(horizontal = 28.dp), + style = DayoTheme.typography.b6.copy(Gray1_50545B) + ) + } + ) + } +} + +@Composable +private fun SettingMenu( + @StringRes titleId: Int, + @DrawableRes iconId: Int, + onClickMenu: () -> Unit, + description: String = "" +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(44.dp) + .clickable { onClickMenu() }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = iconId), + contentDescription = stringResource(id = titleId), + modifier = Modifier.padding(horizontal = 8.dp), + tint = Dark + ) + + Text( + text = stringResource(id = titleId), + modifier = Modifier.weight(1f), + color = Dark, + style = DayoTheme.typography.b6 + ) + + Text( + text = description, + modifier = Modifier.padding(horizontal = 8.dp), + color = Gray2_767B83, + style = DayoTheme.typography.b6 + ) + + Icon( + painter = painterResource(id = R.drawable.ic_next), + contentDescription = stringResource(id = titleId), + tint = Gray4_C5CAD2 + ) + } +} + +fun copyInquiryEmail( + context: Context, + inquiryEmail: String, +) { + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText("inquiry_email", inquiryEmail) + clipboardManager.setPrimaryClip(clipData) +} + + +@Composable +@Preview +fun SignOutDialog( + onConfirmClick: () -> Unit = {}, + onDismissClick: () -> Unit = {} +) { + ConfirmDialog( + title = stringResource(R.string.sign_out_message), + description = "", + onClickConfirmText = stringResource(R.string.confirm), + onClickConfirm = onConfirmClick, + onClickCancelText = stringResource(R.string.cancel), + onClickCancel = onDismissClick, + ) +} + +@Preview +@Composable +private fun PreviewSettingsScreen() { + DayoTheme { + SettingsScreen( + profile = null, + onBackClick = {}, + onProfileEditClick = {}, + onPasswordChangeClick = {}, + onSettingNotificationClick = {}, + onNoticesClick = {}, + onBlockUsersClick = {}, + onInformationClick = {}, + onWithdrawClick = {}, + ) + } +} + +data class SettingItem( + val titleId: Int, + val iconId: Int, + val onClickMenu: () -> Unit, + val description: String = "" +) diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/ImageCropScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/ImageCropScreen.kt new file mode 100644 index 00000000..e4d81488 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/ImageCropScreen.kt @@ -0,0 +1,349 @@ +package daily.dayo.presentation.screen.write + +import android.graphics.Bitmap +import android.net.Uri +import android.widget.Toast +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.toRect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import daily.dayo.presentation.R +import daily.dayo.presentation.common.image.ExifInfo +import daily.dayo.presentation.common.image.decodeSampledBitmapFromUri +import daily.dayo.presentation.common.image.readExifInfo +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.view.DayoTextButton +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.viewmodel.WriteViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.math.min +import kotlin.math.roundToInt + +@Composable +fun ImageCropScreen( + navController: NavController, + viewModel: WriteViewModel, + imageIndex: Int, + onBackClick: () -> Unit, +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val imageAssets by viewModel.writeImagesUri.collectAsState() + val imageAsset = imageAssets.getOrNull(imageIndex) + + var containerSize by remember { mutableStateOf(null) } + var stateHolder by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + viewModel.errorEvent.collect { message -> + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + } + + // Uri๋กœ๋ถ€ํ„ฐ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋น„ํŠธ๋งต๊ณผ EXIF ์ •๋ณด ๋กœ๋“œ (๋กœ๋”ฉ ์ƒํƒœ ์ฒ˜๋ฆฌ) + val bitmapState = + produceState?>( + initialValue = null, + key1 = imageAsset, + key2 = containerSize + ) { + // containerSize๊ฐ€ ์ •ํ•ด์ง„ ํ›„์—์•ผ ์ด ๋ธ”๋ก์ด ์‹คํ–‰๋จ + if (imageAsset != null && containerSize != null) { + value = withContext(Dispatchers.IO) { + val uri = Uri.parse(imageAsset.uriString) + val exifInfo = context.contentResolver.readExifInfo(uri) + // ์ด๋ฏธ์ง€๋ฅผ ์ง์ ‘ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๊ณ , ๊ธฐ๊ธฐ์— ๋งž๊ฒŒ ๋””์ฝ”๋”ฉํ•ด์„œ ๊ฐ€์ ธ์˜ด + val bitmap = decodeSampledBitmapFromUri( + context.contentResolver, + uri, + containerSize!!.width, + containerSize!!.height + ) + Pair(bitmap, exifInfo) + } + } else { + null + } + } + val sampledBitmap = bitmapState.value?.first + val exifInfo = bitmapState.value?.second + + // ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ ๋˜๋Š” Uri๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ + if (imageAsset == null) { + LaunchedEffect(Unit) { onBackClick() } + return + } + + // ๋น„ํŠธ๋งต๊ณผ ์ปจํ…Œ์ด๋„ˆ ์‚ฌ์ด์ฆˆ๊ฐ€ ์ค€๋น„๋˜๋ฉด stateHolder๋ฅผ ์ƒ์„ฑ + LaunchedEffect(sampledBitmap, containerSize, exifInfo) { + if (sampledBitmap != null && containerSize != null) { + stateHolder = ImageCropStateHolder( + originalBitmap = sampledBitmap, + containerSize = containerSize!!, + exifInfo = exifInfo + ) + } + } + + Scaffold( + topBar = { + ImageCropActionbarLayout( + onBackClick = onBackClick, + isCropEnabled = stateHolder != null, + onCropClick = { + stateHolder?.let { holder -> + scope.launch { + viewModel.cropImageAndUpdate(imageIndex, holder) + navController.popBackStack() + } + } + } + ) + } + ) { innerPadding -> + Surface(modifier = Modifier.padding(innerPadding)) { + Box( + modifier = Modifier + .fillMaxSize() + .background(DayoTheme.colorScheme.background) + .onSizeChanged { containerSize = it }, + contentAlignment = Alignment.Center + ) { + // stateHolder์˜ ์กด์žฌ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๋กœ๋”ฉ UI ๋˜๋Š” Crop UI๋ฅผ ํ‘œ์‹œ + stateHolder?.let { holder -> + Image( + bitmap = sampledBitmap!!.asImageBitmap(), + contentDescription = stringResource(id = R.string.write_post_image_edit), + contentScale = ContentScale.FillWidth, + modifier = Modifier + .fillMaxWidth() + .height((holder.imageHeight / LocalDensity.current.density).dp) + ) + ImageCropCanvas(stateHolder = holder) + } ?: run { + CircularProgressIndicator() + } + } + } + } +} + +@Composable +private fun ImageCropCanvas(stateHolder: ImageCropStateHolder) { + val cropProperties by stateHolder.cropProperties + + // ๋‘ ์†๊ฐ€๋ฝ ์ œ์Šค์ฒ˜์™€ 4๊ฐœ์˜ ๋ชจ์„œ๋ฆฌ๋ฅผ ๋“œ๋ž˜๊ทธ ํ•˜๋Š” ์ œ์Šค์ฒ˜์˜ ๋™์‹œ ๋ฐœ์ƒ์„ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ฆฌ์‚ฌ์ด์ง• ์ง„ํ–‰์ค‘ FLag + var isResizing by remember { mutableStateOf(false) } + + val dimPath = remember(cropProperties) { + Path().apply { + val cropRect = Rect( + offset = cropProperties.cropOffset, + size = Size(cropProperties.cropSize, cropProperties.cropSize) + ) + addRect( + Rect( + left = 0f, + top = stateHolder.imageTop, + right = stateHolder.imageWidth, + bottom = stateHolder.imageTop + stateHolder.imageHeight + ) + ) + addRect(cropRect) + fillType = PathFillType.EvenOdd + } + } + + Box( + Modifier + .fillMaxSize() + .pointerInput(Unit) { + detectTransformGestures { _, pan, zoom, _ -> + if (!isResizing) { + stateHolder.handleTransform(pan, zoom) + } + } + } + ) { + Canvas(modifier = Modifier.fillMaxSize()) { + val cropRect = Rect( + offset = cropProperties.cropOffset, + size = Size(cropProperties.cropSize, cropProperties.cropSize) + ) + + // Dimmed Background + drawPath(path = dimPath, color = Color.Black.copy(alpha = 0.4f)) + + // Grid lines + val guideLineColor = Color.White.copy(alpha = 0.5f) + val strokeWidth = 1.dp.toPx() + val third = cropProperties.cropSize / 3 + drawLine( + guideLineColor, + Offset(cropRect.left + third, cropRect.top), + Offset(cropRect.left + third, cropRect.bottom), + strokeWidth + ) + drawLine( + guideLineColor, + Offset(cropRect.left + 2 * third, cropRect.top), + Offset(cropRect.left + 2 * third, cropRect.bottom), + strokeWidth + ) + drawLine( + guideLineColor, + Offset(cropRect.left, cropRect.top + third), + Offset(cropRect.right, cropRect.top + third), + strokeWidth + ) + drawLine( + guideLineColor, + Offset(cropRect.left, cropRect.top + 2 * third), + Offset(cropRect.right, cropRect.top + 2 * third), + strokeWidth + ) + + drawRect( + guideLineColor, + topLeft = cropRect.topLeft, + size = cropRect.size, + style = Stroke(width = 2.dp.toPx()) + ) + } + + CropHandles( + cropProperties = cropProperties, + onResizeStart = { isResizing = true }, + onResizeEnd = { isResizing = false }, + onDrag = { corner, dragAmount -> stateHolder.handleCornerDrag(corner, dragAmount) } + ) + } +} + +@Composable +private fun CropHandles( + cropProperties: CropProperties, + onResizeStart: () -> Unit, + onResizeEnd: () -> Unit, + onDrag: (Corner, Offset) -> Unit +) { + val density = LocalDensity.current + val handleSizeDp = 32.dp + val handleRadiusPx = with(density) { (handleSizeDp / 2).toPx() } + + Corner.values().forEach { corner -> + val position = Offset( + x = cropProperties.cropOffset.x + if (corner.isLeft()) 0f else cropProperties.cropSize, + y = cropProperties.cropOffset.y + if (corner.isTop()) 0f else cropProperties.cropSize + ) + + Box( + modifier = Modifier + .offset { + IntOffset( + (position.x - handleRadiusPx).roundToInt(), + (position.y - handleRadiusPx).roundToInt() + ) + } + .size(handleSizeDp) + .pointerInput(corner) { + detectDragGestures( + onDragStart = { onResizeStart() }, + onDragEnd = { onResizeEnd() }, + onDragCancel = { onResizeEnd() } + ) { change, dragAmount -> + change.consume() + onDrag(corner, dragAmount) + } + } + ) { + Canvas(Modifier.fillMaxSize()) { + val center = size.toRect().center + val stroke = 3.dp.toPx() + val length = min(cropProperties.cropSize * 0.1f, 20.dp.toPx()) + + val hSign = if (corner.isLeft()) 1f else -1f + val vSign = if (corner.isTop()) 1f else -1f + + drawLine( + Color.White, + start = center, + end = Offset(center.x + (length * hSign), center.y), + strokeWidth = stroke + ) + drawLine( + Color.White, + start = center, + end = Offset(center.x, center.y + (length * vSign)), + strokeWidth = stroke + ) + } + } + } +} + +@Composable +private fun ImageCropActionbarLayout( + onBackClick: () -> Unit, + isCropEnabled: Boolean, + onCropClick: () -> Unit, +) { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = onBackClick, + iconContentDescription = stringResource(R.string.back_sign), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + title = stringResource(R.string.write_post_image_crop_title), + rightIcon = { + DayoTextButton( + onClick = { + if (isCropEnabled) onCropClick() + }, + text = stringResource(R.string.complete), + textStyle = DayoTheme.typography.b3.copy( + textAlign = TextAlign.Center, + color = Dark + ), + modifier = Modifier.padding(10.dp), + ) + }, + titleAlignment = TopNavigationAlign.CENTER, + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/ImageCropStateHolder.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/ImageCropStateHolder.kt new file mode 100644 index 00000000..aeb92d76 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/ImageCropStateHolder.kt @@ -0,0 +1,105 @@ +package daily.dayo.presentation.screen.write + +import android.graphics.Bitmap +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntSize +import daily.dayo.presentation.common.image.ExifInfo +import kotlin.math.min + +data class ImageAsset( + val uriString: String, + val lastModified: Long = System.currentTimeMillis(), + val exifInfo: ExifInfo? = null +) + +data class CropProperties( + val cropSize: Float = 0f, + val cropOffset: Offset = Offset.Zero +) + +class ImageCropStateHolder( + private val originalBitmap: Bitmap, + private val containerSize: IntSize, + private val minCropSize: Float = 100f, + val exifInfo: ExifInfo? = null +) { + private val _cropProperties = mutableStateOf(CropProperties()) + val cropProperties: State = _cropProperties + + private val imageAspectRatio = originalBitmap.height.toFloat() / originalBitmap.width + val imageWidth = containerSize.width.toFloat() + val imageHeight = imageWidth * imageAspectRatio + val imageTop = (containerSize.height - imageHeight) / 2f + private val maxCropSize = min(imageWidth, imageHeight) + + init { + val initialSize = min(imageWidth, imageHeight) * 0.8f + _cropProperties.value = CropProperties( + cropSize = initialSize, + cropOffset = Offset( + x = (imageWidth - initialSize) / 2f, + y = imageTop + (imageHeight - initialSize) / 2f + ) + ) + } + + // ๋‘ ์†๊ฐ€๋ฝ์œผ๋กœ ๊ทธ๋ฆฌ๋“œ๋ฅผ ํ™•๋Œ€/์ถ•์†Œํ•˜๊ณ  ์ด๋™ํ•˜๋Š” ์ œ์Šค์ฒ˜ ์ฒ˜๋ฆฌ + fun handleTransform(pan: Offset, zoom: Float) { + val currentSize = _cropProperties.value.cropSize + val currentOffset = _cropProperties.value.cropOffset + + val newSize = (currentSize * zoom).coerceIn(minCropSize, maxCropSize) + val sizeDelta = (newSize - currentSize) / 2f + + val newOffsetX = (currentOffset.x - sizeDelta + pan.x) + .coerceIn(0f, imageWidth - newSize) + val newOffsetY = (currentOffset.y - sizeDelta + pan.y) + .coerceIn(imageTop, imageTop + imageHeight - newSize) + + _cropProperties.value = + CropProperties(cropSize = newSize, cropOffset = Offset(newOffsetX, newOffsetY)) + } + + // ๊ฐ ๊ทธ๋ฆฌ๋“œ ๋ชจ์„œ๋ฆฌ ๋“œ๋ž˜๊ทธ ์ œ์Šค์ฒ˜ ์ฒ˜๋ฆฌ + fun handleCornerDrag(corner: Corner, dragAmount: Offset) { + val currentSize = _cropProperties.value.cropSize + val currentOffset = _cropProperties.value.cropOffset + + val newSize = when (corner) { + Corner.TOP_LEFT, Corner.TOP_RIGHT, Corner.BOTTOM_LEFT, Corner.BOTTOM_RIGHT -> { + val potentialSizeX = + if (corner.isLeft()) currentSize - dragAmount.x else currentSize + dragAmount.x + val potentialSizeY = + if (corner.isTop()) currentSize - dragAmount.y else currentSize + dragAmount.y + min(potentialSizeX, potentialSizeY).coerceIn(minCropSize, maxCropSize) + } + } + + val newOffset = when (corner) { + Corner.TOP_LEFT -> Offset( + currentOffset.x + (currentSize - newSize), + currentOffset.y + (currentSize - newSize) + ) + + Corner.TOP_RIGHT -> Offset(currentOffset.x, currentOffset.y + (currentSize - newSize)) + Corner.BOTTOM_LEFT -> Offset(currentOffset.x + (currentSize - newSize), currentOffset.y) + Corner.BOTTOM_RIGHT -> currentOffset + } + + val finalSize = newSize.coerceIn(minCropSize, maxCropSize) + val finalOffset = Offset( + x = newOffset.x.coerceIn(0f, imageWidth - finalSize), + y = newOffset.y.coerceIn(imageTop, imageTop + imageHeight - finalSize) + ) + _cropProperties.value = CropProperties(cropSize = finalSize, cropOffset = finalOffset) + } +} + +enum class Corner { + TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT; + + fun isLeft(): Boolean = this == TOP_LEFT || this == BOTTOM_LEFT + fun isTop(): Boolean = this == TOP_LEFT || this == TOP_RIGHT +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderNewScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderNewScreen.kt new file mode 100644 index 00000000..312bea42 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderNewScreen.kt @@ -0,0 +1,213 @@ +package daily.dayo.presentation.screen.write + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import daily.dayo.domain.model.Privacy +import daily.dayo.presentation.R +import daily.dayo.presentation.common.extension.limitTo +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.DayoTextButton +import daily.dayo.presentation.view.DayoTextField +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.viewmodel.WriteViewModel + +const val WRITE_FOLDER_NEW_FOLDER_NAME_MAX_LENGTH = 12 +const val WRITE_FOLDER_NEW_FOLDER_DESCRIPTION_MAX_LENGTH = 20 + +@Composable +fun WriteFolderNewRoute( + onBackClick: () -> Unit, + writeViewModel: WriteViewModel = hiltViewModel(), +) { + val folderAddSuccess by writeViewModel.writeFolderAddSuccess.collectAsStateWithLifecycle() + LaunchedEffect(folderAddSuccess) { + if (folderAddSuccess.getContentIfNotHandled() == true) { + onBackClick() + } + } + + + WriteFolderNewScreen( + onBackClick = onBackClick, + writeViewModel = writeViewModel, + ) +} + +@Composable +fun WriteFolderNewScreen( + onBackClick: () -> Unit, + writeViewModel: WriteViewModel, +) { + val newFolderTitle = rememberSaveable { mutableStateOf("") } + val newFolderDescription = rememberSaveable { mutableStateOf("") } + var isToggled = rememberSaveable { mutableStateOf(false) } + + Surface { + Column( + modifier = Modifier.fillMaxSize() + ) { + WriteFolderNewActionBarLayout( + onBackClick = onBackClick, + onCreateNewFolderClick = { + writeViewModel.requestCreateFolderInPost( + newFolderTitle.value.limitTo(WRITE_FOLDER_NEW_FOLDER_NAME_MAX_LENGTH), + newFolderDescription.value.limitTo( + WRITE_FOLDER_NEW_FOLDER_DESCRIPTION_MAX_LENGTH + ), + if (isToggled.value) Privacy.ONLY_ME else Privacy.ALL + ) + }, + ) + WriteFolderNewContent(newFolderTitle, newFolderDescription, isToggled) + } + } +} + +@Composable +fun WriteFolderNewActionBarLayout( + onBackClick: () -> Unit, + onCreateNewFolderClick: () -> Unit, +) { + Column { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = onBackClick, + iconContentDescription = "Previous Page", + iconPainter = painterResource(id = R.drawable.ic_arrow_left) + ) + }, + title = stringResource(R.string.write_post_folder_new_title), + rightIcon = { + DayoTextButton( + onClick = { onCreateNewFolderClick() }, + text = stringResource(R.string.confirm), + textStyle = DayoTheme.typography.b3.copy( + textAlign = TextAlign.Center + ), + modifier = Modifier.padding(10.dp), + ) + }, + titleAlignment = TopNavigationAlign.CENTER + ) + } +} + +@Preview +@Composable +fun WriteFolderNewContent( + newFolderTitle: MutableState = mutableStateOf(""), + newFolderDescription: MutableState = mutableStateOf(""), + isToggled: MutableState = mutableStateOf(false), +) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(White_FFFFFF) + ) { + DayoTextField( + modifier = Modifier + .padding(horizontal = 18.dp) + .fillMaxWidth() + .wrapContentHeight(), + value = newFolderTitle.value, + onValueChange = { textValue -> + if (textValue.length > WRITE_FOLDER_NEW_FOLDER_NAME_MAX_LENGTH) + return@DayoTextField + + newFolderTitle.value = textValue + }, + label = stringResource(R.string.write_post_folder_new_folder_name_title), + placeholder = stringResource(R.string.write_post_folder_new_folder_name_placeholder) + ) + Spacer(modifier = Modifier.height(28.dp)) + DayoTextField( + modifier = Modifier + .padding(horizontal = 18.dp) + .fillMaxWidth() + .wrapContentHeight(), + value = newFolderDescription.value, + onValueChange = { textValue -> + if (textValue.length > WRITE_FOLDER_NEW_FOLDER_DESCRIPTION_MAX_LENGTH) + return@DayoTextField + + newFolderDescription.value = textValue + }, + label = stringResource(R.string.write_post_folder_new_folder_description_title), + placeholder = stringResource(R.string.write_post_folder_new_folder_description_placeholder) + ) + Spacer(modifier = Modifier.height(40.dp)) + ToggleButtonWithLabel( + isToggled = isToggled.value, + onToggleChanged = { isToggled.value = it } + ) + } +} + +@Composable +fun ToggleButtonWithLabel( + isToggled: Boolean, + onToggleChanged: (Boolean) -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + modifier = Modifier + .padding(18.dp) + .fillMaxWidth() + .wrapContentHeight() + ) { + Text( + text = stringResource(R.string.write_post_folder_new_folder_privacy_title), + style = DayoTheme.typography.b6.copy(color = Gray3_9FA5AE), + ) + Spacer(modifier = Modifier.width(8.dp)) + Switch( + checked = isToggled, + onCheckedChange = { onToggleChanged(it) }, + colors = SwitchDefaults.colors( + uncheckedThumbColor = White_FFFFFF, + checkedThumbColor = White_FFFFFF, + checkedTrackColor = Primary_23C882, + uncheckedTrackColor = Gray6_F0F1F3, + uncheckedBorderColor = Gray6_F0F1F3, + checkedBorderColor = Primary_23C882, + ) + ) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt new file mode 100644 index 00000000..37fbd925 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteFolderScreen.kt @@ -0,0 +1,360 @@ +package daily.dayo.presentation.screen.write + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.size.Size +import daily.dayo.domain.model.Folder +import daily.dayo.domain.model.Privacy +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_AD_START_COUNT +import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_THUMBNAIL_RADIUS_SIZE +import daily.dayo.presentation.common.constant.FolderConstants.FOLDER_THUMBNAIL_SIZE +import daily.dayo.presentation.common.constant.FolderConstants.MAX_FOLDER_COUNT +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.common.extension.limitTo +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.RoundImageView +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.viewmodel.AccountViewModel +import daily.dayo.presentation.viewmodel.ProfileViewModel +import daily.dayo.presentation.viewmodel.WriteViewModel + +@Composable +fun WriteFolderRoute( + onBackClick: () -> Unit, + onWriteFolderNewClick: () -> Unit, + onAdRequest: (onRewardSuccess: () -> Unit) -> Unit, + writeViewModel: WriteViewModel = hiltViewModel(), + accountViewModel: AccountViewModel = hiltViewModel(), + profileViewModel: ProfileViewModel = hiltViewModel(), +) { + val folders by profileViewModel.folders.collectAsStateWithLifecycle() + val folderId by writeViewModel.writeFolderId.collectAsState() + + LaunchedEffect(Unit) { + profileViewModel.requestFolderList( + memberId = accountViewModel.getCurrentUserInfo().memberId!!, + true + ) + } + + WriteFolderScreen( + showCreateFolder = (folders.data?.size ?: 0) < MAX_FOLDER_COUNT, + onBackClick = onBackClick, + onFolderClick = { folderId, folderName -> + writeViewModel.setFolderId(folderId) + writeViewModel.setFolderName(folderName) + onBackClick() + }, + navigateToCreateNewFolder = { onWriteFolderNewClick() }, + folders = folders.data.orEmpty(), + currentFolderId = folderId, + onAdRequest = onAdRequest + ) + +} + +@Composable +fun WriteFolderScreen( + showCreateFolder: Boolean, + onBackClick: () -> Unit, + onFolderClick: (Long, String) -> Unit, + navigateToCreateNewFolder: () -> Unit, + folders: List, + currentFolderId: Long?, + onAdRequest: (onRewardSuccess: () -> Unit) -> Unit +) { + Surface( + color = White_FFFFFF, + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier.fillMaxSize() + ) { + WriteFolderActionbarLayout( + onBackClick = onBackClick + ) + WriteFolderContent( + showCreateFolder = showCreateFolder, + onFolderClick = onFolderClick, + navigateToCreateNewFolder = navigateToCreateNewFolder, + folders = folders, + currentFolderId = currentFolderId, + onAdRequest = onAdRequest + ) + } + } +} + +@Composable +fun WriteFolderActionbarLayout(onBackClick: () -> Unit) { + Column { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = { onBackClick() }, + iconContentDescription = stringResource(R.string.previous), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + title = stringResource(R.string.write_post_folder_title), + titleAlignment = TopNavigationAlign.CENTER + ) + } +} + +@Composable +fun WriteFolderContent( + showCreateFolder: Boolean, + onFolderClick: (Long, String) -> Unit = { _, _ -> }, + navigateToCreateNewFolder: () -> Unit = {}, + folders: List = emptyList(), + currentFolderId: Long? = null, + onAdRequest: (onRewardSuccess: () -> Unit) -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 18.dp) + ) { + if (showCreateFolder) { + WriteFolderNewLayout( + shouldShowAd = folders.size + 1 in FOLDER_AD_START_COUNT..MAX_FOLDER_COUNT, + navigateToCreateNewFolder = navigateToCreateNewFolder, + onAdRequest = onAdRequest + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + ) + } + WriteFoldersLayout( + onFolderClick = onFolderClick, + folders = folders.limitTo(MAX_FOLDER_COUNT), // 5๊ฐœ์ด์ƒ ์˜ค๋”๋ผ๋„ 5๊ฐœ๊นŒ์ง€๋งŒ ๋ณด์—ฌ์ฃผ๋„๋ก ํด๋ผ์ด์–ธํŠธ ์ธก๋„ ์ฒ˜๋ฆฌ + currentFolderId = currentFolderId + ) + } +} + +@Composable +fun WriteFolderNewLayout( + shouldShowAd: Boolean, + navigateToCreateNewFolder: () -> Unit, + onAdRequest: (onRewardSuccess: () -> Unit) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(FOLDER_THUMBNAIL_SIZE.dp) + .background(White_FFFFFF) + .clickableSingle { + // ๊ด‘๊ณ  ๋ณด๊ธฐ + if (shouldShowAd) { + onAdRequest { + navigateToCreateNewFolder() + } + } else { + navigateToCreateNewFolder() + } + } + ) { + Image( + contentDescription = "new folderThumbnail", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(FOLDER_THUMBNAIL_SIZE.dp) + .clip(RoundedCornerShape(FOLDER_THUMBNAIL_RADIUS_SIZE.dp)), + painter = painterResource(id = R.drawable.ic_folder_new), + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .wrapContentWidth(Alignment.Start) + .wrapContentHeight(Alignment.CenterVertically), + text = stringResource(R.string.write_post_folder_navigate_create_folder), + style = DayoTheme.typography.b4.copy( + color = Gray2_767B83, + textAlign = TextAlign.Center + ) + ) + } + +} + +@Composable +fun WriteFoldersLayout( + onFolderClick: (Long, String) -> Unit, + folders: List, + currentFolderId: Long?, +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + itemsIndexed(folders) { _, folder -> + WriteFolderItemLayout( + folder = folder, + isSelected = folder.folderId == currentFolderId, + onFolderClick = onFolderClick + ) + } + } + +} + +@Preview +@Composable +fun WriteFolderItemLayout( + folder: Folder = Folder( + folderId = 1, + title = "", + memberId = "", + privacy = Privacy.ONLY_ME, + subheading = "", + thumbnailImage = "", + postCount = 1 + ), + isSelected: Boolean = true, + onFolderClick: (Long, String) -> Unit = { _, _ -> }, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(FOLDER_THUMBNAIL_SIZE.dp) + .clip(RoundedCornerShape(FOLDER_THUMBNAIL_RADIUS_SIZE.dp)) + .background(White_FFFFFF) + .clickableSingle { folder.folderId?.let { onFolderClick(it, folder.title) } } + ) { + Box( + modifier = Modifier + .size(FOLDER_THUMBNAIL_SIZE.dp) + ) { + RoundImageView( + context = LocalContext.current, + imageUrl = "${BuildConfig.BASE_URL}/images/${folder.thumbnailImage}", + modifier = Modifier.size(FOLDER_THUMBNAIL_SIZE.dp), + imageSize = Size(FOLDER_THUMBNAIL_SIZE, FOLDER_THUMBNAIL_SIZE), + roundSize = FOLDER_THUMBNAIL_RADIUS_SIZE.dp, + ) + if (isSelected) { + Box( + modifier = Modifier + .size(FOLDER_THUMBNAIL_SIZE.dp) + .clip(RoundedCornerShape(size = FOLDER_THUMBNAIL_RADIUS_SIZE.dp)) + .background(Primary_23C882.copy(alpha = 0.6f)) + ) { + Image( + painter = painterResource(id = R.drawable.ic_check_mark), + contentDescription = "Selected", + contentScale = ContentScale.Crop, + colorFilter = ColorFilter.tint(White_FFFFFF), + modifier = Modifier + .size(18.dp) + .align(Alignment.Center) + ) + } + } + } + Spacer(modifier = Modifier.width(12.dp)) + Column( + modifier = Modifier + .fillMaxHeight() + .wrapContentHeight(Alignment.CenterVertically) + ) { + Row { + if (folder.privacy == Privacy.ONLY_ME) { + Image( + modifier = Modifier + .height(24.dp) + .wrapContentHeight(Alignment.CenterVertically), + painter = painterResource(id = R.drawable.ic_lock), + contentDescription = "Only Me", + colorFilter = ColorFilter.tint(Dark) + ) + Spacer(modifier = Modifier.width(4.dp)) + } + Text( + modifier = Modifier + .fillMaxWidth() + .height(24.dp) + .wrapContentWidth(Alignment.Start) + .wrapContentHeight(Alignment.CenterVertically), + text = folder.title, + style = DayoTheme.typography.b4.copy( + color = Dark, + ) + ) + } + Spacer(modifier = Modifier.height(2.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .height(24.dp) + .wrapContentWidth(Alignment.Start) + .wrapContentHeight(Alignment.CenterVertically), + text = stringResource(R.string.write_post_folder_detail_current_count).format(folder.postCount), + style = DayoTheme.typography.b6.copy( + color = Gray3_9FA5AE, + ) + ) + } + } +} + +@Preview +@Composable +private fun PreviewWriteFolderNewLayout() { + WriteFolderNewLayout( + shouldShowAd = false, + navigateToCreateNewFolder = {}, + onAdRequest = {} + ) +} diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteNavigation.kt new file mode 100644 index 00000000..938dace2 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteNavigation.kt @@ -0,0 +1,196 @@ +package daily.dayo.presentation.screen.write + +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import androidx.navigation.navigation +import daily.dayo.presentation.viewmodel.WriteViewModel + +fun NavController.navigateWrite() { + navigate(WriteRoute.route) +} + +fun NavController.navigatePostEdit(postId: Long) { + navigate(WriteRoute.postEdit(postId)) +} + +fun NavController.navigateToWritePostWithFolder(folderId: Long) { + navigate(WriteRoute.writeWithQueries(folderId)) +} + +fun NavController.navigateWriteTag() { + navigate(WriteRoute.tag) +} + +fun NavController.navigateWriteFolder() { + navigate(WriteRoute.folder) +} + +fun NavController.navigateWriteFolderNew() { + navigate(WriteRoute.folderNew) +} + +fun NavController.navigateCrop(index: Int) { + navigate(WriteRoute.getCropRoute(index)) +} + +@OptIn(ExperimentalMaterial3Api::class) +fun NavGraphBuilder.writeNavGraph( + snackBarHostState: SnackbarHostState, + navController: NavController, + navigateToWritePost: (Long) -> Unit, + onBackClick: () -> Unit, + onTagClick: () -> Unit, + onWriteFolderClick: () -> Unit, + onWriteFolderNewClick: () -> Unit, + onAdRequest: (onRewardSuccess: () -> Unit) -> Unit, + bottomSheetState: BottomSheetScaffoldState, + bottomSheetContent: (@Composable () -> Unit) -> Unit, +) { + navigation( + startDestination = WriteRoute.route, + route = "writeNavigation" + ) { + composable( + route = WriteRoute.routeWithQueries, + arguments = listOf( + navArgument("folderId") { + type = NavType.StringType + defaultValue = null + nullable = true + } + ) + ) { + val parentStackEntry = remember(it) { + navController.getBackStackEntry(WriteRoute.writeRouteNavigation) + } + WriteRoute( + postId = null, + snackBarHostState = snackBarHostState, + navigateToWritePost = navigateToWritePost, + onBackClick = onBackClick, + onTagClick = onTagClick, + onWriteFolderClick = onWriteFolderClick, + onCropImageClick = { imgIdx -> navController.navigateCrop(imgIdx) }, + writeViewModel = hiltViewModel(parentStackEntry), + bottomSheetState = bottomSheetState, + bottomSheetContent = bottomSheetContent + ) + } + + composable( + route = WriteRoute.postEditRoute, + arguments = listOf(navArgument("postId") { type = NavType.LongType }) + ) { navBackStackEntry -> + val postId = navBackStackEntry.arguments?.getLong("postId") + val parentStackEntry = remember(navBackStackEntry) { + navController.getBackStackEntry(WriteRoute.writeRouteNavigation) + } + + // postId๊ฐ€ null์ด ์•„๋‹ ๋•Œ๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅ + postId?.let { + WriteRoute( + postId = it, + snackBarHostState = snackBarHostState, + navigateToWritePost = navigateToWritePost, + onBackClick = onBackClick, + onTagClick = onTagClick, + onWriteFolderClick = onWriteFolderClick, + onCropImageClick = { imgIdx -> navController.navigateCrop(imgIdx) }, + writeViewModel = hiltViewModel(parentStackEntry), + bottomSheetState = bottomSheetState, + bottomSheetContent = bottomSheetContent + ) + } + } + + composable(route = WriteRoute.tag) { + val parentStackEntry = remember(it) { + navController.getBackStackEntry(WriteRoute.writeRouteNavigation) + } + WriteTagRoute( + onBackClick = onBackClick, + writeViewModel = hiltViewModel(parentStackEntry), + ) + } + + composable(route = WriteRoute.folder) { + val parentStackEntry = remember(it) { + navController.getBackStackEntry(WriteRoute.writeRouteNavigation) + } + WriteFolderRoute( + onBackClick = onBackClick, + onWriteFolderNewClick = onWriteFolderNewClick, + onAdRequest = onAdRequest, + writeViewModel = hiltViewModel(parentStackEntry), + profileViewModel = hiltViewModel(parentStackEntry), + accountViewModel = hiltViewModel(parentStackEntry), + ) + } + + composable(route = WriteRoute.folderNew) { + val parentStackEntry = remember(it) { + navController.getBackStackEntry(WriteRoute.writeRouteNavigation) + } + WriteFolderNewRoute( + onBackClick = onBackClick, + writeViewModel = hiltViewModel(parentStackEntry), + ) + } + + composable( + route = WriteRoute.crop, + arguments = listOf(navArgument("index") { + type = NavType.IntType + }) + ) { backStackEntry -> + val parentStackEntry = remember(backStackEntry) { + navController.getBackStackEntry(WriteRoute.writeRouteNavigation) + } + val vm: WriteViewModel = hiltViewModel(parentStackEntry) + val index = backStackEntry.arguments!!.getInt("index") + + ImageCropScreen( + onBackClick = onBackClick, + imageIndex = index, + viewModel = vm, + navController = navController, + ) + } + } +} + +object WriteRoute { + const val route = "write" + private const val folderIdQuery = "folderId={folderId}" + const val routeWithQueries = "$route?$folderIdQuery" + + const val postEditRoute = "$route/{postId}" + const val writeRouteNavigation = "writeNavigation" + + const val tag = "${route}/tag" + const val folder = "${route}/folder" + const val folderNew = "${route}/folder/new" + + const val crop = "$route/crop/{index}" + + fun getCropRoute(index: Int): String { + return crop.replace("{index}", index.toString()) + } + + fun writeWithQueries(folderId: Long? = null): String { + val params = mutableListOf() + folderId?.let { params.add("folderId=$it") } + return if (params.isEmpty()) route else "$route?${params.joinToString("&")}" + } + + fun postEdit(postId: Long) = "$route/$postId" +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt new file mode 100644 index 00000000..9e8e7950 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteScreen.kt @@ -0,0 +1,793 @@ +package daily.dayo.presentation.screen.write + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.Surface +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetValue +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import coil.request.ImageRequest +import daily.dayo.domain.model.Category +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.screen.home.CategoryMenu +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray5_E8EAEE +import daily.dayo.presentation.theme.Gray7_F6F6F7 +import daily.dayo.presentation.theme.PrimaryL3_F2FBF7 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.DayoTextButton +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.view.dialog.BottomSheetDialog +import daily.dayo.presentation.viewmodel.WriteViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +const val WRITE_POST_IMAGE_MIN_SIZE = 1 +const val WRITE_POST_DETAIL_MAX_LENGTH = 200 +const val WRITE_POST_IMAGE_SIZE = 220 +const val WRITE_POST_TOP_Z_INDEX = 1f + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun WriteRoute( + postId: Long?, + snackBarHostState: SnackbarHostState, + navigateToWritePost: (Long) -> Unit, + onBackClick: () -> Unit, + onTagClick: () -> Unit, + onWriteFolderClick: () -> Unit, + onCropImageClick: (Int) -> Unit = {}, + writeViewModel: WriteViewModel = hiltViewModel(), + bottomSheetState: BottomSheetScaffoldState, + bottomSheetContent: (@Composable () -> Unit) -> Unit, +) { + val isPostEditMode = postId != null + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val option = BitmapFactory.Options().apply { + inPreferredConfig = Bitmap.Config.ARGB_8888 + } + + val postEditId by writeViewModel.postEditId.collectAsStateWithLifecycle() + val writeText by writeViewModel.writeText.collectAsStateWithLifecycle() + val imageAssets by writeViewModel.writeImagesUri.collectAsStateWithLifecycle() + val tags by writeViewModel.writeTags.collectAsStateWithLifecycle() + val folderId by writeViewModel.writeFolderId.collectAsStateWithLifecycle() + val folderName by writeViewModel.writeFolderName.collectAsStateWithLifecycle() + val writePostId by writeViewModel.writePostId.collectAsState(null) + val selectedCategory by writeViewModel.writeCategory.collectAsStateWithLifecycle() // name, index + val onClickCategory: (CategoryMenu, Int) -> Unit = { categoryMenu, index -> + writeViewModel.setPostCategory(Pair(categoryMenu.category, index)) + coroutineScope.launch { bottomSheetState.bottomSheetState.hide() } + } + val categoryMenus = listOf( + CategoryMenu.Scheduler, + CategoryMenu.StudyPlanner, + CategoryMenu.PocketBook, + CategoryMenu.SixHoleDiary, + CategoryMenu.Digital, + CategoryMenu.ETC + ) + val uploadSuccess by writeViewModel.uploadSuccess.collectAsStateWithLifecycle() + val postInfoSuccess by writeViewModel.postInfoSuccess.collectAsStateWithLifecycle(null) + + BackHandler { + if (bottomSheetState.bottomSheetState.currentValue == SheetValue.Expanded) { + coroutineScope.launch { bottomSheetState.bottomSheetState.hide() } + } else { + onBackClick() + } + } + + LaunchedEffect(postId) { + if (isPostEditMode && postEditId == null) { + writeViewModel.requestPostDetail(postId!!, categoryMenus) + } + } + + LaunchedEffect(uploadSuccess) { + when (uploadSuccess) { + Status.LOADING -> { + snackBarHostState.showSnackbar( + ContextCompat.getString(context, R.string.write_post_upload_state_loading) + ) + } + + Status.SUCCESS -> { + onBackClick() + writePostId?.let { newPostId -> + navigateToWritePost(newPostId) + } + } + + Status.ERROR -> { + snackBarHostState.showSnackbar( + ContextCompat.getString(context, R.string.write_post_upload_state_fail) + ) + } + + null -> {} + } + } + + LaunchedEffect(postInfoSuccess) { + if (postInfoSuccess == false) { + launch { + snackBarHostState.showSnackbar(ContextCompat.getString(context, R.string.write_post_edit_state_fail)) + } + onBackClick() + } + } + + WriteScreen( + isPostEditMode = isPostEditMode, + context = context, + onBackClick = onBackClick, + onUploadClick = { + coroutineScope.launch { + writeViewModel.requestUploadPost() + } + }, + setWriteText = { text -> + writeViewModel.setWriteText(text) + }, + writeText = writeText, + addImages = { uris -> + writeViewModel.addOriginalImages(uris) + }, + editImage = { index -> + onCropImageClick(index) + }, + deleteImageUri = { position -> + writeViewModel.removeUploadImage(position) + }, + clearImages = { + writeViewModel.clearUploadImage() + }, + processedImages = imageAssets, + navigateToCategory = { + coroutineScope.launch { bottomSheetState.bottomSheetState.expand() } + }, + categoryMenus = categoryMenus, + category = selectedCategory, + navigateToTag = { onTagClick() }, + tags = tags, + navigateToFolder = { onWriteFolderClick() }, + folderId = folderId, + folderName = folderName, + uploadSuccess = uploadSuccess, + ) + + bottomSheetContent { + CategoryBottomSheetDialog( + categoryMenus, + onClickCategory, + selectedCategory, + coroutineScope, + bottomSheetState + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CategoryBottomSheetDialog( + categoryMenus: List, + onCategorySelected: (CategoryMenu, Int) -> Unit, + selectedCategory: Pair, + coroutineScope: CoroutineScope, + bottomSheetState: BottomSheetScaffoldState +) { + + BottomSheetDialog( + sheetState = bottomSheetState, + buttons = categoryMenus.mapIndexed { index, category -> + Pair(category.name) { + onCategorySelected(category, index) + } + }, + title = stringResource(id = R.string.category), + leftIconButtons = categoryMenus.map { + ImageVector.vectorResource(it.defaultIcon) + }, + leftIconCheckedButtons = categoryMenus.map { + ImageVector.vectorResource(it.checkedIcon) + }, + normalColor = Gray2_767B83, + checkedColor = Primary_23C882, + checkedButtonIndex = selectedCategory.second, + closeButtonAction = { coroutineScope.launch { bottomSheetState.bottomSheetState.hide() } } + ) +} + +@Composable +fun WriteScreen( + isPostEditMode: Boolean, + context: Context = LocalContext.current, + onBackClick: () -> Unit, + onUploadClick: () -> Unit, + setWriteText: (String) -> Unit, + writeText: String, + addImages: (List) -> Unit, + editImage: (Int) -> Unit = {}, + deleteImageUri: (Int) -> Unit, + clearImages: () -> Unit = {}, + processedImages: List, + navigateToCategory: () -> Unit, + categoryMenus: List, + category: Pair = Pair(null, -1), + navigateToTag: () -> Unit, + tags: List = emptyList(), + navigateToFolder: () -> Unit, + folderId: Long? = null, + folderName: String? = null, + uploadSuccess: Status? = null, +) { + val interactionSource = remember { MutableInteractionSource() } + + val selectedUris = remember { mutableStateListOf() } + + // ์ด๋ฏธ์ง€ ์„ ํƒ ๋Ÿฐ์ฒ˜ + val imagePickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenMultipleDocuments(), + onResult = { uris -> + if (uris.isNotEmpty()) { + selectedUris.addAll(uris) + clearImages() + addImages(uris) + } else { + onBackClick.invoke() + } + } + ) + + LaunchedEffect(Unit) { + if (!isPostEditMode && processedImages.isEmpty()) { + imagePickerLauncher.launch(arrayOf("image/*")) + } + } + + Surface( + color = White_FFFFFF, + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier.fillMaxHeight() + ) { + WriteActionbarLayout( + title = if (isPostEditMode) stringResource(R.string.write_post_edit_title) else stringResource(R.string.write_post_title), + rightText = if (isPostEditMode) stringResource(R.string.save) else stringResource(R.string.write_post_upload), + onBackClick = onBackClick, + onUploadClick = onUploadClick, + isUploadEnable = processedImages.isNotEmpty() && category.first != null && folderId != null && folderName != null + ) + WriteUploadImages( + isPostEditMode = isPostEditMode, + images = processedImages, + deleteImage = { index -> deleteImageUri(index) }, + onEditImage = { index -> editImage(index) }, + ) + WritePostDetail( + setWriteText = setWriteText, + writeText = writeText + ) + Spacer(modifier = Modifier.height(36.dp)) + WriteCategoryLayout( + categoryMenus = categoryMenus, + category = category.first, + navigateToCategory = navigateToCategory + ) + Spacer(modifier = Modifier.height(16.dp)) + Divider( + modifier = Modifier + .padding(horizontal = 18.dp) + .fillMaxWidth() + .height(1.dp), + color = Gray7_F6F6F7 + ) + WriteTagLayout( + context = context, + tags = tags, + navigateToTag = navigateToTag + ) + Divider( + modifier = Modifier + .padding(horizontal = 18.dp) + .fillMaxWidth() + .height(1.dp), + color = Gray7_F6F6F7 + ) + WriteFolderLayout( + folderId = folderId, + folderName = folderName, + navigateToFolder = navigateToFolder + ) + } + + // ํด๋ฆญ ์ฐจ๋‹จ ๋ ˆ์ด์–ด + if (uploadSuccess == Status.LOADING || uploadSuccess == Status.SUCCESS) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + .zIndex(WRITE_POST_TOP_Z_INDEX) // ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ ์œ„์— ํ‘œ์‹œ + .clickable( + indication = null, + interactionSource = interactionSource + ) { + // DO NOTHING FOR BLOCKING CLICK + } + ) + } + } +} + +@Composable +fun WriteUploadImages( + isPostEditMode: Boolean, + images: List, + deleteImage: (Int) -> Unit, + onEditImage: (Int) -> Unit, +) { + LazyRow( + contentPadding = PaddingValues(start = 18.dp, end = 18.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.height(WRITE_POST_IMAGE_SIZE.dp) + ) { + itemsIndexed( + items = images, + key = { _, imageAsset -> imageAsset.uriString } + ) { index, imageAsset -> + val context = LocalContext.current + + // ์บ์‹œ ํ‚ค๋ฅผ uri, ์ˆ˜์ •์‹œ๊ฐ„, EXIF ์ •๋ณด๋กœ ์ง€์ •ํ•ด ์ด๋ฏธ์ง€ ํŽธ์ง‘ํ•˜๋Š” ๊ฒฝ์šฐ ๊ฐฑ์‹ ๋ ์ˆ˜ ์žˆ๋„๋ก ์ˆ˜์ • + val cacheKey = "${imageAsset.uriString}-${imageAsset.lastModified}-${imageAsset.exifInfo?.orientation ?: "none"}" + val imageRequest = ImageRequest.Builder(context) + .data( + if (isPostEditMode) { + "${BuildConfig.BASE_URL}/images/${imageAsset.uriString}" + } else { + imageAsset.uriString + } + ) + .memoryCacheKey(cacheKey) + .diskCacheKey(cacheKey) + .crossfade(true) + .build() + + Box(modifier = Modifier.size(WRITE_POST_IMAGE_SIZE.dp)) { + AsyncImage( + model = imageRequest, + contentDescription = "์—…๋กœ๋“œ ์ด๋ฏธ์ง€ $index", + modifier = Modifier + .size(WRITE_POST_IMAGE_SIZE.dp) + .clip(RoundedCornerShape(8.dp)), + contentScale = ContentScale.Crop + ) + + if (!isPostEditMode) { + Surface( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(start = 16.dp, bottom = 16.dp), + shape = RoundedCornerShape(99.dp), + color = Dark, + ) { + Row( + modifier = Modifier + .width(112.dp) + .height(36.dp) + .clickable { + onEditImage(index) + } + .padding(horizontal = 12.dp) + ) { + Image( + painter = painterResource(id = R.drawable.ic_crop), + contentDescription = "edit image", + modifier = Modifier + .align(Alignment.CenterVertically) + .size(20.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringResource(R.string.write_post_image_edit), + style = DayoTheme.typography.b5, + color = White_FFFFFF, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + } + if (images.size == WRITE_POST_IMAGE_MIN_SIZE) return@itemsIndexed + + Image( + painter = painterResource(id = R.drawable.ic_img_delete), + contentDescription = "delete image", + modifier = Modifier + .align(Alignment.TopEnd) + .padding((11.19).dp) + .size((22.4).dp) + .clickable { + deleteImage(index) + } + ) + } + } + } + } +} + +@Composable +fun WriteActionbarLayout( + title: String, + rightText: String, + onBackClick: () -> Unit, + onUploadClick: () -> Unit, + isUploadEnable: Boolean = false +) { + TopNavigation( + title = title, + leftIcon = { + NoRippleIconButton( + onClick = { + onBackClick() + }, + iconContentDescription = stringResource(R.string.previous), + iconPainter = painterResource(id = R.drawable.ic_arrow_left), + ) + }, + rightIcon = { + DayoTextButton( + onClick = { if (isUploadEnable) onUploadClick() }, + text = rightText, + textStyle = DayoTheme.typography.b3.copy( + textAlign = TextAlign.Center, + color = if (isUploadEnable) Primary_23C882 else Gray5_E8EAEE + ), + modifier = Modifier.padding(10.dp), + ) + }, + titleAlignment = TopNavigationAlign.CENTER + ) +} + +@Preview +@Composable +fun WritePostDetail( + setWriteText: (String) -> Unit = {}, + writeText: String = "" +) { + var writeContentValue by remember { mutableStateOf(TextFieldValue(writeText)) } + LaunchedEffect(writeText) { + if (writeText != writeContentValue.text) { + writeContentValue = TextFieldValue( + text = writeText, + selection = TextRange(writeText.length) + ) + } + } + + Column( + modifier = Modifier + .padding(top = 20.dp, start = 20.dp, end = 20.dp) + .fillMaxWidth() + .background(color = White_FFFFFF) + ) { + BasicTextField( + value = writeContentValue, + onValueChange = { + if (it.text.length > WRITE_POST_DETAIL_MAX_LENGTH) return@BasicTextField + + writeContentValue = it + setWriteText(it.text) + }, + singleLine = false, + modifier = Modifier + .height(height = 140.dp) + .fillMaxWidth() + .background(color = White_FFFFFF), + textStyle = DayoTheme.typography.b6.copy( + color = Dark, + ), + decorationBox = { innerTextField -> + if (writeContentValue.text.isEmpty()) { + Text( + text = stringResource(R.string.write_post_detail_placeholder), + style = DayoTheme.typography.b6.copy( + color = Gray4_C5CAD2, + ) + ) + } + innerTextField() + }, + ) + Text( + text = buildAnnotatedString { + withStyle( + style = DayoTheme.typography.caption3.toSpanStyle() + .copy(color = Gray2_767B83) + ) { + append(writeContentValue.text.length.toString()) + } + withStyle( + style = DayoTheme.typography.caption3.toSpanStyle() + .copy(color = Gray4_C5CAD2) + ) { + append( + stringResource(R.string.write_post_detail_count_limit) + .format(WRITE_POST_DETAIL_MAX_LENGTH) + ) + } + }, + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.End), + textAlign = TextAlign.End + ) + } +} + +@Preview +@Composable +fun WriteCategoryLayout( + categoryMenus: List = emptyList(), + category: Category? = null, + navigateToCategory: () -> Unit = {} +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .padding(horizontal = 18.dp) + .clip(RoundedCornerShape(20.dp)) + .clickableSingle { navigateToCategory() }, + shape = RoundedCornerShape(20.dp), + color = PrimaryL3_F2FBF7 + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + if (category == null) { + Text( + modifier = Modifier + .fillMaxHeight() + .wrapContentHeight(Alignment.CenterVertically), + text = stringResource(R.string.write_post_select_category_title), + style = DayoTheme.typography.b6, + color = Gray3_9FA5AE + ) + } else { + Image( + modifier = Modifier.fillMaxHeight(), + painter = painterResource(id = categoryMenus.first { it.category == category }.checkedIcon), + contentDescription = stringResource(R.string.write_post_select_category_title) + ) + Spacer(modifier = Modifier.size(2.dp)) + Text( + modifier = Modifier + .fillMaxHeight() + .wrapContentHeight(Alignment.CenterVertically), + text = categoryMenus.first { it.category == category }.name, + style = DayoTheme.typography.b6, + color = Color.Black + ) + } + Spacer(modifier = Modifier.weight(1f)) + Image( + modifier = Modifier.fillMaxHeight(), + painter = painterResource(id = R.drawable.ic_arrow_category_green), + contentDescription = stringResource(R.string.write_post_select_category_title) + ) + } + } +} + +@Preview +@Composable +fun WriteTagLayout( + context: Context = LocalContext.current, + tags: List = emptyList(), + navigateToTag: () -> Unit = {} +) { + Row( + modifier = Modifier + .clickableSingle { navigateToTag() } + .padding(horizontal = 18.dp) + .fillMaxWidth() + .height(48.dp) + .background(White_FFFFFF) + .padding(horizontal = 2.dp, vertical = 12.dp) + ) { + Text( + modifier = Modifier + .fillMaxHeight() + .wrapContentHeight(Alignment.CenterVertically), + text = stringResource(R.string.write_post_select_tag_title), + style = DayoTheme.typography.b3, + color = Dark + ) + Spacer( + modifier = Modifier + .weight(1f) + .widthIn(min = 54.dp) + ) + if (tags.isNotEmpty()) { + val tag = tags.joinToString(separator = ", ", postfix = " ") { + ContextCompat.getString(context, R.string.write_post_select_tag_contents).format(it) + } + Text( + modifier = Modifier + .fillMaxHeight() + .wrapContentHeight(Alignment.CenterVertically) + .weight(1f), + text = tag, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = DayoTheme.typography.b6, + color = Dark, + textAlign = TextAlign.End + ) + } else { + Text( + modifier = Modifier + .fillMaxHeight() + .wrapContentHeight(Alignment.CenterVertically) + .weight(1f), + text = stringResource(R.string.write_post_select_tag_subheading), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = DayoTheme.typography.b6, + color = Gray3_9FA5AE, + textAlign = TextAlign.End + ) + } + Image( + modifier = Modifier + .fillMaxHeight() + .padding(start = 2.dp), + painter = painterResource(id = R.drawable.ic_arrow_tag_gray), + contentDescription = stringResource(R.string.write_post_select_tag_title) + ) + } +} + +@Preview +@Composable +fun WriteFolderLayout( + folderId: Long? = null, + folderName: String? = null, + navigateToFolder: () -> Unit = {} +) { + Row( + modifier = Modifier + .clickableSingle { navigateToFolder() } + .padding(horizontal = 18.dp) + .fillMaxWidth() + .height(48.dp) + .background(White_FFFFFF) + .padding(horizontal = 2.dp, vertical = 12.dp) + ) { + Text( + modifier = Modifier + .fillMaxHeight() + .wrapContentHeight(Alignment.CenterVertically), + text = stringResource(R.string.write_post_select_folder_title), + style = DayoTheme.typography.b3, + color = Dark + ) + Spacer( + modifier = Modifier + .weight(1f) + .widthIn(min = 54.dp) + ) + if (!folderName.isNullOrEmpty()) { + Text( + modifier = Modifier + .fillMaxHeight() + .wrapContentHeight(Alignment.CenterVertically) + .weight(1f), + text = folderName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = DayoTheme.typography.b6, + color = Dark, + textAlign = TextAlign.End + ) + } else { + Text( + modifier = Modifier + .fillMaxHeight() + .wrapContentHeight(Alignment.CenterVertically) + .weight(1f), + text = stringResource(R.string.write_post_select_folder_subheading), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = DayoTheme.typography.b6, + color = Gray3_9FA5AE, + textAlign = TextAlign.End + ) + } + Image( + modifier = Modifier + .fillMaxHeight() + .padding(start = 2.dp), + painter = painterResource(id = R.drawable.ic_arrow_tag_gray), + contentDescription = stringResource(R.string.write_post_select_folder_title) + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteTagScreen.kt b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteTagScreen.kt new file mode 100644 index 00000000..449364c3 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/screen/write/WriteTagScreen.kt @@ -0,0 +1,439 @@ +package daily.dayo.presentation.screen.write + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.InputChip +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import daily.dayo.presentation.R +import daily.dayo.presentation.common.extension.limitTo +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray7_F6F6F7 +import daily.dayo.presentation.theme.PrimaryL1_8FD9B9 +import daily.dayo.presentation.theme.PrimaryL3_F2FBF7 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.DayoTextButton +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.view.TopNavigation +import daily.dayo.presentation.view.TopNavigationAlign +import daily.dayo.presentation.viewmodel.WriteViewModel + +const val TAG_MAX_COUNT = 8 +const val TAG_MAX_LENGTH = 20 + +@Composable +fun WriteTagRoute( + onBackClick: () -> Unit, + writeViewModel: WriteViewModel = hiltViewModel(), +) { + val currentTags = remember { mutableStateOf(writeViewModel.writeTags.value) } + BackHandler { + onBackClick() + } + + WriteTagScreen( + onBackClick = onBackClick, + currentTags = currentTags.value, + onAddTagsClick = { + // 8๊ธ€์ž ์ œํ•œ ํ•œ๋ฒˆ ๋” ํ™•์ธ + currentTags.value = + currentTags.value.toMutableList().apply { add(it.limitTo(TAG_MAX_LENGTH)) } + }, + onRemoveTagClick = { + currentTags.value = currentTags.value.toMutableList().apply { remove(it) } + }, + onSaveClick = { + writeViewModel.updatePostTags(currentTags.value.limitTo(TAG_MAX_COUNT)) + onBackClick() + } + ) +} + +@Composable +fun WriteTagScreen( + onBackClick: () -> Unit, + onRemoveTagClick: (String) -> Unit, + onAddTagsClick: (String) -> Unit = {}, + onSaveClick: () -> Unit, + currentTags: List, +) { + Surface { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f, true), + ) { + WritTagActionBarLayout( + onBackClick = onBackClick, + onSaveClick = onSaveClick + ) + WriteTagContent( + currentTags = currentTags, + onRemoveTagClick = onRemoveTagClick + ) + } + Box(modifier = Modifier.fillMaxWidth()) { + WriteTadAdd( + currentTags = currentTags, + currentTagCount = currentTags.size, + onAddTagsClick = onAddTagsClick + ) + } + } + } +} + +@Composable +fun WritTagActionBarLayout( + onBackClick: () -> Unit, + onSaveClick: () -> Unit +) { + Column { + TopNavigation( + leftIcon = { + NoRippleIconButton( + onClick = onBackClick, + iconContentDescription = "Previous Page", + iconPainter = painterResource(id = R.drawable.ic_arrow_left) + ) + }, + title = stringResource(R.string.write_post_tag_title), + rightIcon = { + DayoTextButton( + onClick = { + onSaveClick() + }, + text = stringResource(id = R.string.save), + textStyle = DayoTheme.typography.b3.copy( + textAlign = TextAlign.Center + ), + modifier = Modifier.padding(10.dp), + ) + }, + titleAlignment = TopNavigationAlign.CENTER + ) + } +} + +@Composable +fun WriteTagContent( + currentTags: List, + onRemoveTagClick: (String) -> Unit +) { + Box( + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.TopCenter) + ) { + WriteTags( + tags = currentTags, + onRemoveTagClick = { onRemoveTagClick(it) } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun WriteTags( + tags: List = listOf(), + onRemoveTagClick: (String) -> Unit = {} +) { + val scrollState = rememberScrollState() + + LaunchedEffect(tags.size) { + scrollState.animateScrollTo(scrollState.maxValue) + } + + Box( + modifier = Modifier + .padding(horizontal = 18.dp, vertical = 4.dp) + .fillMaxWidth() + ) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(scrollState), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + tags.forEach { tagText -> + InputChip( + modifier = Modifier + .defaultMinSize(1.dp, 1.dp) + .background( + color = PrimaryL3_F2FBF7, + shape = RoundedCornerShape(20.dp) + ) + .height(36.dp), + border = null, + selected = false, + onClick = { /* ํด๋ฆญ ๋™์ž‘ ์—†์Œ */ }, + label = { + Text( + text = tagText, + style = DayoTheme.typography.b5.copy( + color = Primary_23C882 + ) + ) + }, + leadingIcon = { + Box( + modifier = Modifier + .padding(start = 4.dp, end = 0.dp) + .background( + color = PrimaryL1_8FD9B9, + shape = CircleShape + ) + .padding( + vertical = 5.dp, + horizontal = 4.dp + ) + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_tag), + contentDescription = tagText, + tint = Primary_23C882 + ) + } + }, + trailingIcon = { + Box( + modifier = Modifier.size(16.dp) + ) { + Icon( + modifier = Modifier + .fillMaxSize() + .clickable { onRemoveTagClick(tagText) } + .padding(4.dp), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_x_sign), + contentDescription = "Remove $tagText", + tint = Gray4_C5CAD2, + ) + } + } + ) + } + } + } +} + +@Composable +fun WriteTadAdd( + currentTags: List = listOf(), + currentTagCount: Int = 0, + maxTagCount: Int = TAG_MAX_COUNT, + onAddTagsClick: (String) -> Unit = {} +) { + var writeContentValue by remember { mutableStateOf(TextFieldValue()) } + // ์ถ”๊ฐ€์กฐ๊ฑด + // 1. ํƒœ๊ทธ ๋‚ด์šฉ์ด ๋น„์–ด์žˆ์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค. (writeContentValue.text.isNotEmpty()) + // 2. ํ˜„์žฌ ํƒœ๊ทธ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€ ํƒœ๊ทธ ๊ฐœ์ˆ˜๋ณด๋‹ค ์ž‘์•„์•ผ ํ•œ๋‹ค. (currentTagCount < maxTagCount) + // 3. ํƒœ๊ทธ ๋‚ด์šฉ์ด 20์ž ์ดํ•˜์—ฌ์•ผ ํ•œ๋‹ค. (writeContentValue.text.length <= 20) + // 4. ํ˜„์žฌ ํƒœ๊ทธ ๋ชฉ๋ก์— ๊ฐ™์€ ํƒœ๊ทธ๊ฐ€ ์—†์–ด์•ผ ํ•œ๋‹ค. (!currentTags.contains(writeContentValue.text)) + val addBtnEnabled = + writeContentValue.text.isNotEmpty() + && currentTagCount < maxTagCount + && writeContentValue.text.length <= 20 + && !currentTags.contains(writeContentValue.text) + Column( + modifier = Modifier + .background(color = White_FFFFFF) + .padding( + start = 18.dp, + end = 18.dp, + top = 12.dp, + bottom = 20.dp + ) + .fillMaxWidth() + .wrapContentHeight() + .imePadding() + ) { + Row( + + ) { + Text( + modifier = Modifier.padding( + vertical = 2.dp + ), + text = stringResource(R.string.write_post_tag_subheading), + style = DayoTheme.typography.b6.copy( + color = Gray1_50545B + ) + ) + Spacer(modifier = Modifier.width(10.dp)) + Box( + modifier = Modifier + .background( + color = PrimaryL3_F2FBF7, + shape = RoundedCornerShape(12.dp) + ) + .padding( + vertical = 2.dp, + horizontal = 8.dp, + ) + ) { + Text( + text = buildAnnotatedString { + withStyle( + style = DayoTheme.typography.caption2.toSpanStyle().copy( + color = Primary_23C882 + ) + ) { + append("$currentTagCount ") + } + withStyle( + style = DayoTheme.typography.caption2.toSpanStyle() + .copy(color = Gray4_C5CAD2) + ) { + append( + stringResource(R.string.write_post_tag_count_limit).format( + maxTagCount + ) + ) + } + } + ) + } + } + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .align(alignment = Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically + ) { + BasicTextField( + value = writeContentValue, + onValueChange = { + if (it.text.length > 20) return@BasicTextField + writeContentValue = it + }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .background( + color = Gray7_F6F6F7, + shape = RoundedCornerShape(12.dp) + ) + .padding( + vertical = 8.dp, + horizontal = 16.dp, + ) + .align(alignment = Alignment.CenterVertically), + textStyle = DayoTheme.typography.b6.copy( + color = Dark, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ) + ), + decorationBox = { innerTextField -> + if (writeContentValue.text.isEmpty()) { + Text( + text = stringResource(R.string.write_post_tag_input_placeholder), + style = DayoTheme.typography.b6.copy( + color = Gray4_C5CAD2, + ) + ) + } + innerTextField() + }, + ) + Spacer(modifier = Modifier.width(8.dp)) + DayoTextButton( + modifier = Modifier + .background( + color = if (addBtnEnabled) { + Primary_23C882 + } else { + PrimaryL1_8FD9B9 + }, + shape = RoundedCornerShape(12.dp) + ) + .padding( + vertical = 8.dp, + horizontal = 16.dp + ) + .align(alignment = Alignment.CenterVertically), + onClick = { + if (!addBtnEnabled) { + return@DayoTextButton + } else { + onAddTagsClick(writeContentValue.text) + writeContentValue = TextFieldValue() + } + }, + text = stringResource(R.string.add), + textStyle = DayoTheme.typography.b5.copy( + color = if (addBtnEnabled) { + PrimaryL3_F2FBF7 + } else { + White_FFFFFF + }, + ), + ) + } + } +} + +@Preview +@Composable +fun WriteTagAddPreview() { + WriteTadAdd() +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/service/firebase/FirebaseMessagingService.kt b/presentation/src/main/java/daily/dayo/presentation/service/firebase/FirebaseMessagingService.kt index 9c33d6f5..707e7f33 100644 --- a/presentation/src/main/java/daily/dayo/presentation/service/firebase/FirebaseMessagingService.kt +++ b/presentation/src/main/java/daily/dayo/presentation/service/firebase/FirebaseMessagingService.kt @@ -30,7 +30,7 @@ class FirebaseMessagingService : FirebaseMessagingService() { super.onMessageReceived(remoteMessage) if (remoteMessage.data.isNotEmpty()) { val body = remoteMessage.data["body"] - val postId = remoteMessage.data["postId"] + val postId = remoteMessage.data["postId"]?.toLong() val memberId = remoteMessage.data["memberId"] sendNotification(body = body, postId = postId, memberId = memberId) } else if (remoteMessage.notification != null) { @@ -39,7 +39,7 @@ class FirebaseMessagingService : FirebaseMessagingService() { } } - private fun sendNotification(body: String?, postId: String?, memberId: String?) { + private fun sendNotification(body: String?, postId: Long?, memberId: String?) { val id = System.currentTimeMillis().toInt() // notification ํด๋ฆญ ์‹œ ์ด๋™ํ•˜๋Š” ์•กํ‹ฐ๋น„ํ‹ฐ diff --git a/presentation/src/main/java/daily/dayo/presentation/theme/Color.kt b/presentation/src/main/java/daily/dayo/presentation/theme/Color.kt new file mode 100644 index 00000000..0f2581d6 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/theme/Color.kt @@ -0,0 +1,35 @@ +package daily.dayo.presentation.theme + +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +val Dark = Color(0xFF313131) +val Primary_23C882 = Color(0xFF23C882) +val White_FFFFFF = Color(0xFFFFFFFF) + +val Gray1_50545B = Color(0xFF50545B) +val Gray2_767B83 = Color(0xFF767B83) +val Gray3_9FA5AE = Color(0xFF9FA5AE) +val Gray4_C5CAD2 = Color(0xFFC5CAD2) +val Gray5_E8EAEE = Color(0xFFE8EAEE) +val Gray6_F0F1F3 = Color(0xFFF0F1F3) +val Gray7_F6F6F7 = Color(0xFFF6F6F7) + +val Pink_F34D7A = Color(0xFFF34D7A) +val Blue_597CF2 = Color(0xFF597CF2) +val Yellow_FFEB5A = Color(0xFFFFEB5A) +val Red_FF4545 = Color(0xFFFF4545) + +val PrimaryL1_8FD9B9 = Color(0xFFBEEBD3) +val PrimaryL2_E6FBF1 = Color(0xFFE6FBF1) +val PrimaryL3_F2FBF7 = Color(0xFFF2FBF7) +val Transparent_White30 = Color(0x4DFFFFFF) +val primaryD1_0EB36E = Color(0xFF0EB36E) + +val LightColorScheme = lightColorScheme( + primary = Primary_23C882, + background = White_FFFFFF +) + +internal val LocalColorScheme = staticCompositionLocalOf { LightColorScheme } diff --git a/presentation/src/main/java/daily/dayo/presentation/theme/Shapes.kt b/presentation/src/main/java/daily/dayo/presentation/theme/Shapes.kt new file mode 100644 index 00000000..a05c97da --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/theme/Shapes.kt @@ -0,0 +1,6 @@ +package daily.dayo.presentation.theme + +import androidx.compose.material3.Shapes +import androidx.compose.runtime.staticCompositionLocalOf + +internal val LocalShapes = staticCompositionLocalOf { Shapes() } diff --git a/presentation/src/main/java/daily/dayo/presentation/theme/Theme.kt b/presentation/src/main/java/daily/dayo/presentation/theme/Theme.kt new file mode 100644 index 00000000..dd694ebc --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/theme/Theme.kt @@ -0,0 +1,44 @@ +package daily.dayo.presentation.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable + +@Composable +fun DayoTheme( + colorScheme: ColorScheme = DayoTheme.colorScheme, + shapes: Shapes = DayoTheme.shapes, + content: @Composable () -> Unit +) { + CompositionLocalProvider( + LocalColorScheme provides colorScheme, + LocalShapes provides shapes, + LocalTypography provides Typography + ) { + MaterialTheme( + colorScheme = colorScheme, + shapes = shapes, + content = content + ) + } +} + +object DayoTheme { + val colorScheme: ColorScheme + @Composable + @ReadOnlyComposable + get() = LocalColorScheme.current + + val typography: DayoTypography + @Composable + @ReadOnlyComposable + get() = LocalTypography.current + + val shapes: Shapes + @Composable + @ReadOnlyComposable + get() = LocalShapes.current +} diff --git a/presentation/src/main/java/daily/dayo/presentation/theme/Type.kt b/presentation/src/main/java/daily/dayo/presentation/theme/Type.kt new file mode 100644 index 00000000..5a7b1a57 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/theme/Type.kt @@ -0,0 +1,168 @@ +package daily.dayo.presentation.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import daily.dayo.presentation.R + +private val PretendardKrFontFamily = FontFamily( + Font(R.font.pretendard_black, FontWeight.Black), + Font(R.font.pretendard_extra_bold, FontWeight.ExtraBold), + Font(R.font.pretendard_bold, FontWeight.Bold), + Font(R.font.pretendard_semi_bold, FontWeight.SemiBold), + Font(R.font.pretendard_medium, FontWeight.Medium), + Font(R.font.pretendard_regular, FontWeight.Normal), + Font(R.font.pretendard_light, FontWeight.Light), + Font(R.font.pretendard_extra_light, FontWeight.ExtraLight), + Font(R.font.pretendard_thin, FontWeight.Thin) +) + +internal val Typography = DayoTypography() + +@Immutable +data class DayoTypography( + // Heading + val h1: TextStyle = TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 33.sp + ), + + val h2: TextStyle = TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Medium, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 33.sp + ), + + val h3: TextStyle = TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 30.sp + ), + + val h4: TextStyle = TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 30.sp + ), + + val h5: TextStyle = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 27.sp + ), + + // Body + val b1: TextStyle = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 27.sp + ), + + val b2: TextStyle = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 27.sp + ), + + val b3: TextStyle = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 24.sp + ), + + val b4: TextStyle = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 24.sp + ), + + val b5: TextStyle = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 20.sp + ), + + val b6: TextStyle = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 20.sp + ), + + // Caption + val caption1: TextStyle = TextStyle( + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 19.5.sp + ), + + val caption2: TextStyle = TextStyle( + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 19.5.sp + ), + + val caption3: TextStyle = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 18.sp + ), + + val caption4: TextStyle = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 18.sp + ), + + val caption5: TextStyle = TextStyle( + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 15.sp + ), + + val caption6: TextStyle = TextStyle( + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + fontFamily = PretendardKrFontFamily, + letterSpacing = (-0.4).sp, + lineHeight = 15.sp + ) +) + +internal val LocalTypography = staticCompositionLocalOf { DayoTypography() } diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Button.kt b/presentation/src/main/java/daily/dayo/presentation/view/Button.kt new file mode 100644 index 00000000..f13fa2a1 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/Button.kt @@ -0,0 +1,264 @@ +package daily.dayo.presentation.view + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Email +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray5_E8EAEE +import daily.dayo.presentation.theme.PrimaryL1_8FD9B9 +import daily.dayo.presentation.theme.PrimaryL3_F2FBF7 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF + +@Composable +fun FilledButton( + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + isTonal: Boolean = false, + icon: @Composable (() -> Unit)? = null +) { + val buttonColors = if (isTonal) + ButtonDefaults.buttonColors( + containerColor = PrimaryL3_F2FBF7, + contentColor = Primary_23C882, + disabledContainerColor = Gray5_E8EAEE, + disabledContentColor = Gray4_C5CAD2 + + ) + else + ButtonDefaults.buttonColors( + containerColor = Primary_23C882, + contentColor = White_FFFFFF, + disabledContainerColor = Gray5_E8EAEE, + disabledContentColor = Gray4_C5CAD2 + ) + + Button( + onClick = { onClick() }, + modifier = modifier, + enabled = enabled, + colors = buttonColors, + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + content = { + if (icon != null) icon() + Text(text = label, style = DayoTheme.typography.b6) + } + ) +} + +@Composable +fun FilledRoundedCornerButton( + label: String, + onClick: () -> Unit, + modifier: Modifier? = null, + contentModifier: Modifier? = null, + enabled: Boolean = true, + color: ButtonColors? = null, + textStyle: TextStyle = DayoTheme.typography.b3, + radius: Int = 8, + icon: @Composable (() -> Unit)? = null, +) { + val buttonColors = color + ?: ButtonDefaults.buttonColors( + containerColor = Primary_23C882, + contentColor = White_FFFFFF, + disabledContainerColor = PrimaryL1_8FD9B9, + disabledContentColor = White_FFFFFF + ) + + Button( + onClick = { onClick() }, + colors = buttonColors, + enabled = enabled, + shape = RoundedCornerShape(radius.dp), + modifier = modifier ?: Modifier.fillMaxWidth(), + content = { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) icon() + Text( + text = label, + textAlign = TextAlign.Center, + style = textStyle, + modifier = contentModifier ?: Modifier.fillMaxWidth() + ) + } + }, + contentPadding = if (icon != null) PaddingValues(horizontal = 20.dp) else ButtonDefaults.ContentPadding + ) +} + +@Composable +fun DayoOutlinedButton( + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + icon: @Composable (() -> Unit)? = null +) { + OutlinedButton( + onClick = { onClick() }, + modifier = modifier, + colors = ButtonDefaults.outlinedButtonColors( + containerColor = White_FFFFFF, + contentColor = Gray2_767B83 + ), + border = BorderStroke(1.dp, Gray5_E8EAEE), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + content = { + if (icon != null) icon() + Text(text = label, style = DayoTheme.typography.b6) + } + ) +} + +@Composable +fun DayoTextButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + underline: Boolean = false, + textAlign: TextAlign = TextAlign.Start, + textStyle: TextStyle = DayoTheme.typography.b6.copy(color = Primary_23C882) +) { + Text( + text = text, + modifier = modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onClick() } + ), + textDecoration = if (underline) TextDecoration.Underline else null, + textAlign = textAlign, + style = textStyle + ) +} + +@Composable +fun NoRippleIconButton( + onClick: () -> Unit, + iconPainter: Painter, + iconContentDescription: String, + iconButtonModifier: Modifier = Modifier, + iconModifier: Modifier = Modifier, + iconTintColor: Color = Color.Unspecified, + interactionSource: MutableInteractionSource = MutableInteractionSource(), + additionalComponent: @Composable (() -> Unit)? = null +) { + IconButton( + onClick = onClick, + interactionSource = interactionSource, + modifier = iconButtonModifier + .defaultMinSize(1.dp, 1.dp) + .indication( + interactionSource = interactionSource, + indication = null + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painter = iconPainter, + contentDescription = iconContentDescription, + tint = iconTintColor, + modifier = iconModifier + ) + additionalComponent?.invoke() + } + } +} + +@Preview +@Composable +private fun PreviewFilledButton() { + Column { + // Filled Button + FilledButton(onClick = {}, label = "text") + FilledButton(onClick = {}, label = "text", enabled = false) + FilledButton(onClick = {}, label = "text", icon = { Icon(Icons.Filled.Add, "Add") }, isTonal = true) + } +} + +@Preview +@Composable +private fun PreviewFilledRoundedButton() { + Column { + // Rounded Corner Shape Button + FilledRoundedCornerButton(onClick = {}, label = "label") + FilledRoundedCornerButton(onClick = {}, label = "label", enabled = false) + FilledRoundedCornerButton( + onClick = {}, + label = "Email", + modifier = Modifier.size(300.dp, 44.dp), + color = ButtonDefaults.buttonColors(containerColor = Dark, contentColor = White_FFFFFF), + icon = { Icon(Icons.Filled.Email, "Email") }, + textStyle = DayoTheme.typography.b5 + ) + } +} + +@Preview +@Composable +private fun PreviewDayoOutlinedButton() { + Column { + // Outlined Button + DayoOutlinedButton(onClick = {}, label = "text") + DayoOutlinedButton(onClick = {}, label = "text", icon = { Icon(Icons.Filled.Add, "Add") }) + } +} + +@Preview(showBackground = true, backgroundColor = android.graphics.Color.WHITE.toLong()) +@Composable +private fun PreviewDayoTextButton() { + Column { + // Text Button + DayoTextButton(onClick = {}, text = "text") + DayoTextButton(onClick = {}, text = "text", underline = true) + DayoTextButton(onClick = {}, text = "text", textStyle = DayoTheme.typography.b6.copy(Gray2_767B83)) + DayoTextButton(onClick = {}, text = "text", textStyle = DayoTheme.typography.b6.copy(Gray4_C5CAD2)) + + // Underline Text Button + Row { + Text(text = "DAYO์˜ ", style = DayoTheme.typography.caption3.copy(Gray4_C5CAD2)) + DayoTextButton(onClick = {}, text = "์ด์šฉ์•ฝ๊ด€", textStyle = DayoTheme.typography.caption3.copy(Gray4_C5CAD2), underline = true) + Text(text = "์ž…๋‹ˆ๋‹ค.", style = DayoTheme.typography.caption3.copy(Gray4_C5CAD2)) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/CategoryHorizontalGroup.kt b/presentation/src/main/java/daily/dayo/presentation/view/CategoryHorizontalGroup.kt new file mode 100644 index 00000000..f4627bb8 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/CategoryHorizontalGroup.kt @@ -0,0 +1,100 @@ +package daily.dayo.presentation.view + +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.screen.home.CategoryMenu +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray7_F6F6F7 +import daily.dayo.presentation.theme.PrimaryL3_F2FBF7 +import daily.dayo.presentation.theme.Primary_23C882 + +@Composable +fun CategoryHorizontalGroup( + categoryMenus: List, + selectedCategory: CategoryMenu, + onCategorySelect: (CategoryMenu) -> Unit, + modifier: Modifier = Modifier +) { + LazyRow( + modifier = modifier.selectableGroup(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 18.dp) + ) { + categoryMenus.forEach { category -> + item { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val buttonColors = if (category == selectedCategory) + ButtonDefaults.buttonColors( + containerColor = PrimaryL3_F2FBF7, + contentColor = Primary_23C882 + ) + else + ButtonDefaults.buttonColors( + containerColor = if (isPressed) Color(0xFFE4F7ED) else Gray7_F6F6F7, + contentColor = if (isPressed) Primary_23C882 else Gray4_C5CAD2 + ) + + Button( + onClick = { onCategorySelect(category) }, + shape = RoundedCornerShape(12.dp), + contentPadding = PaddingValues(horizontal = 12.dp), + colors = buttonColors, + interactionSource = interactionSource, + modifier = Modifier + .wrapContentWidth() + .height(36.dp) + .selectable( + selected = (category == categoryMenus), + onClick = { onCategorySelect(category) }, + role = Role.RadioButton + ) + .indication(interactionSource = interactionSource, indication = null), + content = { + Text(text = category.name, style = DayoTheme.typography.b5) + } + ) + } + } + } +} + +@Preview +@Composable +private fun PreviewCategoryHorizontalGroup() { + val categoryMenus = listOf( + CategoryMenu.All, + CategoryMenu.Scheduler, + CategoryMenu.StudyPlanner, + CategoryMenu.PocketBook, + CategoryMenu.SixHoleDiary, + CategoryMenu.Digital, + CategoryMenu.ETC + ) + val selectedCategory = remember { mutableStateOf(categoryMenus[0]) } + DayoTheme { + CategoryHorizontalGroup(categoryMenus, selectedCategory.value, {}) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Checkbox.kt b/presentation/src/main/java/daily/dayo/presentation/view/Checkbox.kt new file mode 100644 index 00000000..e62d2fbd --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/Checkbox.kt @@ -0,0 +1,59 @@ +package daily.dayo.presentation.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.White_FFFFFF + +@Composable +fun DayoCheckbox( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + contentDescription: String = "checkbox" +) { + IconButton( + onClick = { onCheckedChange(!checked) }, + interactionSource = interactionSource, + modifier = modifier + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_ok_sign), + tint = if (checked) Color.Unspecified else White_FFFFFF.copy(0.4f), + modifier = modifier + .border( + width = 1.dp, + color = if (checked) Color.Transparent else White_FFFFFF, + shape = RoundedCornerShape(size = 100.dp) + ) + .background( + color = Color.Transparent, + shape = CircleShape + ), + contentDescription = contentDescription + ) + } +} + +@Preview +@Composable +private fun PreviewDayoCheckbox() { + Row { + DayoCheckbox(true, {}) + DayoCheckbox(false, {}) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Chip.kt b/presentation/src/main/java/daily/dayo/presentation/view/Chip.kt new file mode 100644 index 00000000..6983b373 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/Chip.kt @@ -0,0 +1,64 @@ +package daily.dayo.presentation.view + +import android.annotation.SuppressLint +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray5_E8EAEE +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF + +@Composable +fun Chip( + onClickChip: (() -> Unit) = {}, + text: String, + color: Color = Primary_23C882, + enabled: Boolean = true, + @SuppressLint("ModifierParameter") modifier: Modifier = Modifier +) { + AssistChip( + onClick = onClickChip, + enabled = enabled, + label = { + Text( + text = text, + color = if (enabled) color else Gray5_E8EAEE, + style = DayoTheme.typography.caption2 + ) + }, + shape = RoundedCornerShape(100.dp), + border = if (enabled) BorderStroke( + width = 1.dp, + color = color + ) else BorderStroke(width = 1.dp, color = Gray6_F0F1F3), + colors = AssistChipDefaults.assistChipColors( + containerColor = White_FFFFFF, + disabledContainerColor = White_FFFFFF + ), + modifier = modifier + ) +} + +@Preview +@Composable +fun PreviewChip() { + DayoTheme { + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Chip(text = "Text") + Chip(text = "Text", enabled = false) + Chip(text = "Text", color = Dark) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt new file mode 100644 index 00000000..b0ac12e9 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/Comment.kt @@ -0,0 +1,547 @@ +package daily.dayo.presentation.view + +import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.TextFieldDefaults.TextFieldDecorationBox +import androidx.compose.material.TextFieldDefaults.textFieldColors +import androidx.compose.material3.Icon +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.paging.compose.LazyPagingItems +import daily.dayo.domain.model.Comment +import daily.dayo.domain.model.Comments +import daily.dayo.domain.model.MentionUser +import daily.dayo.domain.model.SearchUser +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.TimeChangerUtil +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.Gray7_F6F6F7 +import daily.dayo.presentation.theme.PrimaryL1_8FD9B9 +import daily.dayo.presentation.theme.PrimaryL3_F2FBF7 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF + +@Composable +fun CommentListView( + currentMemberId: String?, + postComments: Comments, + onClickProfile: (String) -> Unit, + onClickReply: (Pair) -> Unit, + onClickDelete: (Long) -> Unit, + onClickReport: (Long) -> Unit, + modifier: Modifier = Modifier, + showEmptyIcon: Boolean = false +) { + with(postComments) { + data.let { postComments -> + if (postComments.isEmpty()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize() + .padding(top = 12.dp, bottom = 30.dp) + .then(modifier) + ) { + if (showEmptyIcon) { + Icon( + painter = painterResource(id = R.drawable.ic_comment_empty), + contentDescription = "empty", + tint = Color.Unspecified + ) + } + + Text( + text = stringResource(id = R.string.post_comment_empty), + style = DayoTheme.typography.b3.copy(Gray3_9FA5AE), + modifier = Modifier.padding(top = 12.dp, bottom = 2.dp) + ) + Text( + text = stringResource(id = R.string.post_comment_empty_description), + style = DayoTheme.typography.caption1.copy(Gray4_C5CAD2) + ) + } + } else { + Column( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxSize() + .then(modifier) + ) { + postComments.forEach { comment -> + Column( + modifier = Modifier + .padding(bottom = 8.dp) + .background(color = DayoTheme.colorScheme.background, shape = RoundedCornerShape(20.dp)) + .border(width = 1.dp, color = Gray7_F6F6F7, shape = RoundedCornerShape(20.dp)) + .padding(12.dp), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + // comment + CommentView( + parentCommentId = comment.commentId, + comment = comment, + isMine = currentMemberId == comment.memberId, + onClickProfile = onClickProfile, + onClickReply = onClickReply, + onClickDelete = onClickDelete, + onClickReport = onClickReport, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) + // reply + comment.replyList.forEach { reply -> + CommentView( + parentCommentId = comment.commentId, + comment = reply, + isMine = currentMemberId == reply.memberId, + onClickProfile = onClickProfile, + onClickReply = onClickReply, + onClickDelete = onClickDelete, + onClickReport = onClickReport, + modifier = Modifier + .background(color = Gray7_F6F6F7, shape = RoundedCornerShape(20.dp)) + .padding(12.dp) + .fillMaxWidth(0.9f) + .wrapContentHeight() + ) + } + } + } + } + } + } + } +} + +@Composable +fun CommentView( + parentCommentId: Long, + comment: Comment, + isMine: Boolean, + onClickProfile: (String) -> Unit, + onClickReply: (Pair) -> Unit, + onClickDelete: (Long) -> Unit, + onClickReport: (Long) -> Unit, + modifier: Modifier +) { + Column( + modifier = modifier, + ) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + RoundImageView( + imageUrl = "${BuildConfig.BASE_URL}/images/${comment.profileImg}", + context = LocalContext.current, + modifier = Modifier + .clip(CircleShape) + .size(36.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onClickProfile(comment.memberId) } + ), + imageDescription = "comment profile image" + ) + Column { + Row( + modifier = Modifier.padding(bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // comment nickname + Text( + text = comment.nickname, + style = DayoTheme.typography.caption3.copy(Gray1_50545B), + modifier = Modifier + .clickableSingle( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onClickProfile(comment.memberId) } + ) + ) + Spacer(Modifier.width(4.dp)) + + // comment create time + Text( + text = TimeChangerUtil.timeChange(context = LocalContext.current, time = comment.createTime), + style = DayoTheme.typography.caption6.copy(Gray4_C5CAD2) + ) + } + + // comment content + Spacer(Modifier.height(2.dp)) + Text( + text = getAnnotatedCommentContent(comment.contents, comment.mentionList), + style = DayoTheme.typography.b6.copy(Dark) + ) + Spacer(Modifier.height(4.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 4.dp) + ) { + // reply comment + Row( + modifier = Modifier.clickableSingle( + indication = ripple(bounded = false, radius = 8.dp), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onClickReply(Pair(parentCommentId, comment)) }), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_comment), + colorFilter = ColorFilter.tint(Gray3_9FA5AE), + modifier = Modifier.size(12.dp), + contentDescription = "reply comment", + ) + Spacer(Modifier.width(3.dp)) + Text( + text = "๋‹ต๊ธ€์“ฐ๊ธฐ", + style = DayoTheme.typography.caption4.copy(Gray3_9FA5AE), + ) + Spacer(Modifier.width(8.dp)) + } + + Text( + text = "โ€ข", + style = DayoTheme.typography.caption4.copy(Gray3_9FA5AE) + ) + Spacer(Modifier.width(8.dp)) + + // comment option + Text( + text = if (isMine) "์‚ญ์ œ" else "์‹ ๊ณ ", + style = DayoTheme.typography.caption4.copy(Gray3_9FA5AE), + modifier = Modifier + .clickableSingle( + indication = ripple(bounded = false, radius = 8.dp), + interactionSource = remember { MutableInteractionSource() }, + onClick = if (isMine) { + { onClickDelete(comment.commentId) } + } else { + { onClickReport(comment.commentId) } + } + ), + ) + } + } + } + } +} + +fun getAnnotatedCommentContent(content: String, mentionList: List): AnnotatedString = buildAnnotatedString { + var currentIndex = 0 + val regex = "@\\w+".toRegex() + + regex.findAll(content).forEach { matchResult -> + val start = matchResult.range.first + val end = matchResult.range.last + 1 + val matchedText = matchResult.value + + // ์ผ๋ฐ˜ ํ…์ŠคํŠธ ์ถ”๊ฐ€ + append(content.substring(currentIndex, start)) + + // ๋งค์นญ๋œ @์œ ์ €๋ช… + if (mentionList.any { it.nickname == matchedText.substring(1) }) { + withStyle(style = SpanStyle(color = Primary_23C882)) { + append(matchedText) + } + } else { + append(matchedText) + } + currentIndex = end + } + + // ๋‚จ์€ ์ผ๋ฐ˜ ํ…์ŠคํŠธ ์ถ”๊ฐ€ + if (currentIndex < content.length) { + append(content.substring(currentIndex)) + } +} + +@Composable +fun CommentMentionSearchView(userResults: LazyPagingItems, onClickFollowUser: (SearchUser) -> Unit) { + val placeholder = AppCompatResources.getDrawable(LocalContext.current, R.drawable.ic_profile_default_user_profile) + LazyColumn( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 18.dp) + ) { + items(userResults.itemCount) { index -> + userResults[index]?.let { user -> + Row( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxWidth() + .padding(vertical = 4.dp) + .clickableSingle( + indication = ripple(bounded = false, radius = 8.dp, color = Gray7_F6F6F7), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onClickFollowUser(user) } + ), + verticalAlignment = Alignment.CenterVertically + ) { + RoundImageView( + imageUrl = "${BuildConfig.BASE_URL}/images/${user.profileImg}", + context = LocalContext.current, + modifier = Modifier + .clip(CircleShape) + .size(24.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { } + ), + imageDescription = "search users profile image", + ) + Spacer(modifier = Modifier.width(12.dp)) + Text(text = user.nickname) + } + } + } + } +} + +@Composable +fun CommentReplyDescriptionView(replyCommentState: MutableState?>, onClickCancelReply: () -> Unit) { + replyCommentState.value?.let { replyComment -> + Row( + modifier = Modifier + .background(color = Color(0xFFE8EAEE)) + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 18.dp) + .wrapContentHeight(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = replyComment.second.nickname, + style = DayoTheme.typography.caption4.copy(Dark) + ) + Text( + text = "๋‹˜์—๊ฒŒ ๋‹ต๊ธ€ ๋‚จ๊ธฐ๋Š” ์ค‘", + style = DayoTheme.typography.caption4.copy(Gray1_50545B) + ) + Spacer(modifier = Modifier.weight(1f)) + DayoTextButton( + onClick = onClickCancelReply, + text = "์ทจ์†Œ", + textStyle = DayoTheme.typography.caption4.copy(Gray2_767B83) + ) + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun CommentTextField( + enabled: Boolean, + commentText: MutableState, + replyCommentState: MutableState?>, + userSearchKeyword: MutableState, + showMentionSearchView: MutableState, + focusRequester: FocusRequester, + onClickPostComment: () -> Unit +) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(Gray6_F0F1F3) + ) + Row( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 18.dp) + .padding(top = 12.dp, bottom = 16.dp), + verticalAlignment = Alignment.Bottom + ) { + val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + BasicTextField( + value = commentText.value, + onValueChange = { inputText -> + if (replyCommentState.value != null) { + val replyUsername = "@${replyCommentState.value?.second?.nickname} " + if (inputText.selection.start < replyUsername.length) { + commentText.value = commentText.value.copy( + selection = TextRange(replyUsername.length) + ) + } else { + commentText.value = inputText + } + } else { + commentText.value = inputText + } + + val cursorPos = commentText.value.selection.start + val text = inputText.text + + // ์–ธ๊ธ‰ํ•  ์‚ฌ์šฉ์ž ๊ฒ€์ƒ‰ + if (cursorPos > 0 && text.getOrNull(cursorPos - 1) == '@') { + showMentionSearchView.value = true + userSearchKeyword.value = "" + } else { + // ์ปค์„œ ์œ„์น˜๊ฐ€ @์™€ ๊ณต๋ฐฑ ์‚ฌ์ด์— ์žˆ์„ ๊ฒฝ์šฐ์—๋งŒ ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ๋ฅผ ์—…๋ฐ์ดํŠธ + val start = text.lastIndexOf('@', cursorPos - 1) + if (start >= 0) { + val end = text.indexOf(' ', start).let { if (it == -1) text.length else it } + if (cursorPos <= end) { + userSearchKeyword.value = text.substring(start + 1, cursorPos) + showMentionSearchView.value = userSearchKeyword.value.isNotEmpty() + } else { + showMentionSearchView.value = false + } + } else { + showMentionSearchView.value = false + } + } + }, + modifier = Modifier + .padding(end = 8.dp) + .heightIn(min = 36.dp) + .weight(1f) + .focusRequester(focusRequester), + textStyle = DayoTheme.typography.b6, + interactionSource = interactionSource, + cursorBrush = SolidColor(Primary_23C882), + decorationBox = @Composable { innerTextField -> + TextFieldDecorationBox( + value = commentText.value.text, + innerTextField = innerTextField, + enabled = true, + singleLine = false, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + placeholder = { Text(text = "๋Œ“๊ธ€์„ ๋‚จ๊ฒจ์ฃผ์„ธ์š”", style = DayoTheme.typography.b6.copy(Gray4_C5CAD2)) }, + shape = DayoTheme.shapes.small.copy(all = CornerSize(12.dp)), + colors = textFieldColors(backgroundColor = Gray7_F6F6F7), + contentPadding = TextFieldDefaults.textFieldWithLabelPadding(top = 8.dp, bottom = 8.dp, start = 12.dp) + ) + } + ) + + Box( + modifier = Modifier + .defaultMinSize(minWidth = 64.dp, minHeight = 36.dp) + .clip(RoundedCornerShape(12.dp)) + .background(color = if (enabled) Primary_23C882 else PrimaryL1_8FD9B9) + .clickableSingle(enabled = enabled) { onClickPostComment() } + .padding(vertical = 8.dp, horizontal = 12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.comment_button_text), + color = if (enabled) White_FFFFFF else PrimaryL3_F2FBF7, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center, + style = DayoTheme.typography.b5 + ) + } + } +} + +// Preview +@Preview +@Composable +private fun PreviewCommentView() { + CommentView( + parentCommentId = 0, + comment = Comment(0, "", "๋‹‰๋„ค์ž„", "", "๋Œ“๊ธ€", "2024-07-20T00:58:45.162925", emptyList(), emptyList()), + onClickProfile = {}, + onClickReply = {}, + onClickReport = {}, + onClickDelete = {}, + isMine = true, + modifier = Modifier + .padding(bottom = 12.dp) + .background(color = DayoTheme.colorScheme.background, shape = RoundedCornerShape(20.dp)) + .border(width = 1.dp, color = Gray7_F6F6F7, shape = RoundedCornerShape(20.dp)) + .padding(12.dp) + .fillMaxWidth() + .wrapContentHeight() + ) +} + +@Preview +@Composable +private fun PreviewCommentTextField() { + val commentText = remember { mutableStateOf(TextFieldValue("")) } + val replyCommentState = remember { mutableStateOf?>(null) } // parent comment Id, reply comment + val userSearchKeyword = remember { mutableStateOf("") } + val showMentionSearchView = remember { mutableStateOf(false) } + val commentFocusRequester = FocusRequester() + CommentTextField( + enabled = commentText.value.text.isNotBlank(), + commentText = commentText, + replyCommentState = replyCommentState, + userSearchKeyword = userSearchKeyword, + showMentionSearchView = showMentionSearchView, + focusRequester = commentFocusRequester, + onClickPostComment = { } + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/DetailPostView.kt b/presentation/src/main/java/daily/dayo/presentation/view/DetailPostView.kt new file mode 100644 index 00000000..a95193d9 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/DetailPostView.kt @@ -0,0 +1,362 @@ +package daily.dayo.presentation.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import daily.dayo.domain.model.Category +import daily.dayo.domain.model.PostDetail +import daily.dayo.domain.model.categoryKR +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.TimeChangerUtil +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray1_50545B +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Transparent_White30 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.dialog.PostReportDialog +import kotlinx.coroutines.launch +import java.text.DecimalFormat +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@Composable +fun DetailPostView( + postId: Long, + post: PostDetail, + commentCount: Int, + currentMemberId: String?, + snackBarHostState: SnackbarHostState, + onClickProfile: (String) -> Unit, + onClickPost: () -> Unit, + onPostModifyClick: (Long) -> Unit, + onPostDeleteClick: (Long) -> Unit, + onClickLikePost: () -> Unit, + onClickComment: () -> Unit, + onClickBookmark: () -> Unit, + onClickReport: (String) -> Unit, + onPostLikeUsersClick: (Long) -> Unit, + onPostHashtagClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + val imageInteractionSource = remember { MutableInteractionSource() } + var showPostOption by remember { mutableStateOf(false) } + var showDialog by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + + Column(modifier = modifier) { + // publisher info + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = 18.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // user profile image + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data("${BuildConfig.BASE_URL}/images/${post.profileImg}") + .build(), + contentDescription = "${post.nickname} profile", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(36.dp) + .clip(shape = CircleShape) + .align(Alignment.CenterVertically) + .clickableSingle( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onClickProfile(post.memberId) } + ) + ) + Spacer(modifier = Modifier.width(12.dp)) + // post info text + Column(Modifier.weight(1f)) { + Text( + text = post.nickname, + style = DayoTheme.typography.b5.copy(Dark), + modifier = Modifier.clickableSingle( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onClickProfile(post.memberId) } + ) + ) + Text( + text = categoryKR(post.category) + " ๏ฝฅ " + + post.createDateTime.let { TimeChangerUtil.timeChange(context = LocalContext.current, time = it) }, + style = DayoTheme.typography.caption3.copy(Gray3_9FA5AE) + ) + } + // post option + Box { + IconButton( + onClick = { showPostOption = true } + ) { + Icon( + modifier = Modifier.padding(8.dp), + painter = painterResource(id = R.drawable.ic_option_horizontal), + tint = Gray2_767B83, + contentDescription = "post option" + ) + } + + if (currentMemberId == post.memberId) { + MyPostDropdownMenu( + postId = postId, + expanded = showPostOption, + onDismissRequest = { showPostOption = !showPostOption }, + onPostModifyClick = onPostModifyClick, + onPostDeleteClick = onPostDeleteClick, + ) + } else { + OthersPostDropdownMenu( + expanded = showPostOption, + onDismissRequest = { showPostOption = !showPostOption }, + onPostReportClick = { + showDialog = !showDialog + showPostOption = !showPostOption + } + ) + } + } + } + + // post images + Box( + modifier = Modifier + .padding(top = 8.dp, bottom = 4.dp) + .padding(horizontal = 18.dp) + .fillMaxWidth() + .aspectRatio(1f) + ) { + post.images.let { postImages -> + val pagerState = rememberPagerState(pageCount = { postImages.size }) + HorizontalPager(state = pagerState) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data("${BuildConfig.BASE_URL}/images/${postImages[it]}") + .build(), + contentDescription = "post images", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(size = 16.dp)) + .clickableSingle( + interactionSource = imageInteractionSource, + indication = null, + onClick = onClickPost + ) + ) + } + + // indicator + if (postImages.size > 1) { + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy( + space = 4.dp, + alignment = Alignment.CenterHorizontally + ), + ) { + repeat(pagerState.pageCount) { iteration -> + val color = if (pagerState.currentPage == iteration) White_FFFFFF else Transparent_White30 + Box( + modifier = Modifier + .clip(CircleShape) + .background(color) + .width(if (pagerState.currentPage == iteration) 12.dp else 6.dp) + .height(6.dp) + ) + } + } + } + } + } + + // reaction + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = 18.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val dec = DecimalFormat("#,###") + + // like button + Image( + imageVector = ImageVector.vectorResource(id = if (post.heart) R.drawable.ic_heart_filled else R.drawable.ic_heart_outlined), + modifier = Modifier + .clickableSingle( + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = onClickLikePost + ), + contentDescription = "like", + ) + Text( + text = "${dec.format(post.heartCount)} ", + modifier = Modifier + .clickable { onPostLikeUsersClick(postId) } + .padding(8.dp), + style = DayoTheme.typography.b5, color = Gray1_50545B + ) + + // comment + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_comment), + modifier = Modifier + .clickableSingle( + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = onClickComment + ), + contentDescription = "comment", + ) + Text( + text = "${dec.format(commentCount)} ", + modifier = Modifier.padding(8.dp), + style = DayoTheme.typography.b5, + color = Gray1_50545B + ) + Spacer(modifier = Modifier.weight(1f)) + + // bookmark + Image( + imageVector = ImageVector.vectorResource(id = if (post.bookmark == true) R.drawable.ic_bookmark_checked else R.drawable.ic_bookmark_default), + modifier = Modifier + .padding(vertical = 8.dp) + .clickableSingle( + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = onClickBookmark + ), + contentDescription = "bookmark", + ) + } + + // post content + if (post.contents.isNotEmpty()) { + Text( + text = post.contents, + modifier = Modifier + .padding(top = 12.dp) + .padding(horizontal = 18.dp), + style = DayoTheme.typography.b6.copy(Dark) + ) + } + + // hashtags + if (post.hashtags.isNotEmpty()) { + HashtagHorizontalGroup( + hashtags = post.hashtags, + onPostHashtagClick = onPostHashtagClick + ) + } + } + + if (showDialog) { + PostReportDialog( + onClickCancel = { showDialog = !showDialog }, + onClickConfirm = { reason -> + onClickReport(reason) + showDialog = !showDialog + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.report_post_alert_message)) + } + } + ) + } +} + +@Preview +@Composable +private fun PreviewDetailPostView() { + DayoTheme { + DetailPostView( + postId = 0L, + post = DEFAULT_POST, + commentCount = 0, + currentMemberId = "", + snackBarHostState = SnackbarHostState(), + onClickProfile = { }, + onClickPost = { }, + onPostModifyClick = { }, + onPostDeleteClick = { }, + onClickLikePost = { }, + onClickComment = { }, + onClickBookmark = { }, + onClickReport = { }, + onPostLikeUsersClick = { }, + onPostHashtagClick = { } + ) + } +} + +val DEFAULT_POST = PostDetail( + bookmark = false, + category = Category.ALL, + contents = "", + createDateTime = LocalDateTime.now().format( + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") + ), + folderId = 0, + folderName = "", + hashtags = emptyList(), + heart = false, + heartCount = 0, + images = emptyList(), + memberId = "", + nickname = "", + profileImg = "", +) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/DropdownMenu.kt b/presentation/src/main/java/daily/dayo/presentation/view/DropdownMenu.kt new file mode 100644 index 00000000..47a0fba1 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/DropdownMenu.kt @@ -0,0 +1,195 @@ +package daily.dayo.presentation.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Red_FF4545 + +@Composable +fun MyPostDropdownMenu( + postId: Long, + expanded: Boolean, + onDismissRequest: () -> Unit, + onPostModifyClick: (Long) -> Unit, + onPostDeleteClick: (Long) -> Unit +) { + DayoTheme(shapes = DayoTheme.shapes.copy(extraSmall = RoundedCornerShape(16.dp))) { + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + modifier = Modifier.background(DayoTheme.colorScheme.background) + ) { + DropdownMenuItem( + modifier = Modifier + .padding(horizontal = 6.dp) + .clip(RoundedCornerShape(12.dp)), + text = { + Row( + modifier = Modifier.width(128.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_menu_post), + contentDescription = stringResource(R.string.post_option_mine_modify), + tint = Dark, + modifier = Modifier.padding(end = 8.dp) + ) + Text( + text = stringResource(R.string.post_option_mine_modify), + style = DayoTheme.typography.b6.copy(Dark) + ) + } + }, + onClick = { + onDismissRequest() + onPostModifyClick(postId) + } + ) + + DropdownMenuItem( + modifier = Modifier + .padding(horizontal = 6.dp) + .clip(RoundedCornerShape(12.dp)), + text = { + Row( + modifier = Modifier.width(128.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_menu_delete), + contentDescription = stringResource(R.string.post_option_mine_delete), + tint = Red_FF4545, + modifier = Modifier.padding(end = 8.dp) + ) + Text( + text = stringResource(R.string.post_option_mine_delete), + style = DayoTheme.typography.b6.copy(Red_FF4545) + ) + } + }, + onClick = { + onDismissRequest() + onPostDeleteClick(postId) + } + ) + } + } +} + +@Composable +fun OthersPostDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + onPostReportClick: () -> Unit +) { + DayoTheme(shapes = DayoTheme.shapes.copy(extraSmall = RoundedCornerShape(16.dp))) { + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + modifier = Modifier.background(DayoTheme.colorScheme.background) + ) { + DropdownMenuItem( + modifier = Modifier + .padding(horizontal = 6.dp) + .clip(RoundedCornerShape(12.dp)), + text = { + Row( + modifier = Modifier.width(128.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_menu_report), + contentDescription = stringResource(R.string.post_option_report_post), + tint = Dark, + modifier = Modifier.padding(end = 8.dp) + ) + Text( + text = stringResource(R.string.post_option_report_post), + style = DayoTheme.typography.b6.copy(Dark) + ) + } + }, + onClick = onPostReportClick + ) + } + } +} + +@Composable +fun ProfileDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + onUserReportClick: () -> Unit, + onUserBlockClick: () -> Unit, +) { + DayoTheme(shapes = DayoTheme.shapes.copy(extraSmall = RoundedCornerShape(16.dp))) { + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + modifier = Modifier.background(DayoTheme.colorScheme.background) + ) { + DropdownMenuItem( + modifier = Modifier + .padding(horizontal = 6.dp) + .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)), + text = { + Row( + modifier = Modifier.width(128.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_menu_report), + contentDescription = stringResource(R.string.other_profile_option_report_user), + tint = Dark, + modifier = Modifier.padding(end = 8.dp) + ) + Text( + text = stringResource(R.string.other_profile_option_report_user), + style = DayoTheme.typography.b6.copy(Dark) + ) + } + }, + onClick = onUserReportClick + ) + DropdownMenuItem( + modifier = Modifier + .padding(horizontal = 6.dp) + .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)), + text = { + Row( + modifier = Modifier.width(128.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_menu_block), + contentDescription = stringResource(R.string.other_profile_option_block_user), + tint = Dark, + modifier = Modifier.padding(end = 8.dp) + ) + Text( + text = stringResource(R.string.other_profile_option_block_user), + style = DayoTheme.typography.b6.copy(Dark) + ) + } + }, + onClick = onUserBlockClick + ) + } + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/view/EmojiView.kt b/presentation/src/main/java/daily/dayo/presentation/view/EmojiView.kt new file mode 100644 index 00000000..6066dcb3 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/EmojiView.kt @@ -0,0 +1,34 @@ +package daily.dayo.presentation.view + +import android.view.View +import androidx.appcompat.widget.AppCompatTextView +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color.Companion.Black +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.viewinterop.AndroidView + +@Composable +fun EmojiView( + emoji: String, + emojiSize: TextUnit, + modifier: Modifier = Modifier +) { + AndroidView( + modifier = modifier, + factory = { context -> + AppCompatTextView(context).apply { + setTextColor(Black.toArgb()) + text = emoji + textSize = emojiSize.value + textAlignment = View.TEXT_ALIGNMENT_CENTER + } + }, + update = { + it.apply { + text = emoji + } + }, + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/FeedPostView.kt b/presentation/src/main/java/daily/dayo/presentation/view/FeedPostView.kt new file mode 100644 index 00000000..527e61ec --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/FeedPostView.kt @@ -0,0 +1,462 @@ +package daily.dayo.presentation.view + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.AsyncImage +import coil.request.ImageRequest +import daily.dayo.domain.model.Category +import daily.dayo.domain.model.Post +import daily.dayo.domain.model.categoryKR +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.TimeChangerUtil +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.Transparent_White30 +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.dialog.CommentBottomSheetDialog +import daily.dayo.presentation.view.dialog.PostReportDialog +import daily.dayo.presentation.viewmodel.ReportViewModel +import kotlinx.coroutines.launch +import java.text.DecimalFormat + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FeedPostView( + post: Post, + modifier: Modifier = Modifier, + snackBarHostState: SnackbarHostState, + onClickProfile: (String) -> Unit, + onClickPost: () -> Unit, + onClickLikePost: () -> Unit, + onClickBookmark: () -> Unit, + onPostLikeUsersClick: (Long) -> Unit, + onPostHashtagClick: (String) -> Unit, + bottomSheetState: BottomSheetScaffoldState, + bottomSheetContent: (@Composable () -> Unit) -> Unit, + reportViewModel: ReportViewModel = hiltViewModel() +) { + val imageInteractionSource = remember { MutableInteractionSource() } + var showPostOption by remember { mutableStateOf(false) } + var showDialog by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + + val onClickComment: (Long) -> Unit = { postId -> + coroutineScope.launch { bottomSheetState.bottomSheetState.expand() } + bottomSheetContent { + CommentBottomSheetDialog( + postId = postId, + sheetState = bottomSheetState, + snackBarHostState = snackBarHostState, + onClickProfile = onClickProfile, + onClickClose = { coroutineScope.launch { bottomSheetState.bottomSheetState.hide() } }, + ) + } + } + + Column(modifier = modifier) { + // publisher info + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = 18.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // user profile image + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data("${BuildConfig.BASE_URL}/images/${post.userProfileImage}") + .build(), + contentDescription = "${post.nickname} + profile", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(36.dp) + .clip(shape = CircleShape) + .align(Alignment.CenterVertically) + .clickableSingle( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { post.memberId?.let { onClickProfile(it) } } + ) + ) + Spacer(modifier = Modifier.width(12.dp)) + // post info text + Column(Modifier.weight(1f)) { + Text( + text = post.nickname, + style = DayoTheme.typography.b5.copy(Dark), + modifier = Modifier.clickableSingle( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { post.memberId?.let { onClickProfile(it) } } + ) + ) + Text( + text = categoryKR(post.category ?: Category.ETC) + " ๏ฝฅ " + + post.createDateTime?.let { TimeChangerUtil.timeChange(context = LocalContext.current, time = it) }, + style = DayoTheme.typography.caption3.copy(Gray3_9FA5AE) + ) + } + // post option + Box { + IconButton( + onClick = { showPostOption = true } + ) { + Icon( + modifier = Modifier.padding(8.dp), + painter = painterResource(id = R.drawable.ic_option_horizontal), + tint = Gray2_767B83, + contentDescription = "post option" + ) + } + OthersPostDropdownMenu( + expanded = showPostOption, + onDismissRequest = { showPostOption = !showPostOption }, + onPostReportClick = { + showDialog = !showDialog + showPostOption = !showPostOption + } + ) + } + } + + // post images + Box( + modifier = Modifier + .padding(top = 8.dp, bottom = 4.dp) + .padding(horizontal = 18.dp) + .fillMaxWidth() + .aspectRatio(1f) + ) { + post.postImages?.let { postImages -> + val pagerState = rememberPagerState(pageCount = { postImages.size }) + HorizontalPager(state = pagerState) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data("${BuildConfig.BASE_URL}/images/${postImages[it]}") + .build(), + contentDescription = "post images", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(size = 16.dp)) + .clickableSingle( + interactionSource = imageInteractionSource, + indication = null, + onClick = onClickPost + ) + ) + } + + // indicator + if (postImages.size > 1) { + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy( + space = 4.dp, + alignment = Alignment.CenterHorizontally + ), + ) { + repeat(pagerState.pageCount) { iteration -> + val color = if (pagerState.currentPage == iteration) White_FFFFFF else Transparent_White30 + Box( + modifier = Modifier + .clip(CircleShape) + .background(color) + .width(if (pagerState.currentPage == iteration) 12.dp else 6.dp) + .height(6.dp) + ) + } + } + } + } + } + + // reaction + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = 18.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // like button + Image( + imageVector = ImageVector.vectorResource(id = if (post.heart) R.drawable.ic_heart_filled else R.drawable.ic_heart_outlined), + modifier = Modifier + .padding(end = 8.dp) + .padding(vertical = 8.dp) + .clickableSingle( + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = onClickLikePost + ), + contentDescription = "like", + ) + + // comment + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_comment), + modifier = Modifier + .padding(8.dp) + .clickableSingle( + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = { post.postId?.let { onClickComment(it) } } + ), + contentDescription = "comment", + ) + Spacer(modifier = Modifier.weight(1f)) + + // bookmark + Image( + imageVector = ImageVector.vectorResource(id = if (post.bookmark == true) R.drawable.ic_bookmark_checked else R.drawable.ic_bookmark_default), + modifier = Modifier + .padding(vertical = 8.dp) + .clickableSingle( + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = onClickBookmark + ), + contentDescription = "bookmark", + ) + } + + // post info + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp) + .padding(horizontal = 18.dp) + .wrapContentHeight(), + verticalAlignment = Alignment.CenterVertically + ) { + // like count + val dec = DecimalFormat("#,###") + Row(modifier = Modifier.weight(1f)) { + Text(text = stringResource(id = R.string.post_like_count_message_1), style = DayoTheme.typography.caption1.copy(Gray2_767B83)) + Text( + text = " ${dec.format(post.heartCount)} ", + style = DayoTheme.typography.caption1, + modifier = if (post.heartCount != 0) Modifier.clickableSingle { post.postId?.let { onPostLikeUsersClick(it) } } else Modifier, + color = if (post.heartCount != 0) Primary_23C882 else Gray4_C5CAD2) + Text(text = stringResource(id = R.string.post_like_count_message_2), style = DayoTheme.typography.caption1.copy(Gray2_767B83)) + } + + // comment count + Row { + Text(text = " ${dec.format(post.commentCount)} ", style = DayoTheme.typography.caption1, color = if (post.commentCount != 0) Primary_23C882 else Gray4_C5CAD2) + Text(text = stringResource(id = R.string.post_comment_count_message), style = DayoTheme.typography.caption1.copy(Gray2_767B83)) + } + } + + // post content + if (!post.contents.isNullOrEmpty()) { + SeeMoreText( + text = post.contents!!, + minimizedMaxLines = 2, + modifier = Modifier + .padding(top = 12.dp) + .padding(horizontal = 18.dp), + onClickPost = onClickPost + ) + } + + // hashtags + if (!post.hashtags.isNullOrEmpty()) { + HashtagHorizontalGroup( + hashtags = post.hashtags!!, + onPostHashtagClick = onPostHashtagClick + ) + } + } + + if (showDialog) { + PostReportDialog( + onClickCancel = { showDialog = !showDialog }, + onClickConfirm = { reason -> + post.postId?.let { postId -> + reportViewModel.requestSavePostReport(reason, postId) + showDialog = !showDialog + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.report_comment_alert_message)) + } + } + } + ) + } +} + +@Composable +fun HashtagHorizontalGroup( + hashtags: List, + onPostHashtagClick: (String) -> Unit +) { + LazyRow(contentPadding = PaddingValues(vertical = 4.dp, horizontal = 18.dp)) { + hashtags.forEach { hashtag -> + item { + Row( + modifier = Modifier + .padding(end = 8.dp) + .clickableSingle( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onPostHashtagClick(hashtag) } + ), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .padding(4.dp) + .size(20.dp) + .clip(CircleShape) + .background(Color(0xFFE4F7ED)) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_hashtag), + contentDescription = null, + modifier = Modifier.align(Alignment.Center), + tint = Primary_23C882 + ) + } + Text( + text = hashtag, + style = DayoTheme.typography.caption1.copy(Primary_23C882), + modifier = Modifier.padding(vertical = 4.dp) + ) + } + } + } + } +} + +@Composable +fun SeeMoreText( + text: String, + minimizedMaxLines: Int = 2, + onClickPost: () -> Unit, + @SuppressLint("ModifierParameter") modifier: Modifier = Modifier, +) { + var cutText by remember(text) { mutableStateOf(null) } + val textLayoutResultState = remember { mutableStateOf(null) } + val seeMoreSizeState = remember { mutableStateOf(null) } + val seeMoreOffsetState = remember { mutableStateOf(null) } + + val textLayoutResult = textLayoutResultState.value + val seeMoreSize = seeMoreSizeState.value + val seeMoreOffset = seeMoreOffsetState.value + + LaunchedEffect(text, textLayoutResult, seeMoreSize) { + val lastLineIndex = minimizedMaxLines - 1 + if (textLayoutResult != null && seeMoreSize != null + && lastLineIndex + 1 == textLayoutResult.lineCount + && textLayoutResult.isLineEllipsized(lastLineIndex) + ) { + var lastCharIndex = textLayoutResult.getLineEnd(lastLineIndex, visibleEnd = true) + 1 + var charRect: Rect + do { + lastCharIndex -= 1 + charRect = textLayoutResult.getCursorRect(lastCharIndex) + } while ( + charRect.left > textLayoutResult.size.width - seeMoreSize.width + ) + seeMoreOffsetState.value = Offset(charRect.left, charRect.bottom - seeMoreSize.height) + cutText = text.substring(startIndex = 0, endIndex = lastCharIndex - 3) + "..." + } + } + + Box(modifier) { + Text( + text = cutText ?: text, + style = DayoTheme.typography.b6.copy(Dark), + maxLines = minimizedMaxLines, + overflow = TextOverflow.Ellipsis, + onTextLayout = { textLayoutResultState.value = it }, + ) + + val density = LocalDensity.current + Text( + text = " " + stringResource(id = R.string.post_contents_more), + onTextLayout = { seeMoreSizeState.value = it.size }, + style = DayoTheme.typography.b6.copy(Gray3_9FA5AE), + modifier = Modifier + .then( + if (seeMoreOffset != null) + Modifier.offset( + x = with(density) { seeMoreOffset.x.toDp() }, + y = with(density) { seeMoreOffset.y.toDp() }, + ) + else Modifier + ) + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { onClickPost() }) + .alpha(if (seeMoreOffset != null) 1f else 0f) + ) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/view/FolderView.kt b/presentation/src/main/java/daily/dayo/presentation/view/FolderView.kt new file mode 100644 index 00000000..d49cce7e --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/FolderView.kt @@ -0,0 +1,86 @@ +package daily.dayo.presentation.view + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import daily.dayo.domain.model.Folder +import daily.dayo.domain.model.Privacy +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray5_E8EAEE +import java.text.DecimalFormat + +@Composable +fun FolderView( + folder: Folder, + onClickFolder: (Long) -> Unit, + modifier: Modifier = Modifier, +) { + val imageInteractionSource = remember { MutableInteractionSource() } + Column(modifier = modifier + .clickableSingle( + interactionSource = imageInteractionSource, + indication = null, + onClick = { folder.folderId?.let { onClickFolder(it) } } + ) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) { + // thumbnail image + RoundImageView( + context = LocalContext.current, + imageUrl = "${BuildConfig.BASE_URL}/images/${folder.thumbnailImage}", + imageDescription = folder.title, + modifier = Modifier + .border(BorderStroke(1.dp, Gray5_E8EAEE), RoundedCornerShape(8.dp)) + .matchParentSize() + ) + + // private icon + if (folder.privacy == Privacy.ONLY_ME) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_folder_private_my_page), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(10.dp), + contentDescription = "๋น„๊ณต๊ฐœ ํด๋”" + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // folder info + Column { + Text(text = folder.title, style = DayoTheme.typography.b6.copy(Dark)) + + val dec = DecimalFormat("#,###") + Text(text = "${dec.format(folder.postCount)}๊ฐœ", style = DayoTheme.typography.b6.copy(Gray3_9FA5AE)) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/HomePostView.kt b/presentation/src/main/java/daily/dayo/presentation/view/HomePostView.kt new file mode 100644 index 00000000..e30856e9 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/HomePostView.kt @@ -0,0 +1,167 @@ +package daily.dayo.presentation.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import daily.dayo.domain.model.Post +import daily.dayo.presentation.BuildConfig +import daily.dayo.presentation.R +import daily.dayo.presentation.common.extension.clickableSingle +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import java.text.DecimalFormat + +@Composable +fun HomePostView( + post: Post, + modifier: Modifier = Modifier, + isDayoPick: Boolean = false, + onClickPost: () -> Unit, + onClickLikePost: () -> Unit, + onClickProfile: () -> Unit +) { + val imageInteractionSource = remember { MutableInteractionSource() } + Column(modifier = modifier) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) { + // thumbnail image + RoundImageView( + context = LocalContext.current, + imageUrl = "${BuildConfig.BASE_URL}/images/${post.thumbnailImage}", + imageDescription = "dayo pick image", + modifier = Modifier + .matchParentSize() + .clickableSingle( + interactionSource = imageInteractionSource, + indication = null, + onClick = { onClickPost() } + ) + ) + + // dayo pick icon + if (isDayoPick) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_dayo_pick), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(end = 12.dp), + contentDescription = null + ) + } + + // like button + Image( + imageVector = ImageVector.vectorResource(id = if (post.heart) R.drawable.ic_heart_filled else R.drawable.ic_heart), + modifier = Modifier + .padding(bottom = 12.dp, end = 11.dp) + .align(Alignment.BottomEnd) + .clickableSingle( + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onClickLikePost() } + ), + contentDescription = "like Button", + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // publisher info + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .wrapContentHeight() + .clickableSingle( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onClickProfile + ) + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data("${BuildConfig.BASE_URL}/images/${post.userProfileImage}") + .build(), + contentDescription = "${post.nickname} + profile", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(16.dp) + .clip(shape = CircleShape) + .align(Alignment.CenterVertically) + ) + Text(text = post.nickname, style = DayoTheme.typography.b5.copy(Dark)) + } + + Spacer(modifier = Modifier.height(2.dp)) + + // post info + val dec = DecimalFormat("#,###") + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text(text = stringResource(id = R.string.like) + " ${dec.format(post.heartCount)}", style = DayoTheme.typography.caption3.copy(Gray3_9FA5AE)) + Text(text = stringResource(id = R.string.comment) + " ${dec.format(post.commentCount)}", style = DayoTheme.typography.caption3.copy(Gray3_9FA5AE)) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun PreviewHomePostView() { + DayoTheme { + HomePostView( + modifier = Modifier.size(156.dp, 205.dp), + post = Post( + postId = 0, + thumbnailImage = "", + memberId = "", + nickname = "nickname", + userProfileImage = "", + heartCount = 123456, + commentCount = 8100, + heart = true, + category = null, + postImages = null, + contents = null, + createDateTime = null, + folderId = null, + folderName = null, + comments = null, + hashtags = null, + bookmark = null + ), + isDayoPick = true, + onClickPost = {}, + onClickLikePost = {}, + onClickProfile = {} + ) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Loading.kt b/presentation/src/main/java/daily/dayo/presentation/view/Loading.kt new file mode 100644 index 00000000..5654cb62 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/Loading.kt @@ -0,0 +1,93 @@ +package daily.dayo.presentation.view + +import androidx.annotation.RawRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray7_F6F6F7 + +const val LOADING_TOP_Z_INDEX = Float.MAX_VALUE + +@Composable +fun Loading( + isVisible: Boolean, + @RawRes lottieFile: Int = R.raw.dayo_loading, + lottieModifier: Modifier = Modifier, + lottieWidth: Dp = 92.dp, + lottieHeight: Dp = 85.dp, + modifier: Modifier = Modifier, + message: String = stringResource(id = R.string.loading_default_message), + dimColor: Color = Dark.copy(alpha = 0.4f), + animationSpeed: Float = 1f, +) { + val interactionSource = remember { MutableInteractionSource() } + + if (isVisible) { + Box( + modifier = modifier + .fillMaxSize() + .background(dimColor) + .zIndex(LOADING_TOP_Z_INDEX) // ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ ์œ„์— ํ‘œ์‹œ + .clickable( + indication = null, + interactionSource = interactionSource + ) { + // DO NOTHING FOR BLOCKING CLICK + }, + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(lottieFile)) + val progress by animateLottieCompositionAsState( + composition = composition, + iterations = LottieConstants.IterateForever, + speed = animationSpeed + ) + LottieAnimation( + modifier = Modifier + .width(lottieWidth) + .height(lottieHeight) + .then(lottieModifier), + composition = composition, + progress = { progress }, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = message, + style = DayoTheme.typography.b3.copy(color = Gray7_F6F6F7), + textAlign = TextAlign.Center + ) + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/RoundImageView.kt b/presentation/src/main/java/daily/dayo/presentation/view/RoundImageView.kt new file mode 100644 index 00000000..26a3eaa5 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/RoundImageView.kt @@ -0,0 +1,77 @@ +package daily.dayo.presentation.view + +import android.content.Context +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import coil.size.Size +import daily.dayo.presentation.R + +@Composable +fun RoundImageView( + imageUrl: Any, + context: Context, + modifier: Modifier = Modifier, + imageDescription: String = "default image view", + imageSize: Size = Size.ORIGINAL, + roundSize: Dp = 8.dp, + @DrawableRes placeholderResId: Int? = null +) { + AsyncImage( + model = ImageRequest.Builder(context) + .crossfade(true) + .data(imageUrl) + .size(imageSize) + .build(), + contentDescription = imageDescription, + contentScale = ContentScale.Crop, + modifier = modifier.clip(RoundedCornerShape(size = roundSize)), + placeholder = if (placeholderResId != null) painterResource(id = placeholderResId) else null, + error = if (placeholderResId != null) painterResource(id = placeholderResId) else null + ) +} + +@Composable +fun BadgeRoundImageView( + imageUrl: Any, + context: Context, + contentModifier: Modifier, + modifier: Modifier = Modifier, + imageDescription: String = "default image view", + @DrawableRes placeholderResId: Int? = null, + roundSize: Dp = 8.dp, + badgeSize: Dp = 30.dp +) { + Box(modifier = modifier) { + RoundImageView( + context = context, + imageUrl = imageUrl, + imageDescription = imageDescription, + placeholderResId = placeholderResId, + roundSize = roundSize, + modifier = contentModifier + ) + + Icon( + painter = painterResource(id = R.drawable.ic_camera_button), + contentDescription = "set image", + modifier = Modifier + .align(Alignment.BottomEnd) + .size(badgeSize), + tint = Color.Unspecified + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/Switch.kt b/presentation/src/main/java/daily/dayo/presentation/view/Switch.kt new file mode 100644 index 00000000..9578b159 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/Switch.kt @@ -0,0 +1,58 @@ +package daily.dayo.presentation.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.Text +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF + +@Composable +fun ToggleButtonWithLabel( + label: String, + isToggled: Boolean, + onToggleChanged: (Boolean) -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + modifier = Modifier + .padding(18.dp) + .fillMaxWidth() + .wrapContentHeight() + ) { + Text( + text = label, + color = Gray3_9FA5AE, + style = DayoTheme.typography.b6 + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Switch( + checked = isToggled, + onCheckedChange = { onToggleChanged(it) }, + colors = SwitchDefaults.colors( + uncheckedThumbColor = White_FFFFFF, + checkedThumbColor = White_FFFFFF, + checkedTrackColor = Primary_23C882, + uncheckedTrackColor = Gray6_F0F1F3, + uncheckedBorderColor = Gray6_F0F1F3, + checkedBorderColor = Primary_23C882, + ) + ) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt b/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt new file mode 100644 index 00000000..c3fbfd17 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/TextField.kt @@ -0,0 +1,531 @@ +package daily.dayo.presentation.view + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldColors +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.R +import daily.dayo.presentation.common.TextLimitUtil +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Gray5_E8EAEE +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.Gray7_F6F6F7 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.Red_FF4545 +import kotlinx.coroutines.delay +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DayoTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(top = 0.dp, bottom = 8.dp), + label: String = "", + placeholder: String = "", + @DrawableRes trailingIconId: Int? = null, + @DrawableRes errorTrailingIconId: Int = R.drawable.ic_trailing_error, + isError: Boolean? = null, + errorMessage: String = "", + textAlign: TextAlign = TextAlign.Left, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + onTrailingIconClick: (() -> Unit) = { }, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + isEnabled: Boolean = true, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (label.isNotEmpty()) { + Text( + text = label, + style = DayoTheme.typography.caption3.copy( + color = Gray4_C5CAD2, + fontWeight = FontWeight.SemiBold + ) + ) + } + + Box(modifier = Modifier.fillMaxWidth()) { + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + textStyle = TextStyle( + textAlign = textAlign, + color = Dark, + fontStyle = DayoTheme.typography.b4.fontStyle + ), + enabled = isEnabled, + interactionSource = interactionSource, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.DecorationBox( + value = value, + visualTransformation = VisualTransformation.None, + innerTextField = innerTextField, + placeholder = { + Text( + text = placeholder, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = DayoTheme.typography.b4.copy(Gray4_C5CAD2) + ) + }, + singleLine = true, + isError = isError ?: false, + enabled = isEnabled, + interactionSource = interactionSource, + contentPadding = PaddingValues( + start = contentPadding.calculateStartPadding(LayoutDirection.Ltr), + top = contentPadding.calculateTopPadding(), + end = contentPadding.calculateEndPadding(LayoutDirection.Ltr) + 20.dp, + bottom = contentPadding.calculateBottomPadding() + ), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Primary_23C882, // ๋ฐ‘์ค„ + unfocusedIndicatorColor = Gray6_F0F1F3, + errorIndicatorColor = Red_FF4545, + unfocusedContainerColor = Color.Transparent, // ๋ฐฐ๊ฒฝ + disabledContainerColor = Color.Transparent, + errorContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedLabelColor = Color.Transparent, // ๋ผ๋ฒจ + focusedLabelColor = Gray4_C5CAD2, + errorLabelColor = Red_FF4545, + focusedPlaceholderColor = Gray5_E8EAEE, // ํžŒํŠธ + unfocusedPlaceholderColor = Gray5_E8EAEE, + disabledPlaceholderColor = Gray5_E8EAEE + ) + ) + } + ) + + Box( + modifier = Modifier.align(alignment = Alignment.CenterEnd) + ) { + if (isError != null && isError == true) { + NoRippleIconButton( + onClick = onTrailingIconClick, + iconContentDescription = "error icon", + iconPainter = painterResource(id = errorTrailingIconId), + iconButtonModifier = Modifier.size(20.dp) + ) + } else if (trailingIconId != null) { + NoRippleIconButton( + onClick = onTrailingIconClick, + iconContentDescription = "trailing icon", + iconPainter = painterResource(id = trailingIconId), + iconButtonModifier = Modifier.size(20.dp) + ) + } + } + } + + if (isError != null) { + Text( + text = if (isError) errorMessage else "", + modifier = Modifier.padding(top = 4.dp), + style = DayoTheme.typography.caption4.copy(Red_FF4545) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DayoPasswordTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(top = 0.dp, bottom = 8.dp), + label: String = "", + placeholder: String = "", + isError: Boolean? = null, + @DrawableRes errorTrailingIconId: Int = R.drawable.ic_trailing_error, + errorMessage: String = "", + textAlign: TextAlign = TextAlign.Left, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + onErrorIconClick: (() -> Unit) = { }, + keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + keyboardActions: KeyboardActions = KeyboardActions.Default, + isEnabled: Boolean = true, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + var passwordHidden by remember { mutableStateOf(true) } + + if (label.isNotEmpty()) { + Text( + text = label, + style = DayoTheme.typography.caption3.copy( + color = Gray4_C5CAD2, + fontWeight = FontWeight.SemiBold + ) + ) + } + + Box(modifier = Modifier.fillMaxWidth()) { + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + textStyle = TextStyle( + textAlign = textAlign, + color = Dark, + fontStyle = DayoTheme.typography.b4.fontStyle + ), + enabled = isEnabled, + visualTransformation = if (passwordHidden) PasswordVisualTransformation(mask = 'โ—') else VisualTransformation.None, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.DecorationBox( + value = value, + visualTransformation = VisualTransformation.None, + innerTextField = innerTextField, + placeholder = { + Text( + text = placeholder, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = DayoTheme.typography.b4.copy(Gray4_C5CAD2) + ) + }, + singleLine = true, + isError = isError ?: false, + enabled = isEnabled, + interactionSource = interactionSource, + contentPadding = PaddingValues( + start = contentPadding.calculateStartPadding(LayoutDirection.Ltr), + top = contentPadding.calculateTopPadding(), + end = contentPadding.calculateEndPadding(LayoutDirection.Ltr) + 20.dp, + bottom = contentPadding.calculateBottomPadding() + ), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Primary_23C882, // ๋ฐ‘์ค„ + unfocusedIndicatorColor = Gray6_F0F1F3, + errorIndicatorColor = Red_FF4545, + unfocusedContainerColor = Color.Transparent, // ๋ฐฐ๊ฒฝ + disabledContainerColor = Color.Transparent, + errorContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedLabelColor = Color.Transparent, // ๋ผ๋ฒจ + focusedLabelColor = Gray4_C5CAD2, + errorLabelColor = Red_FF4545, + focusedPlaceholderColor = Gray5_E8EAEE, // ํžŒํŠธ + unfocusedPlaceholderColor = Gray5_E8EAEE, + disabledPlaceholderColor = Gray5_E8EAEE + ) + ) + } + ) + + Box( + modifier = Modifier.align(alignment = Alignment.CenterEnd) + ) { + if (isError != null && isError == true) { + NoRippleIconButton( + onClick = onErrorIconClick, + iconContentDescription = "error icon", + iconPainter = painterResource(id = errorTrailingIconId), + iconButtonModifier = Modifier.size(20.dp) + ) + } else { + val trailingIconId = if (passwordHidden) R.drawable.ic_trailing_invisible else R.drawable.ic_trailing_visible + val description = if (passwordHidden) "Show password" else "Hide password" + NoRippleIconButton( + onClick = { passwordHidden = passwordHidden.not() }, + iconContentDescription = description, + iconPainter = painterResource(id = trailingIconId), + iconButtonModifier = Modifier.size(20.dp) + ) + } + } + } + + if (isError != null) { + Text( + text = if (isError) errorMessage else "", + modifier = Modifier.padding(top = 4.dp), + style = DayoTheme.typography.caption4.copy(Red_FF4545) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DayoTimerTextField( + value: String, + onValueChange: (String) -> Unit, + seconds: Int, + modifier: Modifier = Modifier, + isPaused: Boolean = false, + contentPadding: PaddingValues = PaddingValues(top = 0.dp, bottom = 8.dp), + label: String = "", + placeholder: String = "", + isError: Boolean = false, + errorMessage: String = "", + timeOutErrorMessage: String = stringResource(id = R.string.email_address_certificate_alert_message_time_fail), + onTimeOut: (() -> Unit) = { }, + textAlign: TextAlign = TextAlign.Left, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + keyboardActions: KeyboardActions = KeyboardActions.Default, + isEnabled: Boolean = true, +) { + var timeLeft by rememberSaveable { mutableIntStateOf(seconds) } + + LaunchedEffect(key1 = timeLeft, key2 = isPaused) { + while (timeLeft > 0 && !isPaused) { + delay(1000L) + timeLeft-- + } + + if (timeLeft <= 0) { + onTimeOut() + } + } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (label.isNotEmpty()) { + Text( + text = label, + style = DayoTheme.typography.caption3.copy( + color = Gray4_C5CAD2, + fontWeight = FontWeight.SemiBold + ) + ) + } + + Box(modifier = Modifier.fillMaxWidth()) { + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + textStyle = TextStyle( + textAlign = textAlign, + color = Dark, + fontStyle = DayoTheme.typography.b4.fontStyle + ), + enabled = isEnabled, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.DecorationBox( + value = value, + visualTransformation = VisualTransformation.None, + innerTextField = innerTextField, + placeholder = { + Text( + text = placeholder, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = DayoTheme.typography.b4.copy(Gray4_C5CAD2) + ) + }, + singleLine = true, + isError = isError || timeLeft == 0, + enabled = isEnabled, + interactionSource = interactionSource, + contentPadding = PaddingValues( + start = contentPadding.calculateStartPadding(LayoutDirection.Ltr), + top = contentPadding.calculateTopPadding(), + end = contentPadding.calculateEndPadding(LayoutDirection.Ltr) + 40.dp, + bottom = contentPadding.calculateBottomPadding() + ), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Primary_23C882, // ๋ฐ‘์ค„ + unfocusedIndicatorColor = Gray6_F0F1F3, + errorIndicatorColor = Red_FF4545, + unfocusedContainerColor = Color.Transparent, // ๋ฐฐ๊ฒฝ + disabledContainerColor = Color.Transparent, + errorContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedLabelColor = Color.Transparent, // ๋ผ๋ฒจ + focusedLabelColor = Gray4_C5CAD2, + errorLabelColor = Red_FF4545, + focusedPlaceholderColor = Gray5_E8EAEE, // ํžŒํŠธ + unfocusedPlaceholderColor = Gray5_E8EAEE, + disabledPlaceholderColor = Gray5_E8EAEE + ) + ) + } + ) + + Box( + modifier = Modifier.align(alignment = Alignment.CenterEnd) + ) { + Text( + text = String.format(Locale.getDefault(), "%02d", (timeLeft / 60) % 60) + + ":" + String.format(Locale.getDefault(), "%02d", timeLeft % 60), + style = DayoTheme.typography.b6.copy(Gray2_767B83) + ) + } + } + + Text( + text = if (timeLeft == 0) { + timeOutErrorMessage + } else if (isError) { + errorMessage + } else "", + modifier = Modifier.padding(top = 4.dp), + style = DayoTheme.typography.caption4.copy(Red_FF4545) + ) + } +} + +@Composable +fun CharacterLimitOutlinedTextField( + value: MutableState, + maxLength: Int, + modifier: Modifier = Modifier, + singleLine: Boolean = false, + cornerSize: Dp = 8.dp, + placeholder: String = "", + outlinedTextFieldColors: TextFieldColors? = null, +) { + OutlinedTextField( + value = value.value.text, + onValueChange = { textValue -> + value.value = value.value.copy( + text = TextLimitUtil.trimToMaxLength(textValue, maxLength) + ) + }, + placeholder = { Text(text = placeholder, style = DayoTheme.typography.b6.copy(Gray2_767B83)) }, + singleLine = singleLine, + shape = RoundedCornerShape(cornerSize), + colors = outlinedTextFieldColors + ?: OutlinedTextFieldDefaults.colors( + focusedContainerColor = Gray7_F6F6F7, + unfocusedContainerColor = Gray7_F6F6F7, + focusedBorderColor = Primary_23C882, + unfocusedBorderColor = Color.Transparent, + cursorColor = Primary_23C882 + ), + textStyle = DayoTheme.typography.b6.copy(Dark), + modifier = modifier + ) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewFilledTextField() { + Column(Modifier.fillMaxWidth()) { + // Default ์‚ฌ์šฉ ์˜ˆ์‹œ + val text = remember { mutableStateOf("") } + val isError = text.value == "error" + + DayoTextField( + value = text.value, + onValueChange = { textValue -> text.value = textValue }, + modifier = Modifier.padding(horizontal = 16.dp), + label = "label", + trailingIconId = R.drawable.ic_trailing_delete, + isError = isError, + errorMessage = "error", + placeholder = "text๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + ) + + // Password ์‚ฌ์šฉ ์˜ˆ์‹œ + val password = remember { mutableStateOf("") } + val isPasswordError = password.value == "error" + DayoPasswordTextField( + value = password.value, + onValueChange = { textValue -> password.value = textValue }, + modifier = Modifier.padding(horizontal = 16.dp), + isError = isPasswordError, + placeholder = "๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + ) + + // Timer ์‚ฌ์šฉ ์˜ˆ์‹œ + val timerText = remember { mutableStateOf("") } + val isTimerError = timerText.value == "error" + val isPaused by remember { mutableStateOf(false) } + DayoTimerTextField( + value = timerText.value, + onValueChange = { textValue -> timerText.value = textValue }, + modifier = Modifier.padding(horizontal = 16.dp), + seconds = 10, + isPaused = isPaused, + label = "์ธ์ฆ๋ฒˆํ˜ธ", + placeholder = "์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”", + isError = isTimerError, + errorMessage = "error", + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewOutlinedTextField() { + // ๊ธ€์ž์ˆ˜ ์ œํ•œ text field ์‚ฌ์šฉ ์˜ˆ์‹œ + val limitText = remember { mutableStateOf(TextFieldValue("")) } + CharacterLimitOutlinedTextField( + value = limitText, + placeholder = stringResource(id = R.string.report_post_reason_other_hint), + maxLength = 5 + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/TopNavigation.kt b/presentation/src/main/java/daily/dayo/presentation/view/TopNavigation.kt new file mode 100644 index 00000000..25ad39c0 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/TopNavigation.kt @@ -0,0 +1,96 @@ +package daily.dayo.presentation.view + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.White_FFFFFF + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopNavigation( + title: String = "", + leftIcon: @Composable () -> Unit = {}, + rightIcon: @Composable () -> Unit = {}, + titleAlignment: TopNavigationAlign = TopNavigationAlign.LEFT +) { + when (titleAlignment) { + TopNavigationAlign.LEFT -> { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = White_FFFFFF, + titleContentColor = Dark, + ), + navigationIcon = leftIcon, + actions = { rightIcon() }, + title = { + Text(text = title, maxLines = 1, style = DayoTheme.typography.h3) + } + ) + } + + TopNavigationAlign.CENTER -> { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = White_FFFFFF, + titleContentColor = Dark, + ), + navigationIcon = leftIcon, + actions = { rightIcon() }, + title = { + Text(text = title, maxLines = 1, style = DayoTheme.typography.h3) + } + ) + } + } +} + +enum class TopNavigationAlign { + LEFT, CENTER +} + +@Preview +@Composable +fun PreviewTopNavigation() { + Column { + // Default ์ •๋ ฌ ์‚ฌ์šฉ ์˜ˆ์‹œ + TopNavigation( + title = "Title", + leftIcon = { + IconButton(onClick = { /* do something */ }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = "Back" + ) + } + } + ) + + // Center ์ •๋ ฌ ์‚ฌ์šฉ ์˜ˆ์‹œ + TopNavigation( + title = "Title", + rightIcon = { + IconButton(onClick = { /* do something */ }) { + Icon( + painter = painterResource(id = R.drawable.ic_option), + contentDescription = "Option" + ) + } + }, + titleAlignment = TopNavigationAlign.CENTER + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt new file mode 100644 index 00000000..041ed2c1 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/BottomSheetDialog.kt @@ -0,0 +1,300 @@ +package daily.dayo.presentation.view.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.ImageSearch +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Surface +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.material3.rememberStandardBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray6_F0F1F3 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.theme.White_FFFFFF +import kotlinx.coroutines.launch + +// 1. ๊ธฐ๋ณธ๊ณผ Hover ์ƒํƒœ ๊ตฌ๋ถ„ +// 2. primary๊ฐ€ ์„ค์ •๋˜๋Š” ๊ฒฝ์šฐ ๊ฐ€์žฅ 1๋ฒˆ์งธ ์ƒ‰์ด ํ•ญ์ƒ prmiary color๋กœ ์„ค์ •, ์•„๋‹Œ ๊ฒฝ์šฐ ๋ชจ๋‘ ๊ฐ™์€ ์ƒ‰ +// 3. ๊ฐ€์šด๋ฐ ์ •๋ ฌ๊ณผ ์ขŒ์ธก ์ •๋ ฌ ์„ค์ • +// 4. ์ขŒ์ธก ์ •๋ ฌํ•˜๋Š” ๊ฒฝ์šฐ ์ขŒ์ธก๊ณผ ์šฐ์ธก์— ์•„์ด์ฝ˜ ์กด์žฌ ๊ฐ€๋Šฅ. + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BottomSheetDialog( + sheetState: BottomSheetScaffoldState, + buttons: List Unit>>, + leftIconButtons: List? = null, + leftIconCheckedButtons: List? = null, + isFirstButtonColored: Boolean = false, + normalColor: Color = Dark, + checkedColor: Color = Primary_23C882, + title: String = "", + titleButtonAction: () -> Unit = {}, + rightIcon: ImageVector = ImageVector.vectorResource(id = R.drawable.ic_check_mark), + checkedButtonIndex: Int = -1, + closeButtonAction: (() -> Unit)? = null +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + shape = RoundedCornerShape(12.dp, 12.dp, 0.dp, 0.dp), + color = White_FFFFFF + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + Spacer(modifier = Modifier.height(2.dp)) + if (title.isNotEmpty()) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(DayoTheme.colorScheme.background) + ) { + Text( + text = title, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(12.dp), + textAlign = TextAlign.Center, + color = Dark, + style = DayoTheme.typography.b1 + ) + androidx.compose.material3.IconButton( + onClick = titleButtonAction, + modifier = Modifier + .align(Alignment.CenterEnd) + .wrapContentSize() + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_x_sign), + modifier = Modifier + .padding(12.dp) + .wrapContentSize() + .align(Alignment.CenterEnd) + .clickable(onClick = closeButtonAction ?: { }), + contentDescription = "x sign", + ) + } + } + } + + buttons.forEachIndexed { index, button -> + val interactionSource = remember { MutableInteractionSource() } + val isPressed = interactionSource.collectIsPressedAsState().value + + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background( + if (isPressed) Gray6_F0F1F3 else White_FFFFFF, + RoundedCornerShape(12.dp, 12.dp, 0.dp, 0.dp) + ) + .padding(if (leftIconButtons == null) 16.dp else 12.dp) + .clickable( + onClick = button.second, + interactionSource = interactionSource, + indication = null + ), + horizontalArrangement = if (leftIconButtons == null) Arrangement.Center else Arrangement.SpaceBetween, + ) { + if (leftIconButtons != null && leftIconCheckedButtons != null) { + Icon( + imageVector = if (checkedButtonIndex == index) leftIconCheckedButtons[index] else leftIconButtons[index], + contentDescription = "", + modifier = Modifier.align(Alignment.CenterVertically), + tint = Color.Unspecified + ) + } + Text( + text = button.first, + modifier = Modifier.offset( + if (leftIconButtons == null) 0.dp else 8.dp, + 0.dp + ), + color = if ((isFirstButtonColored && index == 0) || (checkedButtonIndex == index)) checkedColor else normalColor, + fontSize = 16.sp, + style = DayoTheme.typography.b4 + ) + if (leftIconButtons != null) { + Spacer(modifier = Modifier.weight(1f)) + if (checkedButtonIndex == index) { + Icon( + imageVector = rightIcon, + contentDescription = "", + modifier = Modifier.align(Alignment.CenterVertically), + tint = Color.Unspecified + ) + } + } + } + if (index < buttons.size - 1 && title.isEmpty()) { + Divider( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(0.dp), + color = Gray6_F0F1F3 + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun getBottomSheetDialogState( + disableFullExpanded: Boolean = false, + skipHiddenState: Boolean = false +): BottomSheetScaffoldState { + val bottomSheetState = rememberStandardBottomSheetState( + initialValue = SheetValue.Hidden, + confirmValueChange = { + if (disableFullExpanded) { + it != SheetValue.Expanded + } else { + true + } + }, + skipHiddenState = skipHiddenState + ) + + return rememberBottomSheetScaffoldState( + bottomSheetState = bottomSheetState + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +fun PreviewMyBottomSheetDialog() { + + // BottomSheetDialog๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ Box๋ฅผ ์ด์šฉํ•ด์„œ ๊ฒน์ณ๋ณด์ผ ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค + Box(modifier = Modifier.fillMaxSize()) { + val coroutineScope = rememberCoroutineScope() + + // BottomSheetDialog๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์— ๊ฐ™์ด ์‚ฌ์šฉํ•˜๋Š” State + val bottomSheetState1 = getBottomSheetDialogState() + val bottomSheetState2 = getBottomSheetDialogState() + val bottomSheetState3 = getBottomSheetDialogState() + val bottomSheetState4 = getBottomSheetDialogState() + val bottomSheetState5 = getBottomSheetDialogState() + val bottomSheetState6 = getBottomSheetDialogState() + val bottomSheetState7 = getBottomSheetDialogState() + + Column(modifier = Modifier.fillMaxSize()) { + // ๊ตฌํ˜„ ๋ณธ๋ฌธ ๋‚ด์šฉ + + // BottomSheetDialog๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒฝ์šฐ ํด๋ฆญํ•˜๋Š” Button + Button(onClick = { coroutineScope.launch { bottomSheetState1.bottomSheetState.expand() } }) { + Text(text = "Bottom Sheet Dialog / buton 1 / non-primary") + } + Button(onClick = { coroutineScope.launch { bottomSheetState2.bottomSheetState.expand() } }) { + Text(text = "Bottom Sheet Dialog / buton 1 / primary") + } + Button(onClick = { coroutineScope.launch { bottomSheetState3.bottomSheetState.expand() } }) { + Text(text = "Bottom Sheet Dialog / buton 2 / non-primary") + } + Button(onClick = { coroutineScope.launch { bottomSheetState4.bottomSheetState.expand() } }) { + Text(text = "Bottom Sheet Dialog / buton 2 / primary") + } + Button(onClick = { coroutineScope.launch { bottomSheetState5.bottomSheetState.expand() } }) { + Text(text = "Bottom Sheet Dialog / buton 3 / non-primary") + } + Button(onClick = { coroutineScope.launch { bottomSheetState6.bottomSheetState.expand() } }) { + Text(text = "Bottom Sheet Dialog / buton 3 / primary") + } + Button(onClick = { coroutineScope.launch { bottomSheetState7.bottomSheetState.expand() } }) { + Text(text = "Bottom Sheet Dialog / Category") + } + } + + BottomSheetDialog( + sheetState = bottomSheetState1, + buttons = listOf( + Pair("text") { + // ๋ฒ„ํŠผ ํด๋ฆญ์‹œ ๋™์ž‘ ์˜ + }), + isFirstButtonColored = false, + ) + BottomSheetDialog( + sheetState = bottomSheetState2, + buttons = listOf(Pair("text") { }), + isFirstButtonColored = true + ) + + BottomSheetDialog( + sheetState = bottomSheetState3, + buttons = listOf(Pair("text") { }, Pair("text") { }), + isFirstButtonColored = false, + ) + BottomSheetDialog( + sheetState = bottomSheetState4, + buttons = listOf(Pair("text") { }, Pair("text") { }), + isFirstButtonColored = true + ) + + BottomSheetDialog( + sheetState = bottomSheetState5, + buttons = listOf(Pair("text") { }, Pair("text") { }, Pair("text") { }), + isFirstButtonColored = false, + ) + BottomSheetDialog( + sheetState = bottomSheetState6, + buttons = listOf(Pair("text") { }, Pair("text") { }, Pair("text") { }), + isFirstButtonColored = true + ) + + BottomSheetDialog( + sheetState = bottomSheetState7, + buttons = listOf(Pair("contents") { }, Pair("contents") { }, Pair("contents") { }), + title = "title", + leftIconButtons = listOf(Icons.Default.Image, Icons.Default.Image, Icons.Default.Image), + leftIconCheckedButtons = listOf(Icons.Default.ImageSearch, Icons.Default.ImageSearch, Icons.Default.ImageSearch), + checkedButtonIndex = 0, + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt new file mode 100644 index 00000000..91b30945 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/CommentBottomSheetDialog.kt @@ -0,0 +1,330 @@ +package daily.dayo.presentation.view.dialog + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetValue +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.paging.compose.collectAsLazyPagingItems +import daily.dayo.domain.model.Comment +import daily.dayo.domain.model.Comments +import daily.dayo.domain.model.SearchUser +import daily.dayo.presentation.R +import daily.dayo.presentation.common.Event +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.White_FFFFFF +import daily.dayo.presentation.view.CommentListView +import daily.dayo.presentation.view.CommentMentionSearchView +import daily.dayo.presentation.view.CommentReplyDescriptionView +import daily.dayo.presentation.view.CommentTextField +import daily.dayo.presentation.view.NoRippleIconButton +import daily.dayo.presentation.viewmodel.AccountViewModel +import daily.dayo.presentation.viewmodel.PostViewModel +import daily.dayo.presentation.viewmodel.ReportViewModel +import daily.dayo.presentation.viewmodel.SearchViewModel +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CommentBottomSheetDialog( + postId: Long, + sheetState: BottomSheetScaffoldState, + snackBarHostState: SnackbarHostState, + onClickProfile: (String) -> Unit, + onClickClose: () -> Unit, + modifier: Modifier = Modifier, + postViewModel: PostViewModel = hiltViewModel(), + accountViewModel: AccountViewModel = hiltViewModel(), + searchViewModel: SearchViewModel = hiltViewModel(), + reportViewModel: ReportViewModel = hiltViewModel() +) { + val currentMemberId = accountViewModel.getCurrentUserInfo().memberId + val commentText = remember { mutableStateOf(TextFieldValue("")) } + val showMentionSearchView = remember { mutableStateOf(false) } + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val commentFocusRequester = FocusRequester() + val keyboardController = LocalSoftwareKeyboardController.current + val onClickCommentProfile: (String) -> Unit = { memberId -> + onClickClose() + onClickProfile(memberId) + } + + // show comments + val postComments = postViewModel.postComments.observeAsState() + LaunchedEffect(postId) { + postViewModel.requestPostComment(postId) + } + + // comment option + val onClickDelete: (Long) -> Unit = { commentId -> + postViewModel.requestDeletePostComment(commentId) + } + val postCommentDeleteSuccess by postViewModel.postCommentDeleteSuccess.observeAsState(Event(false)) + if (postCommentDeleteSuccess.getContentIfNotHandled() == true) { + postViewModel.requestPostComment(postId) + SideEffect { + coroutineScope.launch { + snackBarHostState.showSnackbar("๋Œ“๊ธ€์ด ์‚ญ์ œ๋˜์—ˆ์–ด์š”.") + } + } + } + var showReportDialog by remember { mutableStateOf(false) } + var reportCommentId by remember { mutableStateOf(null) } + val onClickReport: (Long) -> Unit = { commentId -> + reportCommentId = commentId + showReportDialog = true + } + + // search follow user + val userResults = searchViewModel.searchFollowUserList.collectAsLazyPagingItems() + val userSearchKeyword = remember { mutableStateOf("") } + val mentionedMemberIds = remember { mutableStateListOf() } + LaunchedEffect(userSearchKeyword.value) { + searchViewModel.searchFollowUser(userSearchKeyword.value) + } + val onClickFollowUser: (SearchUser) -> Unit = { mentionUser -> + with(commentText.value) { + val cursorPos = selection.start + val start = text.lastIndexOf('@', cursorPos - 1) + val end = text.indexOf(' ', start).let { if (it == -1) text.length else it } + val newText = text.replaceRange(start, end, "@${mentionUser.nickname} ") + commentText.value = TextFieldValue( + text = newText, + selection = TextRange(start + mentionUser.nickname.length + 2) + ) + userSearchKeyword.value = "" + mentionedMemberIds.add(mentionUser) + showMentionSearchView.value = false + } + } + + // create comment + val replyCommentState = remember { mutableStateOf?>(null) } // parent comment Id, reply comment + val onClickPostComment: () -> Unit = { + if (replyCommentState.value == null) { + if (commentText.value.text.isNotBlank()) { + postViewModel.requestCreatePostComment(commentText.value.text, postId, mentionedMemberIds) + } + } else { + postViewModel.requestCreatePostCommentReply(replyCommentState.value!!, commentText.value.text, postId, mentionedMemberIds) + } + } + val onClickReply: (Pair?) -> Unit = { reply -> + // set reply comment state + replyCommentState.value = reply + + // show mention user name + val replyUsername = "@${replyCommentState.value?.second?.nickname} " + commentText.value = TextFieldValue(text = replyUsername, selection = TextRange(replyUsername.length)) + commentFocusRequester.requestFocus() + } + val commentEnabled = if (replyCommentState.value == null) { + commentText.value.text.isNotBlank() + } else { + val replyUsername = "@${replyCommentState.value?.second?.nickname} " + commentText.value.text.drop(replyUsername.length).isNotBlank() + } + + // clear comment + val clearComment = { + commentText.value = TextFieldValue("") + replyCommentState.value = null + mentionedMemberIds.clear() + } + val onClickCancelReply: () -> Unit = { + clearComment() + } + val postCommentCreateSuccess by postViewModel.postCommentCreateSuccess.observeAsState(Event(false)) + if (postCommentCreateSuccess.getContentIfNotHandled() == true) { + clearComment() + keyboardController?.hide() + postViewModel.requestPostComment(postId) + } + + BackHandler(enabled = sheetState.bottomSheetState.isVisible) { + coroutineScope.launch { + clearComment() + sheetState.bottomSheetState.hide() + } + } + + LaunchedEffect(sheetState.bottomSheetState.currentValue) { + if (sheetState.bottomSheetState.currentValue == SheetValue.Hidden) { + keyboardController?.hide() + } + } + + Surface( + modifier = modifier, + shape = RoundedCornerShape(12.dp, 12.dp, 0.dp, 0.dp), + color = White_FFFFFF + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 65.dp) + .wrapContentHeight(), + ) { + CommentBottomSheetDialogTitle(clearComment, onClickClose) + CommentBottomSheetDialogContent( + currentMemberId = currentMemberId, + postComments = when (postComments.value?.status) { + Status.SUCCESS -> postComments.value?.data ?: DEFAULT_COMMENTS + else -> DEFAULT_COMMENTS + }, + onClickCommentProfile = onClickCommentProfile, + onClickReply = onClickReply, + onClickDelete = onClickDelete, + onClickReport = onClickReport + ) + } + + Column(modifier = Modifier.align(Alignment.BottomCenter)) { + if (showMentionSearchView.value) CommentMentionSearchView(userResults, onClickFollowUser) + if (replyCommentState.value != null) CommentReplyDescriptionView(replyCommentState, onClickCancelReply) + CommentTextField( + enabled = commentEnabled, + commentText = commentText, + replyCommentState = replyCommentState, + userSearchKeyword = userSearchKeyword, + showMentionSearchView = showMentionSearchView, + focusRequester = commentFocusRequester, + onClickPostComment = onClickPostComment + ) + } + } + + reportCommentId?.let { commentId -> + if (showReportDialog) { + CommentReportDialog( + onClickCancel = { showReportDialog = !showReportDialog }, + onClickConfirm = { reason -> + reportViewModel.requestSaveCommentReport(reason, commentId) + showReportDialog = !showReportDialog + coroutineScope.launch { + snackBarHostState.showSnackbar(context.getString(R.string.comment_report_message)) + } + } + ) + } + } + } +} + +@Composable +private fun CommentBottomSheetDialogTitle(clearComment: () -> Unit, onClickClose: () -> Unit) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(DayoTheme.colorScheme.background) + ) { + Text( + text = stringResource(id = R.string.comment), + modifier = Modifier.align(Alignment.Center), + textAlign = TextAlign.Center, + style = DayoTheme.typography.b1.copy(color = Dark, fontWeight = FontWeight.SemiBold) + ) + + NoRippleIconButton( + onClick = { + clearComment() + onClickClose() + }, + iconContentDescription = "close", + iconPainter = painterResource(id = R.drawable.ic_x_sign), + iconButtonModifier = Modifier.align(Alignment.CenterEnd) + ) + } +} + +@Composable +private fun CommentBottomSheetDialogContent( + currentMemberId: String?, + postComments: Comments, + onClickCommentProfile: (String) -> Unit, + onClickReply: (Pair) -> Unit, + onClickDelete: (Long) -> Unit, + onClickReport: (Long) -> Unit +) { + LazyColumn( + modifier = Modifier + .background(DayoTheme.colorScheme.background) + .fillMaxWidth() + .fillMaxHeight(0.8f) + ) { + item { + CommentListView( + currentMemberId = currentMemberId, + postComments = postComments, + onClickProfile = onClickCommentProfile, + onClickReply = onClickReply, + onClickDelete = onClickDelete, + onClickReport = onClickReport, + modifier = Modifier.padding(horizontal = 18.dp), + showEmptyIcon = true + ) + } + } +} + +// Preview +@Preview +@Composable +private fun PreviewCommentBottomSheetDialogTitle() { + CommentBottomSheetDialogTitle({}, {}) +} + +@Preview +@Composable +private fun PreviewCommentBottomSheetDialogContent() { + CommentBottomSheetDialogContent("", DEFAULT_COMMENTS, {}, {}, {}, {}) +} + +val DEFAULT_COMMENTS = Comments( + count = 0, + data = emptyList() +) diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt new file mode 100644 index 00000000..55981649 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ConfirmDialog.kt @@ -0,0 +1,141 @@ +package daily.dayo.presentation.view.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material3.Divider +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray7_F6F6F7 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.view.DayoTextButton + +@Composable +fun ConfirmDialog( + title: String, + description: String, + onClickConfirm: () -> Unit, // onClickConfirm is required + modifier: Modifier = Modifier, + onClickCancel: (() -> Unit)? = {}, + onClickConfirmText: String = stringResource(id = R.string.confirm), + onClickCancelText: String = stringResource(id = R.string.cancel), +) { + Dialog( + onDismissRequest = onClickCancel ?: onClickConfirm, + ) { + Surface( + modifier = modifier + .background( + DayoTheme.colorScheme.background, + RoundedCornerShape(10.dp) + ) + .clip(RoundedCornerShape(10.dp)) + ) { + Column { + DialogHeader(title, description) + Divider( + modifier = Modifier.fillMaxWidth(), + thickness = 1.dp, + color = Gray7_F6F6F7 + ) + DialogActionButton( + onClickConfirmText = onClickConfirmText, + onClickConfirm = onClickConfirm, + onClickCancelText = onClickCancelText, + onClickCancel = onClickCancel + ) + } + } + } +} + +@Composable +private fun DialogHeader(title: String, description: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 28.dp, bottom = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (title.isNotBlank()) { + Text( + text = title, + color = Dark, + textAlign = TextAlign.Center, + style = DayoTheme.typography.b3 + ) + } + + if (description.isNotBlank()) { + Text( + text = description, + color = Gray2_767B83, + textAlign = TextAlign.Center, + style = DayoTheme.typography.caption4 + ) + } + } +} + +@Composable +private fun DialogActionButton( + onClickConfirm: () -> Unit, + onClickCancel: (() -> Unit)? = {}, + onClickConfirmText: String = stringResource(id = R.string.confirm), + onClickCancelText: String = stringResource(id = R.string.cancel), +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (onClickCancel != null) { + DayoTextButton( + text = onClickCancelText, + onClick = onClickCancel, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + textStyle = DayoTheme.typography.b5.copy(Gray3_9FA5AE) + ) + } + + DayoTextButton( + text = onClickConfirmText, + onClick = onClickConfirm, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + textStyle = DayoTheme.typography.b5.copy(Primary_23C882) + ) + } +} + +@Preview +@Composable +private fun PreviewConfirmDialog() { + DayoTheme { + ConfirmDialog("title", "description", + onClickConfirm = {}, + onClickCancel = {} + ) + } +} diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/NetworkErrorDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/NetworkErrorDialog.kt new file mode 100644 index 00000000..360cd2f5 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/NetworkErrorDialog.kt @@ -0,0 +1,24 @@ +package daily.dayo.presentation.view.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import daily.dayo.presentation.R + +@Composable +fun NetworkErrorDialog( + modifier: Modifier = Modifier, + title: String = stringResource(id = R.string.network_error_dialog_default_message), + description: String = "", + onClickRetry: () -> Unit = {}, + onClickRetryText: String = stringResource(id = R.string.re_try), +) { + ConfirmDialog( + title = title, + description = description, + onClickConfirm = onClickRetry, + modifier = modifier, + onClickCancel = null, + onClickConfirmText = onClickRetryText + ) +} diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/ProfileImageBottomSheetDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ProfileImageBottomSheetDialog.kt new file mode 100644 index 00000000..37877d63 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ProfileImageBottomSheetDialog.kt @@ -0,0 +1,29 @@ +package daily.dayo.presentation.view.dialog + +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import daily.dayo.presentation.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProfileImageBottomSheetDialog( + bottomSheetState: BottomSheetScaffoldState, + onClickProfileSelect: () -> Unit, + onClickProfileCapture: () -> Unit, + onClickProfileReset: () -> Unit, +) { + BottomSheetDialog( + sheetState = bottomSheetState, + buttons = listOf( + Pair(stringResource(id = R.string.image_option_gallery)) { + onClickProfileSelect() + }, Pair(stringResource(id = R.string.image_option_camera)) { + onClickProfileCapture() + }, Pair(stringResource(id = R.string.sign_up_email_set_profile_image_reset)) { + onClickProfileReset() + }), + isFirstButtonColored = true + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/RadioButtonDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/RadioButtonDialog.kt new file mode 100644 index 00000000..77fe7904 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/RadioButtonDialog.kt @@ -0,0 +1,290 @@ +package daily.dayo.presentation.view.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.Dark +import daily.dayo.presentation.theme.DayoTheme +import daily.dayo.presentation.theme.Gray2_767B83 +import daily.dayo.presentation.theme.Gray3_9FA5AE +import daily.dayo.presentation.theme.Gray4_C5CAD2 +import daily.dayo.presentation.theme.Primary_23C882 +import daily.dayo.presentation.view.CharacterLimitOutlinedTextField +import daily.dayo.presentation.view.DayoTextButton +import kotlinx.coroutines.launch + +@Composable +fun RadioButtonDialog( + title: String, + description: String, + radioItems: Array, + onClickCancel: () -> Unit, + onClickConfirm: (String) -> Unit, + modifier: Modifier = Modifier, + lastInputEnabled: Boolean = false, // ๋งˆ์ง€๋ง‰ ์•„์ดํ…œ ์„ ํƒ ์‹œ edit text ์ž…๋ ฅ์ฐฝ ์—ฌ๋ถ€ + lastTextPlaceholder: String = "", + lastTextMaxLength: Int = 100 +) { + val selectedIndex = remember { mutableStateOf(null) } + val lastTextValue = remember { mutableStateOf(TextFieldValue("")) } + + Dialog( + onDismissRequest = onClickCancel + ) { + Surface(modifier = modifier.padding(vertical = 24.dp)) { + Column( + modifier = Modifier.background(DayoTheme.colorScheme.background) + ) { + DialogHeader(title, description) + + Box(modifier = Modifier.weight(1f)) { + DialogRadioButtons( + radioItems, + selectedIndex, + lastInputEnabled, + lastTextValue, + lastTextPlaceholder, + lastTextMaxLength + ) + } + + DialogActionButton( + onClickCancel, + selectedIndex, + radioItems, + lastInputEnabled, + lastTextValue, + onClickConfirm + ) + } + } + } +} + +@Composable +private fun DialogHeader(title: String, description: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) { + Text( + text = title, + style = DayoTheme.typography.b1.copy( + color = Dark, + fontWeight = FontWeight.SemiBold + ), + modifier = Modifier.padding(bottom = 8.dp) + ) + Text( + text = description, + style = DayoTheme.typography.caption1.copy( + color = Gray3_9FA5AE + ), + modifier = Modifier.padding(bottom = 20.dp) + ) + } +} + +@Composable +private fun DialogRadioButtons( + radioItems: Array, + selectedIndex: MutableState, + lastInputEnabled: Boolean, + lastTextValue: MutableState, + lastTextPlaceholder: String, + lastTextMaxLength: Int +) { + val coroutineScope = rememberCoroutineScope() + val scrollState = rememberLazyListState() + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + val isTextFieldVisible = remember { mutableStateOf(false) } + + LazyColumn(state = scrollState) { + radioItems.forEachIndexed { index, text -> + item { + Row( + Modifier + .fillMaxWidth() + .selectable( + selected = selectedIndex.value != null && selectedIndex.value == index, + onClick = { + selectedIndex.value = index + if (lastInputEnabled && selectedIndex.value == radioItems.lastIndex) { + isTextFieldVisible.value = true + coroutineScope.launch { + scrollState.animateScrollToItem(index + 1) + } + } else { + isTextFieldVisible.value = false + focusManager.clearFocus() + } + }, + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) + .padding(start = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedIndex.value != null && selectedIndex.value == index, + onClick = { + selectedIndex.value = index + if (lastInputEnabled && selectedIndex.value == radioItems.lastIndex) { + isTextFieldVisible.value = true + coroutineScope.launch { + scrollState.animateScrollToItem(index + 1) + } + } else { + isTextFieldVisible.value = false + focusManager.clearFocus() + } + }, + colors = RadioButtonDefaults.colors( + selectedColor = Primary_23C882, + unselectedColor = Gray3_9FA5AE + ) + ) + Text( + text = text, + style = DayoTheme.typography.b4.copy(color = Color(0xFF50545B)), + modifier = Modifier.padding(vertical = 12.dp) + ) + } + } + } + + item { + if (isTextFieldVisible.value) { + SideEffect { + focusRequester.requestFocus() + } + + CharacterLimitOutlinedTextField( + value = lastTextValue, + placeholder = lastTextPlaceholder, + maxLength = lastTextMaxLength, + modifier = Modifier + .padding(horizontal = 18.dp) + .height(144.dp) + .focusRequester(focusRequester) + ) + } + } + } +} + +@Composable +private fun DialogActionButton( + onClickCancel: () -> Unit, + selectedIndex: MutableState, + radioItems: Array, + lastInputEnabled: Boolean, + lastTextValue: MutableState, + onClickConfirm: (String) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(top = 16.dp), + horizontalArrangement = Arrangement.End + ) { + DayoTextButton( + onClick = onClickCancel, + text = stringResource(id = R.string.cancel), + textStyle = DayoTheme.typography.b6.copy( + color = Gray2_767B83 + ), + modifier = Modifier.padding(vertical = 10.dp, horizontal = 12.dp) + ) + + val canSubmit = selectedIndex.value != null && when { + !lastInputEnabled -> true // ๊ธฐํƒ€ ์‚ฌ์œ ๋ฅผ ์ž…๋ ฅํ•˜์ง€ ์•Š์•„๋„ ๋˜๋Š” ๊ฒฝ์šฐ + selectedIndex.value == radioItems.lastIndex -> lastTextValue.value.text.isNotBlank() // ๊ธฐํƒ€ ์‚ฌ์œ  ํ•„์ˆ˜ ์ž…๋ ฅ + else -> true // ๊ธฐํƒ€ ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + } + + DayoTextButton( + onClick = { + selectedIndex.value?.let { index -> + val item = if (index == radioItems.lastIndex && lastInputEnabled) { + lastTextValue.value.text.ifBlank { return@let } + } else { + radioItems[index] + } + onClickConfirm(item) + } + }, + text = stringResource(id = R.string.submit), + textStyle = DayoTheme.typography.b6.copy( + color = if (canSubmit) Primary_23C882 else Gray4_C5CAD2 + ), + modifier = Modifier.padding(vertical = 10.dp, horizontal = 12.dp) + ) + } +} + +@Preview +@Composable +private fun PreviewRadioButtonDialog() { + var showDialog by remember { mutableStateOf(false) } + val commentPostReasons = stringArrayResource(id = R.array.report_post_reasons) + + DayoTheme { + RadioButtonDialog( + title = stringResource(id = R.string.report_post_title), + description = stringResource(id = R.string.report_post_description), + radioItems = commentPostReasons, + lastInputEnabled = true, + lastTextPlaceholder = stringResource(id = R.string.report_post_reason_other_hint), + lastTextMaxLength = 100, + onClickCancel = { showDialog = !showDialog }, + onClickConfirm = {}, + modifier = Modifier + .height(400.dp) + .imePadding() + .clip(RoundedCornerShape(28.dp)) + .background(DayoTheme.colorScheme.background) + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/view/dialog/ReportDialog.kt b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ReportDialog.kt new file mode 100644 index 00000000..870766f0 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/view/dialog/ReportDialog.kt @@ -0,0 +1,75 @@ +package daily.dayo.presentation.view.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import daily.dayo.presentation.R +import daily.dayo.presentation.theme.DayoTheme + +@Composable +fun CommentReportDialog(onClickCancel: () -> Unit, onClickConfirm: (String) -> Unit) { + val reportCommentReasons = stringArrayResource(id = R.array.report_comment_reasons) + + RadioButtonDialog( + title = stringResource(id = R.string.report_comment_title), + description = stringResource(id = R.string.report_comment_description), + radioItems = reportCommentReasons, + onClickCancel = onClickCancel, + onClickConfirm = onClickConfirm, + modifier = Modifier + .height(400.dp) + .imePadding() + .clip(RoundedCornerShape(28.dp)) + .background(DayoTheme.colorScheme.background) + ) +} + +@Composable +fun PostReportDialog(onClickCancel: () -> Unit, onClickConfirm: (String) -> Unit) { + val reportPostReasons = stringArrayResource(id = R.array.report_post_reasons) + + RadioButtonDialog( + title = stringResource(id = R.string.report_post_title), + description = stringResource(id = R.string.report_post_description), + radioItems = reportPostReasons, + lastInputEnabled = true, + lastTextPlaceholder = stringResource(id = R.string.report_post_reason_other_hint), + lastTextMaxLength = 100, + onClickCancel = onClickCancel, + onClickConfirm = onClickConfirm, + modifier = Modifier + .height(400.dp) + .imePadding() + .clip(RoundedCornerShape(28.dp)) + .background(DayoTheme.colorScheme.background) + ) +} + +@Composable +fun UserReportDialog(onClickCancel: () -> Unit, onClickConfirm: (String) -> Unit) { + val reportPostReasons = stringArrayResource(id = R.array.report_user_reasons) + + RadioButtonDialog( + title = stringResource(id = R.string.report_user_title), + description = stringResource(id = R.string.report_user_description), + radioItems = reportPostReasons, + lastInputEnabled = true, + lastTextPlaceholder = stringResource(id = R.string.report_user_reason_other_hint), + lastTextMaxLength = 100, + onClickCancel = onClickCancel, + onClickConfirm = onClickConfirm, + modifier = Modifier + .height(400.dp) + .imePadding() + .clip(RoundedCornerShape(28.dp)) + .background(DayoTheme.colorScheme.background) + ) +} + diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/AccountViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/AccountViewModel.kt index edd2c26c..3338a64a 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/AccountViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/AccountViewModel.kt @@ -1,5 +1,6 @@ package daily.dayo.presentation.viewmodel +import android.graphics.Bitmap import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -9,15 +10,22 @@ import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.usecase.member.* import daily.dayo.presentation.service.firebase.FirebaseMessagingService import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.presentation.common.Status +import daily.dayo.domain.model.WithdrawalReason +import daily.dayo.presentation.common.image.ImageResizeUtil.cropCenterBitmap +import daily.dayo.presentation.common.toFile +import daily.dayo.presentation.screen.account.model.CheckOAuthEmailStatus +import daily.dayo.presentation.screen.account.model.EmailExistenceStatus import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import java.io.File import javax.inject.Inject @HiltViewModel class AccountViewModel @Inject constructor( - private val requestLoginKakaoUseCase: RequestLoginKakaoUseCase, - private val requestLoginEmailUseCase: RequestLoginEmailUseCase, + private val requestSignInKakaoUseCase: RequestSignInKakaoUseCase, + private val requestSignInEmailUseCase: RequestSignInEmailUseCase, private val requestRefreshTokenUseCase: RequestRefreshTokenUseCase, private val requestMemberInfoUseCase: RequestMemberInfoUseCase, private val requestSignUpEmailUseCase: RequestSignUpEmailUseCase, @@ -26,9 +34,12 @@ class AccountViewModel @Inject constructor( private val requestCertificateEmailUseCase: RequestCertificateEmailUseCase, private val requestDeviceTokenUseCase: RequestDeviceTokenUseCase, private val requestResignUseCase: RequestResignUseCase, - private val requestLogoutUseCase: RequestLogoutUseCase, + private val requestResignGuideImageUseCase: RequestResignGuideImageUseCase, + private val requestResignGuideWordsUseCase: RequestResignGuideWordsUseCase, + private val requestSignOutUseCase: RequestSignOutUseCase, private val requestCheckEmailUseCase: RequestCheckEmailUseCase, - private val requestCheckEmailAuthUseCase: RequestCheckEmailAuthUseCase, + private val requestCheckOAuthEmailUseCase: RequestCheckOAuthEmailUseCase, + private val requestCertificateEmailPasswordResetUseCase: RequestCertificateEmailPasswordResetUseCase, private val requestCheckCurrentPasswordUseCase: RequestCheckCurrentPasswordUseCase, private val requestChangePasswordUseCase: RequestChangePasswordUseCase, private val requestSettingChangePasswordUseCase: RequestSettingChangePasswordUseCase, @@ -37,44 +48,62 @@ class AccountViewModel @Inject constructor( private val requestSaveCurrentUserAccessTokenUseCase: RequestSaveCurrentUserAccessTokenUseCase, private val requestCurrentUserNotiDevicePermitUseCase: RequestCurrentUserNotiDevicePermitUseCase, private val requestCurrentUserNotiNoticePermitUseCase: RequestCurrentUserNotiNoticePermitUseCase, - private val requestClearCurrentUserUseCase: RequestClearCurrentUserUseCase + private val requestClearCurrentUserUseCase: RequestClearCurrentUserUseCase, ) : ViewModel() { + companion object { + const val EMAIL_CERTIFICATE_AUTH_CODE_INITIAL = Int.MIN_VALUE + 10 + const val SIGN_UP_EMAIL_CERTIFICATE_AUTH_CODE_FAIL = Int.MIN_VALUE + 20 + const val RESET_PASSWORD_EMAIL_CERTIFICATE_AUTH_CODE_FAIL = Int.MIN_VALUE + 30 + } + + private val _signupSuccess = MutableStateFlow(null) + val signupSuccess: StateFlow get() = _signupSuccess + + private val _signInSuccess: MutableStateFlow = MutableStateFlow(null) + val signInSuccess: StateFlow get() = _signInSuccess + + private val _autoSignInSuccess = MutableLiveData>() + val autoSignInSuccess: LiveData> get() = _autoSignInSuccess - private val _signupSuccess = MutableLiveData>() - val signupSuccess: LiveData> get() = _signupSuccess + private val _isEmailDuplicate = MutableStateFlow(Status.LOADING) + val isEmailDuplicate: StateFlow get() = _isEmailDuplicate - private val _loginSuccess = MutableLiveData>() - val loginSuccess: LiveData> get() = _loginSuccess + private val _isNicknameDuplicate = MutableStateFlow(false) + val isNicknameDuplicate: StateFlow get() = _isNicknameDuplicate - private val _memberInfoSuccess = MutableLiveData() - val memberInfoSuccess get() = _memberInfoSuccess + private val _certificateEmailAuthCode = + MutableStateFlow(EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString()) + val certificateEmailAuthCode: StateFlow get() = _certificateEmailAuthCode - private val _isEmailDuplicate = MutableLiveData() - val isEmailDuplicate: LiveData get() = _isEmailDuplicate + private val _withdrawSuccess = MutableStateFlow(null) + val withdrawSuccess: StateFlow get() = _withdrawSuccess - private val _isNicknameDuplicate = MutableLiveData() - val isNicknameDuplicate: LiveData get() = _isNicknameDuplicate + private val _signOutSuccess = MutableStateFlow(null) + val signOutSuccess: StateFlow get() = _signOutSuccess - private val _isCertificateEmailSend = MutableLiveData() - val isCertificateEmailSend: LiveData get() = _isCertificateEmailSend + private val _checkEmailSuccess = MutableStateFlow(EmailExistenceStatus.IDLE) + val checkEmailSuccess: StateFlow get() = _checkEmailSuccess - private val _certificateEmailAuthCode = MutableLiveData() - val certificateEmailAuthCode: MutableLiveData get() = _certificateEmailAuthCode + private val _checkOAuthEmailSuccess = MutableStateFlow(CheckOAuthEmailStatus.LOADING) + val checkOAuthEmailSuccess: StateFlow get() = _checkOAuthEmailSuccess - private val _withdrawSuccess = MutableLiveData>() - val withdrawSuccess: LiveData> get() = _withdrawSuccess + private val _resetPasswordSuccess = MutableStateFlow(null) + val resetPasswordSuccess: StateFlow get() = _resetPasswordSuccess - private val _logoutSuccess = MutableLiveData>() - val logoutSuccess: LiveData> get() = _logoutSuccess + private val _checkCurrentPasswordSuccess = MutableStateFlow(null) + val checkCurrentPasswordSuccess:StateFlow get() = _checkCurrentPasswordSuccess - private val _checkEmailSuccess = MutableLiveData() - val checkEmailSuccess get() = _checkEmailSuccess + private val _changePasswordSuccess = MutableStateFlow(null) + val changePasswordSuccess: StateFlow get() = _changePasswordSuccess - private val _checkCurrentPasswordSuccess = MutableLiveData>() - val checkCurrentPasswordSuccess get() = _checkCurrentPasswordSuccess + private val _recordGuideWords = MutableStateFlow>(emptyList()) + val recordGuideWords: StateFlow> get() = _recordGuideWords - private val _changePasswordSuccess = MutableLiveData() - val changePasswordSuccess get() = _changePasswordSuccess + private val _followGuideWords = MutableStateFlow>(emptyList()) + val followGuideWords: StateFlow> get() = _followGuideWords + + private val _guideImages = MutableStateFlow>(emptyMap()) + val guideImages: StateFlow> get() = _guideImages private val _isErrorExceptionOccurred = MutableLiveData>() val isErrorExceptionOccurred get() = _isErrorExceptionOccurred @@ -85,50 +114,61 @@ class AccountViewModel @Inject constructor( private val _isLoginFailByUncorrected = MutableLiveData>() val isLoginFailByUncorrected get() = _isLoginFailByUncorrected - fun requestLoginKakao(accessToken: String) = viewModelScope.launch { - requestLoginKakaoUseCase(accessToken = accessToken).let { ApiResponse -> + private val _isNoticeNotificationEnabled = MutableStateFlow(requestCurrentUserNotiNoticePermitUseCase()) + val isNoticeNotificationEnabled: StateFlow get() = _isNoticeNotificationEnabled + + fun initializeSignInSuccess() { + _signInSuccess.value = null + } + + fun requestSignInKakao(accessToken: String) = viewModelScope.launch { + _signInSuccess.emit(Status.LOADING) + requestSignInKakaoUseCase(accessToken = accessToken).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { requestSaveCurrentUserInfoUseCase(ApiResponse.body) coroutineScope { requestMemberInfo() - _loginSuccess.postValue(Event(true)) + _signInSuccess.emit(Status.SUCCESS) } } + is NetworkResponse.ApiError -> { - _isApiErrorExceptionOccurred.postValue(Event(true)) - _loginSuccess.postValue(Event(false)) + _signInSuccess.emit(Status.ERROR) } + is NetworkResponse.NetworkError -> { - _isErrorExceptionOccurred.postValue(Event(true)) - _loginSuccess.postValue(Event(false)) + _signInSuccess.emit(Status.ERROR) } + is NetworkResponse.UnknownError -> { - _loginSuccess.postValue(Event(false)) + _signInSuccess.emit(Status.ERROR) } } } } - fun requestLoginEmail(email: String, password: String) = viewModelScope.launch { - requestLoginEmailUseCase(email = email, password = password).let { ApiResponse -> + fun requestSignInEmail(email: String, password: String) = viewModelScope.launch { + _signInSuccess.emit(Status.LOADING) + requestSignInEmailUseCase(email = email, password = password).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { requestSaveCurrentUserInfoUseCase(ApiResponse.body) requestMemberInfo() - _loginSuccess.postValue(Event(true)) + _signInSuccess.emit(Status.SUCCESS) } + is NetworkResponse.NetworkError -> { - _isErrorExceptionOccurred.postValue(Event(true)) - _loginSuccess.postValue(Event(false)) + _signInSuccess.emit(Status.ERROR) } + is NetworkResponse.ApiError -> { - if (ApiResponse.code != 404) _isApiErrorExceptionOccurred.postValue(Event(true)) - else isLoginFailByUncorrected.postValue(Event(true)) - _loginSuccess.postValue(Event(false)) + // TODO 404 ์—๋Ÿฌ์ฝ”๋“œ ๋ณ„๋„ ์ฒ˜๋ฆฌ + _signInSuccess.emit(Status.ERROR) } + is NetworkResponse.UnknownError -> { - _loginSuccess.postValue(Event(false)) + _signInSuccess.emit(Status.ERROR) } } } @@ -142,18 +182,21 @@ class AccountViewModel @Inject constructor( requestSaveCurrentUserAccessTokenUseCase(accessToken = response) } requestMemberInfo() - _loginSuccess.postValue(Event(true)) + _autoSignInSuccess.postValue(Event(true)) } + is NetworkResponse.NetworkError -> { _isErrorExceptionOccurred.postValue(Event(true)) - _loginSuccess.postValue(Event(false)) + _autoSignInSuccess.postValue(Event(false)) } + is NetworkResponse.ApiError -> { if (ApiResponse.code != 401) _isApiErrorExceptionOccurred.postValue(Event(true)) - _loginSuccess.postValue(Event(false)) + _autoSignInSuccess.postValue(Event(false)) } + is NetworkResponse.UnknownError -> { - _loginSuccess.postValue(Event(false)) + _autoSignInSuccess.postValue(Event(false)) } } } @@ -165,51 +208,77 @@ class AccountViewModel @Inject constructor( is NetworkResponse.Success -> { requestSaveCurrentUserInfoUseCase(ApiResponse.body) } + is NetworkResponse.NetworkError -> { _isErrorExceptionOccurred.postValue(Event(true)) } + is NetworkResponse.ApiError -> { _isApiErrorExceptionOccurred.postValue(Event(true)) } + else -> {} } } } - fun requestSignupEmail(email: String, nickname: String, password: String, profileImg: File?) = + fun requestSignupEmail( + email: String, + nickname: String, + password: String, + profileImg: Bitmap?= null, + profileImgTempDir: String? = null + ) = viewModelScope.launch { - requestSignUpEmailUseCase(email, nickname, password, profileImg).let { ApiResponse -> + _signupSuccess.emit(Status.LOADING) + val resizedImage = profileImg?.let { selectedImage -> + if (profileImgTempDir != null) { + return@let selectedImage.cropCenterBitmap().toFile(profileImgTempDir) + } else { + return@let null + } + } + + requestSignUpEmailUseCase(email, nickname, password, resizedImage).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _signupSuccess.postValue(Event(true)) + _signupSuccess.emit(Status.SUCCESS) } + is NetworkResponse.NetworkError -> { _isErrorExceptionOccurred.postValue(Event(true)) - _signupSuccess.postValue(Event(false)) + _signupSuccess.emit(Status.ERROR) } + is NetworkResponse.ApiError -> { _isApiErrorExceptionOccurred.postValue(Event(true)) - _signupSuccess.postValue(Event(false)) + _signupSuccess.emit(Status.ERROR) } + else -> {} } } } fun requestCheckEmailDuplicate(email: String) = viewModelScope.launch { + _isEmailDuplicate.emit(Status.LOADING) requestCheckEmailDuplicateUseCase(email).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _isEmailDuplicate.postValue(true) + _isEmailDuplicate.emit(Status.SUCCESS) } + is NetworkResponse.NetworkError -> { - _isErrorExceptionOccurred.postValue(Event(true)) - _isEmailDuplicate.postValue(false) + // TODO ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ•„์š” } + is NetworkResponse.ApiError -> { - _isApiErrorExceptionOccurred.postValue(Event(true)) - _isEmailDuplicate.postValue(false) + // TODO ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ•„์š” + if (ApiResponse.code == 409) { + _isEmailDuplicate.emit(Status.ERROR) + } } + else -> {} } } @@ -219,36 +288,41 @@ class AccountViewModel @Inject constructor( requestCheckNicknameDuplicateUseCase(nickname).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _isNicknameDuplicate.postValue(true) + _isNicknameDuplicate.emit(false) } + is NetworkResponse.NetworkError -> { - _isErrorExceptionOccurred.postValue(Event(true)) - _isNicknameDuplicate.postValue(false) + // TODO ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ•„์š” } + is NetworkResponse.ApiError -> { - _isApiErrorExceptionOccurred.postValue(Event(true)) - _isNicknameDuplicate.postValue(false) + // TODO ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ•„์š” + if (ApiResponse.code == 409) { + _isNicknameDuplicate.emit(true) + } } + else -> {} } } } fun requestCertificateEmail(email: String) = viewModelScope.launch { + _certificateEmailAuthCode.emit(EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString()) requestCertificateEmailUseCase(email).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _isCertificateEmailSend.postValue(true) - certificateEmailAuthCode.postValue(ApiResponse.body) + _certificateEmailAuthCode.emit(ApiResponse.body) } + is NetworkResponse.NetworkError -> { - _isErrorExceptionOccurred.postValue(Event(true)) - _isCertificateEmailSend.postValue(false) + _certificateEmailAuthCode.emit(SIGN_UP_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString()) } + is NetworkResponse.ApiError -> { - _isApiErrorExceptionOccurred.postValue(Event(true)) - _isCertificateEmailSend.postValue(false) + _certificateEmailAuthCode.emit(SIGN_UP_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString()) } + else -> {} } } @@ -262,137 +336,260 @@ class AccountViewModel @Inject constructor( suspend fun getCurrentFcmToken() = FirebaseMessagingService().getCurrentToken() fun requestWithdraw(content: String) = viewModelScope.launch { + _withdrawSuccess.emit(Status.LOADING) requestResignUseCase(content).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _withdrawSuccess.postValue(Event(true)) + _withdrawSuccess.emit(Status.SUCCESS) } + is NetworkResponse.NetworkError -> { _isErrorExceptionOccurred.postValue(Event(true)) - _withdrawSuccess.postValue(Event(false)) + _withdrawSuccess.emit(Status.ERROR) } + is NetworkResponse.ApiError -> { _isApiErrorExceptionOccurred.postValue(Event(true)) - _withdrawSuccess.postValue(Event(false)) + _withdrawSuccess.emit(Status.ERROR) } + else -> {} } } } - fun requestLogout() = viewModelScope.launch { - requestLogoutUseCase().let { ApiResponse -> + fun requestWithdrawGuideImage(fileName: String, withdrawalReason: WithdrawalReason) = viewModelScope.launch { + requestResignGuideImageUseCase(fileName, withdrawalReason)?.let { apiResponse -> + when (apiResponse) { + is NetworkResponse.Success -> { + apiResponse.body?.let { imageData -> + val currentImages = _guideImages.value.toMutableMap() + currentImages[fileName] = imageData + _guideImages.emit(currentImages) + } + } + is NetworkResponse.ApiError -> { + // ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ํ•ด๋‹น ์ด๋ฏธ์ง€๋ฅผ ์บ์‹œ์—์„œ ์ œ๊ฑฐ + removeGuideImageFromCache(fileName) + } + is NetworkResponse.NetworkError -> { + // ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ ์‹œ ํ•ด๋‹น ์ด๋ฏธ์ง€๋ฅผ ์บ์‹œ์—์„œ ์ œ๊ฑฐ + removeGuideImageFromCache(fileName) + } + is NetworkResponse.UnknownError -> { + // ์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ ์‹œ ํ•ด๋‹น ์ด๋ฏธ์ง€๋ฅผ ์บ์‹œ์—์„œ ์ œ๊ฑฐ + removeGuideImageFromCache(fileName) + } + } + } + } + + private suspend fun removeGuideImageFromCache(fileName: String) { + val currentImages = _guideImages.value.toMutableMap() + currentImages.remove(fileName) + _guideImages.emit(currentImages) + } + fun clearGuideImages() = viewModelScope.launch { + _guideImages.emit(emptyMap()) + } + + fun requestWithdrawGuideWords(withdrawalReason: WithdrawalReason) = viewModelScope.launch { + requestResignGuideWordsUseCase(withdrawalReason)?.let { apiResponse -> + when (apiResponse) { + is NetworkResponse.Success -> { + val words = apiResponse.body ?: emptyList() + when (withdrawalReason) { + WithdrawalReason.WANT_TO_DELETE_HISTORY -> _recordGuideWords.emit(words) + WithdrawalReason.CONTENT_NOT_SATISFYING -> _followGuideWords.emit(words) + else -> { /* DO NOTHING */ } + } + } + is NetworkResponse.ApiError -> { + when (withdrawalReason) { + WithdrawalReason.WANT_TO_DELETE_HISTORY -> _recordGuideWords.emit(emptyList()) + WithdrawalReason.CONTENT_NOT_SATISFYING -> _followGuideWords.emit(emptyList()) + else -> { /* DO NOTHING */ } + } + } + is NetworkResponse.NetworkError -> { + when (withdrawalReason) { + WithdrawalReason.WANT_TO_DELETE_HISTORY -> _recordGuideWords.emit(emptyList()) + WithdrawalReason.CONTENT_NOT_SATISFYING -> _followGuideWords.emit(emptyList()) + else -> { /* DO NOTHING */ } + } + } + is NetworkResponse.UnknownError -> { + when (withdrawalReason) { + WithdrawalReason.WANT_TO_DELETE_HISTORY -> _recordGuideWords.emit(emptyList()) + WithdrawalReason.CONTENT_NOT_SATISFYING -> _followGuideWords.emit(emptyList()) + else -> { /* DO NOTHING */ } + } + } + } + } + } + + fun requestSignOut() = viewModelScope.launch { + _signOutSuccess.emit(Status.LOADING) + requestSignOutUseCase().let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _logoutSuccess.postValue(Event(true)) + _signOutSuccess.emit(Status.SUCCESS) } + is NetworkResponse.NetworkError -> { _isErrorExceptionOccurred.postValue(Event(true)) - _logoutSuccess.postValue(Event(false)) + _signOutSuccess.emit(Status.ERROR) } + is NetworkResponse.ApiError -> { _isApiErrorExceptionOccurred.postValue(Event(true)) - _logoutSuccess.postValue(Event(false)) + _signOutSuccess.emit(Status.ERROR) } + else -> {} } } } fun requestCheckEmail(inputEmail: String) = viewModelScope.launch { + _checkEmailSuccess.emit(EmailExistenceStatus.LOADING) requestCheckEmailUseCase(email = inputEmail).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _checkEmailSuccess.postValue(true) + _checkEmailSuccess.emit(EmailExistenceStatus.EXISTS) } + is NetworkResponse.NetworkError -> { - _isErrorExceptionOccurred.postValue(Event(true)) - _checkEmailSuccess.postValue(false) + _checkEmailSuccess.emit(EmailExistenceStatus.ERROR) } + is NetworkResponse.ApiError -> { - _isApiErrorExceptionOccurred.postValue(Event(true)) - _checkEmailSuccess.postValue(false) + if (ApiResponse.code == 404) { + _checkEmailSuccess.emit(EmailExistenceStatus.NOT_EXISTS) + } else { + _checkEmailSuccess.emit(EmailExistenceStatus.ERROR) + } + } + is NetworkResponse.UnknownError -> { + _checkEmailSuccess.emit(EmailExistenceStatus.ERROR) } - else -> {} } } } - fun requestCheckEmailAuth(inputEmail: String) = viewModelScope.launch { - requestCheckEmailAuthUseCase(inputEmail).let { ApiResponse -> + fun requestCheckOAuthEmail(inputEmail: String) = viewModelScope.launch { + _checkOAuthEmailSuccess.emit(CheckOAuthEmailStatus.LOADING) + requestCheckOAuthEmailUseCase(email = inputEmail).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _isCertificateEmailSend.postValue(true) - certificateEmailAuthCode.postValue(ApiResponse.body) + _checkOAuthEmailSuccess.emit(CheckOAuthEmailStatus.NORMAL_EMAIL) } + is NetworkResponse.NetworkError -> { - _isErrorExceptionOccurred.postValue(Event(true)) - _isCertificateEmailSend.postValue(false) + _checkOAuthEmailSuccess.emit(CheckOAuthEmailStatus.ERROR) } + is NetworkResponse.ApiError -> { - _isApiErrorExceptionOccurred.postValue(Event(true)) - _isCertificateEmailSend.postValue(false) + if (ApiResponse.code == 400) { + _checkOAuthEmailSuccess.emit(CheckOAuthEmailStatus.OAUTH_ACCOUNT) + } else { + _checkOAuthEmailSuccess.emit(CheckOAuthEmailStatus.ERROR) + } + } + is NetworkResponse.UnknownError -> { + _checkOAuthEmailSuccess.emit(CheckOAuthEmailStatus.ERROR) + } + } + } + } + + fun requestCertificateEmailPasswordReset(inputEmail: String) = viewModelScope.launch { + _certificateEmailAuthCode.emit(EMAIL_CERTIFICATE_AUTH_CODE_INITIAL.toString()) + requestCertificateEmailPasswordResetUseCase(inputEmail).let { ApiResponse -> + when (ApiResponse) { + is NetworkResponse.Success -> { + _certificateEmailAuthCode.emit(ApiResponse.body) + } + + is NetworkResponse.NetworkError -> { + _certificateEmailAuthCode.emit(RESET_PASSWORD_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString()) } + + is NetworkResponse.ApiError -> { + _certificateEmailAuthCode.emit(RESET_PASSWORD_EMAIL_CERTIFICATE_AUTH_CODE_FAIL.toString()) + } + else -> {} } } } fun requestCheckCurrentPassword(inputPassword: String) = viewModelScope.launch { + _checkCurrentPasswordSuccess.emit(null) requestCheckCurrentPasswordUseCase(password = inputPassword).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _checkCurrentPasswordSuccess.postValue(Event(true)) + _checkCurrentPasswordSuccess.emit(true) } + is NetworkResponse.NetworkError -> { _isErrorExceptionOccurred.postValue(Event(true)) - _checkCurrentPasswordSuccess.postValue(Event(false)) + _checkCurrentPasswordSuccess.emit(false) } + is NetworkResponse.ApiError -> { _isApiErrorExceptionOccurred.postValue(Event(true)) - _checkCurrentPasswordSuccess.postValue(Event(false)) + _checkCurrentPasswordSuccess.emit(false) } + else -> {} } } } fun requestChangePassword(email: String, newPassword: String) = viewModelScope.launch { + _resetPasswordSuccess.emit(Status.LOADING) requestChangePasswordUseCase(email = email, password = newPassword).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _changePasswordSuccess.postValue(true) + _resetPasswordSuccess.emit(Status.SUCCESS) } + is NetworkResponse.NetworkError -> { - _isErrorExceptionOccurred.postValue(Event(true)) - _changePasswordSuccess.postValue(false) + _resetPasswordSuccess.emit(Status.ERROR) } + is NetworkResponse.ApiError -> { - _isApiErrorExceptionOccurred.postValue(Event(true)) - _changePasswordSuccess.postValue(false) + _resetPasswordSuccess.emit(Status.ERROR) } + else -> {} } } } fun requestChangePassword(newPassword: String) = viewModelScope.launch { + _changePasswordSuccess.emit(Status.LOADING) requestSettingChangePasswordUseCase( email = requestCurrentUserInfoUseCase().email!!, password = newPassword ).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _changePasswordSuccess.postValue(true) + _changePasswordSuccess.emit(Status.SUCCESS) } + is NetworkResponse.NetworkError -> { _isErrorExceptionOccurred.postValue(Event(true)) - _changePasswordSuccess.postValue(false) + _changePasswordSuccess.emit(Status.ERROR) } + is NetworkResponse.ApiError -> { _isApiErrorExceptionOccurred.postValue(Event(true)) - _changePasswordSuccess.postValue(false) + _changePasswordSuccess.emit(Status.ERROR) } + else -> {} } } @@ -402,7 +599,11 @@ class AccountViewModel @Inject constructor( fun getCurrentUserInfo() = requestCurrentUserInfoUseCase() fun getCurrentUserNotiDevicePermit() = requestCurrentUserNotiDevicePermitUseCase() - fun requestCurrentUserNotiDevicePermit(isPermit: Boolean) = requestCurrentUserNotiDevicePermitUseCase(isPermit = isPermit) - fun getCurrentUserNotiNoticePermit() = requestCurrentUserNotiNoticePermitUseCase() - fun requestCurrentUserNotiNoticePermit(isPermit: Boolean) = requestCurrentUserNotiNoticePermitUseCase(isPermit = isPermit) + fun requestCurrentUserNotiDevicePermit(isPermit: Boolean) = + requestCurrentUserNotiDevicePermitUseCase(isPermit = isPermit) + + fun changeNoticeNotificationSetting(isPermit: Boolean) = viewModelScope.launch { + requestCurrentUserNotiNoticePermitUseCase(isPermit = isPermit) + _isNoticeNotificationEnabled.emit(isPermit) + } } diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/BookmarkViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/BookmarkViewModel.kt new file mode 100644 index 00000000..0db8b2a2 --- /dev/null +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/BookmarkViewModel.kt @@ -0,0 +1,92 @@ +package daily.dayo.presentation.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.domain.model.BookmarkPost +import daily.dayo.domain.repository.BookmarkRepository +import daily.dayo.domain.usecase.bookmark.RequestAllMyBookmarkPostListUseCase +import daily.dayo.domain.usecase.bookmark.RequestDeleteBookmarkPostUseCase +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class BookmarkViewModel @Inject constructor( + private val bookmarkRepository: BookmarkRepository, + private val requestAllMyBookmarkPostListUseCase: RequestAllMyBookmarkPostListUseCase, + private val requestDeleteBookmarkPostUseCase: RequestDeleteBookmarkPostUseCase +) : ViewModel() { + private val _uiState = MutableStateFlow(BookmarkUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + requestBookmarkCount() + requestAllMyBookmarkPostList() + } + + fun toggleEditMode() { + _uiState.update { it.copy(isEditMode = !it.isEditMode, selectedBookmarks = emptySet()) } + } + + fun toggleSelection(postId: Long) { + _uiState.update { + val currentSelection = it.selectedBookmarks + val newSelection = if (currentSelection.contains(postId)) { + currentSelection - postId + } else { + currentSelection + postId + } + it.copy(selectedBookmarks = newSelection) + } + } + + fun deleteSelectedBookmarks() { + val selectedIds = _uiState.value.selectedBookmarks + viewModelScope.launch { + selectedIds.map { id -> + async { requestDeleteBookmarkPostUseCase(id) } + }.awaitAll() + toggleEditMode() + requestBookmarkCount() + requestAllMyBookmarkPostList() + } + } + + private fun requestBookmarkCount() { + viewModelScope.launch { + _uiState.update { + it.copy( + count = bookmarkRepository.requestBookmarkCount() + ) + } + } + } + + private fun requestAllMyBookmarkPostList() { + viewModelScope.launch { + val bookmarkPosts = requestAllMyBookmarkPostListUseCase() + .cachedIn(viewModelScope) + + _uiState.update { + it.copy(bookmarks = bookmarkPosts) + } + } + } +} + +data class BookmarkUiState( + val count: Int = 0, + val bookmarks: Flow> = flow { emit(PagingData.empty()) }, + val isEditMode: Boolean = false, + val selectedBookmarks: Set = emptySet() +) \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/FeedViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/FeedViewModel.kt index c5fcb541..9aedf768 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/FeedViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/FeedViewModel.kt @@ -1,15 +1,12 @@ package daily.dayo.presentation.viewmodel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map -import daily.dayo.presentation.common.Resource -import daily.dayo.domain.model.BookmarkPostResponse -import daily.dayo.domain.model.LikePostResponse +import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.domain.model.Category import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.model.Post import daily.dayo.domain.usecase.bookmark.RequestBookmarkPostUseCase @@ -17,8 +14,8 @@ import daily.dayo.domain.usecase.bookmark.RequestDeleteBookmarkPostUseCase import daily.dayo.domain.usecase.like.RequestLikePostUseCase import daily.dayo.domain.usecase.like.RequestUnlikePostUseCase import daily.dayo.domain.usecase.post.RequestFeedListUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import javax.inject.Inject @@ -32,110 +29,97 @@ class FeedViewModel @Inject constructor( private val requestDeleteBookmarkPostUseCase: RequestDeleteBookmarkPostUseCase ) : ViewModel() { - private val _feedList = MutableLiveData>() - val feedList: LiveData> get() = _feedList + private var _currentCategory = Category.ALL + private val currentCategory get() = _currentCategory - private val _postLiked = MutableLiveData>() - val postLiked: LiveData> get() = _postLiked + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing = _isRefreshing.asStateFlow() - private val _postBookmarked = MutableLiveData>() - val postBookmarked: LiveData> get() = _postBookmarked + private val _feedPosts = MutableStateFlow>(PagingData.empty()) + val feedPosts = _feedPosts.asStateFlow() - fun requestFeedList() = viewModelScope.launch(Dispatchers.IO) { - requestFeedListUseCase() - .cachedIn(viewModelScope) - .collectLatest { _feedList.postValue(it) } + fun loadFeedPosts() { + viewModelScope.launch { + _isRefreshing.emit(true) + requestFeedList() + _isRefreshing.emit(false) + } } - fun requestLikePost(postId: Int) = viewModelScope.launch(Dispatchers.IO) { - requestLikePostUseCase(postId = postId)?.let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _feedList.postValue( - _feedList.value?.map { - if (it.postId == postId) { - it.copy( - heart = true, - heartCount = ApiResponse.body?.allCount ?: 0 - ) - } else { - it - } - } - ) - _postLiked.postValue(Resource.success(ApiResponse.body)) - } - is NetworkResponse.NetworkError -> { _postLiked.postValue(Resource.error(ApiResponse.exception.toString(), null)) } - is NetworkResponse.ApiError -> { _postLiked.postValue(Resource.error(ApiResponse.error.toString(), null)) } - is NetworkResponse.UnknownError -> { _postLiked.postValue(Resource.error(ApiResponse.throwable.toString(), null)) } - } - } + fun setCurrentCategory(category: Category) { + _currentCategory = category } - fun requestUnlikePost(postId: Int) = viewModelScope.launch(Dispatchers.IO) { - requestUnlikePostUseCase(postId = postId)?.let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _feedList.postValue( - _feedList.value?.map { - if (it.postId == postId) { - it.copy( - heart = false, - heartCount = ApiResponse.body?.allCount ?: 0 - ) - } else { - it - } + fun toggleLikePost(post: Post) { + viewModelScope.launch { + post.postId?.let { postId -> + if (post.heart) { + requestUnlikePostUseCase(postId = postId) + } else { + requestLikePostUseCase(postId = postId) + }.let { response -> + when (response) { + is NetworkResponse.Success -> { + _feedPosts.emit( + _feedPosts.value.map { + if (it.postId == post.postId) { + it.copy( + heart = !post.heart, + heartCount = response.body?.allCount ?: 0 + ) + } else { + it + } + } + ) } - ) + + else -> Unit + } } - else -> {} } } } - fun requestBookmarkPost(postId: Int) = viewModelScope.launch(Dispatchers.IO) { - requestBookmarkPostUseCase(postId = postId).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _feedList.postValue( - _feedList.value?.map { - if (it.postId == postId) { - it.copy( - bookmark = true + fun toggleBookmarkPost(post: Post) { + viewModelScope.launch { + post.postId?.let { postId -> + post.bookmark?.let { bookmark -> + if (bookmark) { + requestDeleteBookmarkPostUseCase(postId = postId) + } else { + requestBookmarkPostUseCase(postId = postId) + }.let { response -> + when (response) { + is NetworkResponse.Success -> { + _feedPosts.emit( + _feedPosts.value.map { + if (it.postId == post.postId) { + it.copy( + bookmark = !bookmark + ) + } else { + it + } + } ) - } else { - it } + + else -> Unit } - ) - _postBookmarked.postValue(Resource.success(ApiResponse.body)) + } } - is NetworkResponse.NetworkError -> { _postBookmarked.postValue(Resource.error(ApiResponse.exception.toString(), null)) } - is NetworkResponse.ApiError -> { _postBookmarked.postValue(Resource.error(ApiResponse.error.toString(), null)) } - is NetworkResponse.UnknownError -> { _postBookmarked.postValue(Resource.error(ApiResponse.throwable.toString(), null)) } } } } - fun requestDeleteBookmarkPost(postId: Int) = viewModelScope.launch(Dispatchers.IO) { - requestDeleteBookmarkPostUseCase(postId = postId)?.let { ApiResponse -> - when(ApiResponse) { - is NetworkResponse.Success -> { - _feedList.postValue( - _feedList.value?.map { - if (it.postId == postId) { - it.copy( - bookmark = false - ) - } else { - it - } - } - ) + private fun requestFeedList() { + viewModelScope.launch { + requestFeedListUseCase(currentCategory) + .cachedIn(viewModelScope) + .collectLatest { + _feedPosts.emit(it) } - else -> {} - } } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/FolderViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/FolderViewModel.kt index fae82f59..c8430163 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/FolderViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/FolderViewModel.kt @@ -1,103 +1,166 @@ package daily.dayo.presentation.viewmodel +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn -import daily.dayo.presentation.common.Event -import daily.dayo.presentation.common.Resource -import daily.dayo.domain.model.* -import daily.dayo.domain.usecase.folder.* import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.collectLatest +import daily.dayo.domain.model.Folder +import daily.dayo.domain.model.FolderInfo +import daily.dayo.domain.model.FolderOrder +import daily.dayo.domain.model.FolderPost +import daily.dayo.domain.model.NetworkResponse +import daily.dayo.domain.model.Privacy +import daily.dayo.domain.usecase.folder.RequestAllFolderListUseCase +import daily.dayo.domain.usecase.folder.RequestAllMyFolderListUseCase +import daily.dayo.domain.usecase.folder.RequestCreateFolderUseCase +import daily.dayo.domain.usecase.folder.RequestDeleteFolderUseCase +import daily.dayo.domain.usecase.folder.RequestEditFolderUseCase +import daily.dayo.domain.usecase.folder.RequestFolderInfoUseCase +import daily.dayo.domain.usecase.folder.RequestFolderMoveUseCase +import daily.dayo.domain.usecase.folder.RequestFolderPostListUseCase +import daily.dayo.domain.usecase.post.RequestDeletePostUseCase +import daily.dayo.presentation.common.Resource +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import java.io.File import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException @HiltViewModel class FolderViewModel @Inject constructor( private val requestCreateFolderUseCase: RequestCreateFolderUseCase, private val requestEditFolderUseCase: RequestEditFolderUseCase, private val requestDeleteFolderUseCase: RequestDeleteFolderUseCase, - private val requestOrderFolderUseCase: RequestOrderFolderUseCase, private val requestAllMyFolderListUseCase: RequestAllMyFolderListUseCase, + private val requestUserFolderListUseCase: RequestAllFolderListUseCase, private val requestFolderInfoUseCase: RequestFolderInfoUseCase, - private val requestFolderPostListUseCase: RequestFolderPostListUseCase + private val requestFolderPostListUseCase: RequestFolderPostListUseCase, + private val requestDeletePostUseCase: RequestDeletePostUseCase, + private val requestFolderMoveUseCase: RequestFolderMoveUseCase ) : ViewModel() { - private val _deleteSuccess = MutableLiveData>() - val deleteSuccess: LiveData> get() = _deleteSuccess + private val _uiState = MutableStateFlow(FolderUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _createSuccess = MutableSharedFlow() + val createSuccess = _createSuccess.asSharedFlow() + + private val _editSuccess = MutableSharedFlow() + val editSuccess = _editSuccess.asSharedFlow() - private val _editSuccess = MutableLiveData>() - val editSuccess: LiveData> get() = _editSuccess + private val _folderDeleteSuccess = MutableSharedFlow() + val folderDeleteSuccess = _folderDeleteSuccess.asSharedFlow() - private val _thumbnailUri = MutableLiveData() - val thumbnailUri: LiveData get() = _thumbnailUri + private val _postDeleteSuccess = MutableSharedFlow() + val postDeleteSuccess = _postDeleteSuccess.asSharedFlow() + + private val _postMoveSuccess = MutableSharedFlow() + val postMoveSuccess = _postMoveSuccess.asSharedFlow() private val _folderList = MutableLiveData>>() val folderList: LiveData>> get() = _folderList - private val _orderFolderSuccess = MutableLiveData>() - val orderFolderSuccess: LiveData> get() = _orderFolderSuccess - - private val _folderAddSuccess = MutableLiveData>() - val folderAddSuccess: LiveData> get() = _folderAddSuccess + fun toggleEditMode() { + _uiState.update { it.copy(isEditMode = !it.isEditMode, selectedPosts = emptySet()) } + } - private val _folderInfo = MutableLiveData>() - val folderInfo: LiveData> get() = _folderInfo + fun toggleSelection(postId: Long) { + _uiState.update { + val currentSelection = it.selectedPosts + val newSelection = if (currentSelection.contains(postId)) { + currentSelection - postId + } else { + currentSelection + postId + } + it.copy(selectedPosts = newSelection) + } + } - private val _folderPostList = MutableLiveData>() - val folderPostList: LiveData> get() = _folderPostList + fun toggleFolderOrder(folderId: Long) { + val newOrder = when (_uiState.value.folderOrder) { + FolderOrder.NEW -> FolderOrder.OLD + FolderOrder.OLD -> FolderOrder.NEW + } + _uiState.update { it.copy(folderOrder = newOrder) } + requestFolderPostList(folderId) + } - fun requestCreateFolder(name: String, privacy: Privacy, subheading: String?, thumbnailImg: File?) = viewModelScope.launch { - requestCreateFolderUseCase(name = name, privacy = privacy, subheading = subheading, thumbnailImg = thumbnailImg).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _folderAddSuccess.postValue(Event(true)) - } - else -> { - _folderAddSuccess.postValue(Event(false)) + fun deletePosts() { + viewModelScope.launch { + try { + val deleteJobs = uiState.value.selectedPosts.map { postId -> + async { + requestDeletePostUseCase(postId).let { response -> + when (response) { + is NetworkResponse.Success -> true + is NetworkResponse.ApiError -> throw CancellationException("API Error: PostId ${postId}, ${response.error}") + is NetworkResponse.NetworkError -> throw CancellationException("Network Error: PostId ${postId}, ${response.exception.message}") + is NetworkResponse.UnknownError -> throw CancellationException("Unknown Error: PostId ${postId}, ${response.throwable?.message}") + } + } + } } + deleteJobs.awaitAll() + _postDeleteSuccess.emit(true) + } catch (e: CancellationException) { + Log.e("Delete Post", "${e.message}") + _postDeleteSuccess.emit(false) } } } - fun requestEditFolder(folderId: Int, name: String, privacy: Privacy, subheading: String?, isFileChange: Boolean, thumbnailImage: File?) = viewModelScope.launch { - requestEditFolderUseCase(folderId = folderId, name = name, privacy = privacy, subheading = subheading, isFileChange = isFileChange, thumbnailImage).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _editSuccess.postValue(Event(true)) - } - else -> { - _editSuccess.postValue(Event(false)) + fun requestCreateFolder(name: String, subheading: String, privacy: Privacy) { + viewModelScope.launch { + requestCreateFolderUseCase( + name = name, + subheading = subheading, + privacy = privacy, + thumbnailImg = null + ).let { response -> + when (response) { + is NetworkResponse.Success -> _createSuccess.emit(true) + else -> _createSuccess.emit(false) } } } } - fun requestDeleteFolder(folderId: Int) = viewModelScope.launch { - requestDeleteFolderUseCase(folderId = folderId).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _deleteSuccess.postValue(Event(true)) - } - else -> { - _deleteSuccess.postValue(Event(false)) + fun requestEditFolder(folderId: Long, name: String, subheading: String, privacy: Privacy) { + viewModelScope.launch { + requestEditFolderUseCase( + folderId = folderId, + name = name, + subheading = subheading, + privacy = privacy, + isFileChange = false, + thumbnailImg = null + ).let { response -> + when (response) { + is NetworkResponse.Success -> _editSuccess.emit(true) + else -> _editSuccess.emit(false) } } } } - fun requestOrderFolder(folderOrder: List) = viewModelScope.launch { - requestOrderFolderUseCase(folderOrder = folderOrder).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _orderFolderSuccess.postValue(Event(true)) - } - else -> { - _orderFolderSuccess.postValue(Event(false)) + fun requestDeleteFolder(folderId: Long) { + viewModelScope.launch { + requestDeleteFolderUseCase(folderId = folderId).let { response -> + when (response) { + is NetworkResponse.Success -> _folderDeleteSuccess.emit(true) + else -> _folderDeleteSuccess.emit(false) } } } @@ -110,12 +173,15 @@ class FolderViewModel @Inject constructor( is NetworkResponse.Success -> { _folderList.postValue(Resource.success(ApiResponse.body?.data)) } + is NetworkResponse.NetworkError -> { _folderList.postValue(Resource.error(ApiResponse.exception.toString(), null)) } + is NetworkResponse.ApiError -> { _folderList.postValue(Resource.error(ApiResponse.error.toString(), null)) } + is NetworkResponse.UnknownError -> { _folderList.postValue(Resource.error(ApiResponse.throwable.toString(), null)) } @@ -123,28 +189,83 @@ class FolderViewModel @Inject constructor( } } - fun requestFolderInfo(folderId: Int) = viewModelScope.launch { - requestFolderInfoUseCase(folderId).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _folderInfo.postValue(Resource.success(ApiResponse.body)) - } - is NetworkResponse.NetworkError -> { - _folderInfo.postValue(Resource.error(ApiResponse.exception.toString(), null)) - } - is NetworkResponse.ApiError -> { - _folderInfo.postValue(Resource.error(ApiResponse.error.toString(), null)) - } - is NetworkResponse.UnknownError -> { - _folderInfo.postValue(Resource.error(ApiResponse.throwable.toString(), null)) + fun requestUserFolderList(memberId: String) { + viewModelScope.launch { + _folderList.postValue(Resource.loading(null)) + requestUserFolderListUseCase(memberId).let { apiResponse -> + when (apiResponse) { + is NetworkResponse.Success -> { + _folderList.postValue(Resource.success(apiResponse.body?.data)) + } + + is NetworkResponse.NetworkError -> { + _folderList.postValue(Resource.error(apiResponse.exception.toString(), null)) + } + + is NetworkResponse.ApiError -> { + _folderList.postValue(Resource.error(apiResponse.error.toString(), null)) + } + + is NetworkResponse.UnknownError -> { + _folderList.postValue(Resource.error(apiResponse.throwable.toString(), null)) + } } } } } - fun requestFolderPostList(folderId: Int) = viewModelScope.launch { - requestFolderPostListUseCase(folderId) - .cachedIn(viewModelScope) - .collectLatest { _folderPostList.postValue(it) } + fun requestFolderInfo(folderId: Long) { + viewModelScope.launch { + val result = when (val response = requestFolderInfoUseCase(folderId)) { + is NetworkResponse.Success -> response.body ?: DEFAULT_FOLDER_INFO + else -> DEFAULT_FOLDER_INFO + } + + _uiState.update { + it.copy(folderInfo = result) + } + } } -} \ No newline at end of file + + fun requestFolderPostList(folderId: Long) { + viewModelScope.launch { + val folderPosts = requestFolderPostListUseCase(folderId, _uiState.value.folderOrder) + .cachedIn(viewModelScope) + + _uiState.update { + it.copy(folderPosts = folderPosts) + } + } + } + + fun moveSelectedPost(targetFolderId: Long) { + viewModelScope.launch { + requestFolderMoveUseCase( + postIdList = uiState.value.selectedPosts.map { it.toLong() }, + targetFolderId = targetFolderId + ).let { response -> + when (response) { + is NetworkResponse.Success -> _postMoveSuccess.emit(true) + else -> _postMoveSuccess.emit(false) + } + } + } + } +} + +data class FolderUiState( + val folderInfo: FolderInfo = DEFAULT_FOLDER_INFO, + val folderPosts: Flow> = flow { emit(PagingData.empty()) }, + val folderOrder: FolderOrder = FolderOrder.NEW, + val isEditMode: Boolean = false, + val selectedPosts: Set = emptySet() +) + +val DEFAULT_FOLDER_INFO = FolderInfo( + memberId = "", + name = "", + postCount = 0, + privacy = Privacy.ALL, + subheading = "", + thumbnailImage = "" +) diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/FollowViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/FollowViewModel.kt index 59717a59..7cf80abd 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/FollowViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/FollowViewModel.kt @@ -4,16 +4,15 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import daily.dayo.presentation.common.Event -import daily.dayo.presentation.common.Resource -import daily.dayo.domain.model.MyFollower +import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.domain.model.Follow import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.usecase.follow.RequestCreateFollowUseCase import daily.dayo.domain.usecase.follow.RequestDeleteFollowUseCase import daily.dayo.domain.usecase.follow.RequestListAllFollowerUseCase import daily.dayo.domain.usecase.follow.RequestListAllFollowingUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import daily.dayo.domain.usecase.member.RequestCurrentUserInfoUseCase +import daily.dayo.presentation.common.Event import kotlinx.coroutines.launch import javax.inject.Inject @@ -26,113 +25,142 @@ class FollowViewModel @Inject constructor( private val requestCurrentUserInfoUseCase: RequestCurrentUserInfoUseCase ) : ViewModel() { - var memberId: String = "" - - private val _followerFollowSuccess = MutableLiveData>() - val followerFollowSuccess: LiveData> get() = _followerFollowSuccess - private val _followerUnfollowSuccess = MutableLiveData>() - val followerUnfollowSuccess: LiveData> get() = _followerUnfollowSuccess + val followerUnfollowSuccess: LiveData> = _followerUnfollowSuccess private val _followingFollowSuccess = MutableLiveData>() - val followingFollowSuccess: LiveData> get() = _followingFollowSuccess - - private val _followingUnfollowSuccess = MutableLiveData>() - val followingUnfollowSuccess: LiveData> get() = _followingUnfollowSuccess - - private val _followerList = MutableLiveData>>() - val followerList: LiveData>> get() = _followerList - - private val _followerCount = MutableLiveData>() - val followerCount: LiveData> get() = _followerCount - - private val _followingList = MutableLiveData>>() - val followingList: LiveData>> get() = _followingList - - private val _followingCount = MutableLiveData>() - val followingCount: LiveData> get() = _followingCount - - fun requestListAllFollower(memberId: String) = viewModelScope.launch { - requestListAllFollowerUseCase(memberId = memberId).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _followerCount.postValue(Resource.success(ApiResponse.body?.count)) - val myInfo = ApiResponse.body - ?.data - ?.find { it.memberId == requestCurrentUserInfoUseCase().memberId } - val tmpFollowerList = ApiResponse.body?.data - ?.filterNot { it.memberId == requestCurrentUserInfoUseCase().memberId } - ?.toMutableList() - if (myInfo != null) tmpFollowerList?.add(0, myInfo) - _followerList.postValue(Resource.success(tmpFollowerList)) - } - is NetworkResponse.NetworkError -> { - _followerList.postValue(Resource.error(ApiResponse.exception.toString(), null)) - } - is NetworkResponse.ApiError -> { - _followerList.postValue(Resource.error(ApiResponse.error.toString(), null)) - } - is NetworkResponse.UnknownError -> { - _followerList.postValue(Resource.error(ApiResponse.throwable.toString(), null)) + val followingFollowSuccess: LiveData> = _followingFollowSuccess + + private val _followerUiState = MutableLiveData() + val followerUiState: LiveData = _followerUiState + + private val _followingUiState = MutableLiveData() + val followingUiState: LiveData = _followingUiState + + fun requestFollowerList(memberId: String) { + viewModelScope.launch { + requestListAllFollowerUseCase(memberId).let { response -> + when (response) { + is NetworkResponse.Success -> { + val result = response.body?.let { result -> + val followerList = result.data.sortedByDescending { + it.memberId == requestCurrentUserInfoUseCase().memberId + } + FollowUiState.Success(result.count, followerList) + } ?: FollowUiState.Success() + _followerUiState.postValue(result) + } + + is NetworkResponse.NetworkError, + is NetworkResponse.ApiError, + is NetworkResponse.UnknownError -> { + _followerUiState.postValue(FollowUiState.Error) + } } } } } - fun requestListAllFollowing(memberId: String) = viewModelScope.launch { - requestListAllFollowingUseCase(memberId = memberId).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _followingCount.postValue(Resource.success(ApiResponse.body?.count)) - val myInfo = ApiResponse.body - ?.data - ?.find { it.memberId == requestCurrentUserInfoUseCase().memberId } - val tmpFollowingList = ApiResponse.body?.data - ?.filterNot { it.memberId == requestCurrentUserInfoUseCase().memberId } - ?.toMutableList() - if (myInfo != null) tmpFollowingList?.add(0, myInfo) - _followingList.postValue(Resource.success(tmpFollowingList)) - } - is NetworkResponse.NetworkError -> { - _followingList.postValue(Resource.error(ApiResponse.exception.toString(), null)) - } - is NetworkResponse.ApiError -> { - _followingList.postValue(Resource.error(ApiResponse.error.toString(), null)) - } - is NetworkResponse.UnknownError -> { - _followingList.postValue(Resource.error(ApiResponse.throwable.toString(), null)) + fun requestFollowingList(memberId: String) { + viewModelScope.launch { + requestListAllFollowingUseCase(memberId).let { response -> + when (response) { + is NetworkResponse.Success -> { + val result = response.body?.let { result -> + val followingList = result.data.sortedByDescending { + it.memberId == requestCurrentUserInfoUseCase().memberId + } + FollowUiState.Success(result.count, followingList) + } ?: FollowUiState.Success() + _followingUiState.postValue(result) + } + + is NetworkResponse.NetworkError, + is NetworkResponse.ApiError, + is NetworkResponse.UnknownError -> { + _followingUiState.postValue(FollowUiState.Error) + } } } } } - fun requestCreateFollow(followerId: String, isFollower: Boolean) = viewModelScope.launch { - requestCreateFollowUseCase(followerId = followerId).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - if (isFollower) _followerFollowSuccess.postValue(Event(true)) - else _followingFollowSuccess.postValue(Event(true)) - } - else -> { - if (isFollower) _followerFollowSuccess.postValue(Event(false)) - else _followingFollowSuccess.postValue(Event(false)) + fun requestFollow(followerId: String, isFollower: Boolean) { + viewModelScope.launch { + requestCreateFollowUseCase(followerId = followerId).let { response -> + when (response) { + is NetworkResponse.Success -> { + if (!isFollower) _followingFollowSuccess.postValue(Event(true)) + } + + else -> { + if (!isFollower) _followingFollowSuccess.postValue(Event(false)) + } } } } } - fun requestDeleteFollow(followerId: String, isFollower: Boolean) = viewModelScope.launch { - requestDeleteFollowUseCase(followerId).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - if (isFollower) _followerUnfollowSuccess.postValue(Event(true)) - else _followingUnfollowSuccess.postValue(Event(true)) + fun requestUnfollow(followerId: String, isFollower: Boolean) { + viewModelScope.launch { + requestDeleteFollowUseCase(followerId).let { response -> + when (response) { + is NetworkResponse.Success -> { + if (isFollower) _followerUnfollowSuccess.postValue(Event(true)) + } + + else -> { + if (isFollower) _followerUnfollowSuccess.postValue(Event(false)) + } } - else -> { - if (isFollower) _followerUnfollowSuccess.postValue(Event(false)) - else _followingUnfollowSuccess.postValue(Event(false)) + } + } + } + + fun toggleFollow(follow: Follow, isFollower: Boolean) { + viewModelScope.launch { + if (follow.isFollow) { + requestUnfollow(follow.memberId, isFollower) + } else { + requestFollow(follow.memberId, isFollower) + } + + if (isFollower) { + updateFollowUiState(follow, _followerUiState) + } else { + updateFollowUiState(follow, _followingUiState) + } + } + } + + private fun updateFollowUiState( + follow: Follow, + followUiState: MutableLiveData, + ) { + val currentState = followUiState.value + if (currentState is FollowUiState.Success) { + val updatedData = currentState.data.map { + if (it.memberId == follow.memberId) { + it.copy(isFollow = !it.isFollow) + } else { + it } } + + followUiState.value = FollowUiState.Success( + count = currentState.count, + data = updatedData + ) } } -} \ No newline at end of file +} + +sealed class FollowUiState { + object Loading : FollowUiState() + data class Success( + val count: Int = 0, + val data: List = emptyList() + ) : FollowUiState() + + object Error : FollowUiState() +} diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/HomeViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/HomeViewModel.kt index 1b253ea8..ef25d0fc 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/HomeViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/HomeViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import daily.dayo.presentation.common.Resource +import dagger.hilt.android.lifecycle.HiltViewModel import daily.dayo.domain.model.Category import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.model.Post @@ -14,8 +14,10 @@ import daily.dayo.domain.usecase.post.RequestDayoPickPostListCategoryUseCase import daily.dayo.domain.usecase.post.RequestDayoPickPostListUseCase import daily.dayo.domain.usecase.post.RequestNewPostListCategoryUseCase import daily.dayo.domain.usecase.post.RequestNewPostListUseCase -import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.presentation.common.Resource import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -28,8 +30,11 @@ class HomeViewModel @Inject constructor( private val requestLikePostUseCase: RequestLikePostUseCase, private val requestUnlikePostUseCase: RequestUnlikePostUseCase ) : ViewModel() { - var currentDayoPickCategory = Category.ALL - var currentNewCategory = Category.ALL + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing = _isRefreshing.asStateFlow() + + private val _currentCategory = MutableStateFlow(Category.ALL) + val currentCategory = _currentCategory.asStateFlow() private val _dayoPickPostList = MutableLiveData>>() val dayoPickPostList: LiveData>> get() = _dayoPickPostList @@ -37,36 +42,59 @@ class HomeViewModel @Inject constructor( private val _newPostList = MutableLiveData>>() val newPostList: LiveData>> get() = _newPostList - fun requestDayoPickPostList() = viewModelScope.launch { + fun setCategory(category: Category) { + viewModelScope.launch { + _currentCategory.emit(category) + } + } + + fun loadDayoPickPosts() { + viewModelScope.launch { + _isRefreshing.emit(true) + requestDayoPickPostList() + _isRefreshing.emit(false) + } + } + + fun loadNewPosts() { + viewModelScope.launch { + _isRefreshing.emit(true) + requestNewPostList() + _isRefreshing.emit(false) + } + } + + private fun requestDayoPickPostList() = viewModelScope.launch { _dayoPickPostList.postValue(Resource.loading(null)) - if (currentDayoPickCategory == Category.ALL) { - requestHomeDayoPickPostList() - } else { - requestHomeDayoPickPostListCategory(currentDayoPickCategory) + when (currentCategory.value) { + Category.ALL -> requestHomeDayoPickPostList() + else -> requestHomeDayoPickPostListCategory(currentCategory.value) } } - fun requestNewPostList() = viewModelScope.launch { + private fun requestNewPostList() = viewModelScope.launch { _newPostList.postValue(Resource.loading(null)) - if (currentNewCategory == Category.ALL) { - requestHomeNewPostList() - } else { - requestHomeNewPostListCategory(currentNewCategory) + when (currentCategory.value) { + Category.ALL -> requestHomeNewPostList() + else -> requestHomeNewPostListCategory(currentCategory.value) } } private fun requestHomeNewPostList() = viewModelScope.launch { - requestNewPostListUseCase()?.let { ApiResponse -> + requestNewPostListUseCase().let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { _newPostList.postValue(Resource.success(ApiResponse.body?.data)) } + is NetworkResponse.NetworkError -> { _newPostList.postValue(Resource.error(ApiResponse.exception.toString(), null)) } + is NetworkResponse.ApiError -> { _newPostList.postValue(Resource.error(ApiResponse.error.toString(), null)) } + is NetworkResponse.UnknownError -> { _newPostList.postValue(Resource.error(ApiResponse.throwable.toString(), null)) } @@ -80,12 +108,15 @@ class HomeViewModel @Inject constructor( is NetworkResponse.Success -> { _newPostList.postValue(Resource.success(ApiResponse.body?.data)) } + is NetworkResponse.NetworkError -> { _newPostList.postValue(Resource.error(ApiResponse.exception.toString(), null)) } + is NetworkResponse.ApiError -> { _newPostList.postValue(Resource.error(ApiResponse.error.toString(), null)) } + is NetworkResponse.UnknownError -> { _newPostList.postValue(Resource.error(ApiResponse.throwable.toString(), null)) } @@ -99,6 +130,7 @@ class HomeViewModel @Inject constructor( is NetworkResponse.Success -> { _dayoPickPostList.postValue(Resource.success(ApiResponse.body?.data)) } + is NetworkResponse.NetworkError -> { _dayoPickPostList.postValue( Resource.error( @@ -107,9 +139,11 @@ class HomeViewModel @Inject constructor( ) ) } + is NetworkResponse.ApiError -> { _dayoPickPostList.postValue(Resource.error(ApiResponse.error.toString(), null)) } + is NetworkResponse.UnknownError -> { _dayoPickPostList.postValue( Resource.error( @@ -128,6 +162,7 @@ class HomeViewModel @Inject constructor( is NetworkResponse.Success -> { _dayoPickPostList.postValue(Resource.success(ApiResponse.body?.data)) } + is NetworkResponse.NetworkError -> { _dayoPickPostList.postValue( Resource.error( @@ -136,9 +171,11 @@ class HomeViewModel @Inject constructor( ) ) } + is NetworkResponse.ApiError -> { _dayoPickPostList.postValue(Resource.error(ApiResponse.error.toString(), null)) } + is NetworkResponse.UnknownError -> { _dayoPickPostList.postValue( Resource.error( @@ -151,7 +188,7 @@ class HomeViewModel @Inject constructor( } } - fun requestLikePost(postId: Int, isDayoPickLike: Boolean) = + fun requestLikePost(postId: Long, isDayoPickLike: Boolean) = viewModelScope.launch(Dispatchers.IO) { val editList = if (isDayoPickLike) _dayoPickPostList else _newPostList requestLikePostUseCase(postId = postId).let { ApiResponse -> @@ -172,6 +209,7 @@ class HomeViewModel @Inject constructor( ) ) } + is NetworkResponse.NetworkError -> {} is NetworkResponse.ApiError -> {} is NetworkResponse.UnknownError -> {} @@ -179,7 +217,7 @@ class HomeViewModel @Inject constructor( } } - fun requestUnlikePost(postId: Int, isDayoPickLike: Boolean) = + fun requestUnlikePost(postId: Long, isDayoPickLike: Boolean) = viewModelScope.launch(Dispatchers.IO) { val editList = if (isDayoPickLike) _dayoPickPostList else _newPostList requestUnlikePostUseCase(postId = postId).let { ApiResponse -> @@ -200,6 +238,7 @@ class HomeViewModel @Inject constructor( ) ) } + is NetworkResponse.NetworkError -> {} is NetworkResponse.ApiError -> {} is NetworkResponse.UnknownError -> {} @@ -208,7 +247,7 @@ class HomeViewModel @Inject constructor( } fun setPostStatus( - postId: Int, + postId: Long, isLike: Boolean? = null, heartCount: Int? = null, commentCount: Int? = null diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/NoticeViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/NoticeViewModel.kt index 3405cc9d..d1c7ee5c 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/NoticeViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/NoticeViewModel.kt @@ -1,7 +1,5 @@ package daily.dayo.presentation.viewmodel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData @@ -11,7 +9,9 @@ import daily.dayo.domain.model.Notice import daily.dayo.domain.usecase.notice.RequestAllNoticeListUseCase import daily.dayo.domain.usecase.notice.RequestDetailNoticeUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.presentation.common.Resource import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -26,8 +26,15 @@ class NoticeViewModel @Inject constructor( private val _noticeList = MutableStateFlow>(PagingData.empty()) val noticeList = _noticeList.asStateFlow() - private val _detailNotice = MutableLiveData() - val detailNotice: LiveData get() = _detailNotice + private val _detailNotice = MutableStateFlow?>(null) + val detailNotice: StateFlow?> get() = _detailNotice + + private val _selectedNotice = MutableStateFlow(null) + val selectedNotice: StateFlow get() = _selectedNotice + + fun selectNotice(notice: Notice) { + _selectedNotice.value = notice + } fun requestAllNoticeList() = viewModelScope.launch { requestAllNoticeListUseCase() @@ -35,11 +42,11 @@ class NoticeViewModel @Inject constructor( .collectLatest { _noticeList.emit(it) } } - fun requestDetailNotice(noticeId: Int) = viewModelScope.launch { + fun requestDetailNotice(noticeId: Long) = viewModelScope.launch { requestDetailNoticeUseCase(noticeId).let { ApiResponse -> when (ApiResponse) { - is NetworkResponse.Success -> _detailNotice.postValue(ApiResponse.body?.contents.toString()) - else -> _detailNotice.postValue("") + is NetworkResponse.Success -> _detailNotice.emit(Resource.success(ApiResponse.body?.contents.toString())) + else -> _detailNotice.emit(Resource.error(ApiResponse.toString(), null)) } } } diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/NotificationViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/NotificationViewModel.kt index a4b3bfcf..a44be043 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/NotificationViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/NotificationViewModel.kt @@ -9,8 +9,11 @@ import androidx.paging.cachedIn import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.model.Notification import daily.dayo.domain.usecase.notification.RequestAllAlarmListUseCase -import daily.dayo.domain.usecase.notification.RequestIsCheckAlarmUseCase +import daily.dayo.domain.usecase.notification.MarkAlarmAsCheckedUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import javax.inject.Inject @@ -18,23 +21,34 @@ import javax.inject.Inject @HiltViewModel class NotificationViewModel @Inject constructor( private val requestAllAlarmListUseCase: RequestAllAlarmListUseCase, - private val requestIsCheckAlarmUseCase: RequestIsCheckAlarmUseCase + private val markAlarmAsCheckedUseCase: MarkAlarmAsCheckedUseCase ) : ViewModel() { - private val _alarmList = MutableLiveData>() - val alarmList: LiveData> get() = _alarmList + private val _alarmList = MutableStateFlow>(PagingData.empty()) + val alarmList: StateFlow> get() = _alarmList + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing = _isRefreshing.asStateFlow() private val _checkAlarmSuccess = MutableLiveData() val checkAlarmSuccess: LiveData get() = _checkAlarmSuccess + fun loadNotifications() { + viewModelScope.launch { + _isRefreshing.emit(true) + requestAllAlarmList() + _isRefreshing.emit(false) + } + } + fun requestAllAlarmList() = viewModelScope.launch { requestAllAlarmListUseCase() .cachedIn(viewModelScope) - .collectLatest { _alarmList.postValue(it) } + .collectLatest { _alarmList.emit(it) } } - fun requestIsCheckAlarm(alarmId: Int) = viewModelScope.launch { - requestIsCheckAlarmUseCase(alarmId).let { ApiResponse -> + fun markAlarmAsChecked(alarmId: Int) = viewModelScope.launch { + markAlarmAsCheckedUseCase(alarmId).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> _checkAlarmSuccess.postValue(true) else -> _checkAlarmSuccess.postValue(false) diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/PostViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/PostViewModel.kt index db673b87..107e7d8f 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/PostViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/PostViewModel.kt @@ -6,31 +6,37 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn -import daily.dayo.presentation.common.Event -import daily.dayo.presentation.common.Resource -import daily.dayo.domain.model.BookmarkPostResponse +import dagger.hilt.android.lifecycle.HiltViewModel import daily.dayo.domain.model.Comment -import daily.dayo.domain.model.LikePostResponse +import daily.dayo.domain.model.Comments import daily.dayo.domain.model.LikeUser +import daily.dayo.domain.model.MentionUser import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.model.PostDetail +import daily.dayo.domain.model.SearchUser import daily.dayo.domain.usecase.block.RequestBlockMemberUseCase import daily.dayo.domain.usecase.bookmark.RequestBookmarkPostUseCase import daily.dayo.domain.usecase.bookmark.RequestDeleteBookmarkPostUseCase +import daily.dayo.domain.usecase.comment.RequestCreatePostCommentReplyUseCase import daily.dayo.domain.usecase.comment.RequestCreatePostCommentUseCase import daily.dayo.domain.usecase.comment.RequestDeletePostCommentUseCase import daily.dayo.domain.usecase.comment.RequestPostCommentUseCase import daily.dayo.domain.usecase.like.RequestLikePostUseCase import daily.dayo.domain.usecase.like.RequestPostLikeUsersUseCase import daily.dayo.domain.usecase.like.RequestUnlikePostUseCase +import daily.dayo.domain.usecase.member.RequestCurrentUserInfoUseCase import daily.dayo.domain.usecase.post.RequestDeletePostUseCase import daily.dayo.domain.usecase.post.RequestPostDetailUseCase -import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.presentation.common.Event +import daily.dayo.presentation.common.Resource import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import java.util.regex.Pattern import javax.inject.Inject @HiltViewModel @@ -43,19 +49,21 @@ class PostViewModel @Inject constructor( private val requestDeleteBookmarkPostUseCase: RequestDeleteBookmarkPostUseCase, private val requestPostCommentUseCase: RequestPostCommentUseCase, private val requestCreatePostCommentUseCase: RequestCreatePostCommentUseCase, + private val requestCreatePostCommentReplyUseCase: RequestCreatePostCommentReplyUseCase, private val requestDeletePostCommentUseCase: RequestDeletePostCommentUseCase, private val requestBlockMemberUseCase: RequestBlockMemberUseCase, - private val requestPostLikeUsersUseCase: RequestPostLikeUsersUseCase + private val requestPostLikeUsersUseCase: RequestPostLikeUsersUseCase, + private val requestCurrentUserInfoUseCase: RequestCurrentUserInfoUseCase ) : ViewModel() { private val _postDetail = MutableLiveData>() val postDetail: LiveData> get() = _postDetail - private val _postLiked = MutableLiveData>() - val postLiked: LiveData> get() = _postLiked + private val _postComments = MutableLiveData>() + val postComments: LiveData> = _postComments - private val _postBookmarked = MutableLiveData>() - val postBookmarked: LiveData> get() = _postBookmarked + private val _postDeleteSuccess = MutableSharedFlow() + val postDeleteSuccess = _postDeleteSuccess.asSharedFlow() private val _postCommentCreateSuccess = MutableLiveData>() val postCommentCreateSuccess get() = _postCommentCreateSuccess @@ -63,118 +71,221 @@ class PostViewModel @Inject constructor( private val _postCommentDeleteSuccess = MutableLiveData>() val postCommentDeleteSuccess get() = _postCommentDeleteSuccess - private val _postComment = MutableLiveData>>() - val postComment: LiveData>> get() = _postComment - private val _blockSuccess = MutableLiveData>() val blockSuccess: LiveData> get() = _blockSuccess private val _postLikeUsers = MutableStateFlow>(PagingData.empty()) val postLikeUsers = _postLikeUsers.asStateFlow() + val postLikeCountUiState: MutableStateFlow = MutableStateFlow(0) + + fun getCurrentUserInfo() = requestCurrentUserInfoUseCase() + fun cleanUpPostDetail() { _postDetail.postValue(Resource.loading(null)) - _postComment.postValue(Resource.loading(null)) + _postComments.postValue(Resource.loading(null)) } - fun requestPostDetail(postId: Int) = viewModelScope.launch { - _postDetail.postValue(Resource.loading(null)) - requestPostDetailUseCase(postId)?.let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { _postDetail.postValue(Resource.success(ApiResponse.body)) } - is NetworkResponse.NetworkError -> { _postDetail.postValue(Resource.error(ApiResponse.exception.toString(), null)) } - is NetworkResponse.ApiError -> { _postDetail.postValue(Resource.error(ApiResponse.error.toString(), null)) } - is NetworkResponse.UnknownError -> { _postDetail.postValue(Resource.error(ApiResponse.throwable.toString(), null)) } + fun requestPostDetail(postId: Long) { + viewModelScope.launch { + _postDetail.postValue(Resource.loading(null)) + requestPostDetailUseCase(postId).let { response -> + when (response) { + is NetworkResponse.Success -> { + _postDetail.postValue(Resource.success(response.body)) + postLikeCountUiState.value = response.body?.heartCount ?: 0 + } + + is NetworkResponse.NetworkError -> { + _postDetail.postValue(Resource.error(response.exception.toString(), null)) + } + + is NetworkResponse.ApiError -> { + _postDetail.postValue(Resource.error(response.error.toString(), null)) + } + + is NetworkResponse.UnknownError -> { + _postDetail.postValue(Resource.error(response.throwable.toString(), null)) + } + } } } } - fun requestDeletePost(postId: Int) = viewModelScope.launch { - requestDeletePostUseCase(postId) + fun requestDeletePost(postId: Long) { + viewModelScope.launch { + requestDeletePostUseCase(postId).let { response -> + when (response) { + is NetworkResponse.Success -> _postDeleteSuccess.emit(true) + else -> _postDeleteSuccess.emit(false) + } + } + } } - fun requestLikePost(postId: Int) = viewModelScope.launch { - requestLikePostUseCase(postId = postId)?.let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _postDetail.postValue( - Resource.success( - _postDetail.value?.data?.apply { - heart = true - heartCount = ApiResponse.body?.allCount?: 0 - } + fun toggleLikePost(postId: Long, currentHeart: Boolean) { + viewModelScope.launch { + if (currentHeart) { + requestUnlikePostUseCase(postId = postId) + } else { + requestLikePostUseCase(postId = postId) + }.let { response -> + when (response) { + is NetworkResponse.Success -> { + _postDetail.postValue( + Resource.success( + _postDetail.value?.data?.copy( + heart = !currentHeart, + heartCount = response.body?.allCount ?: 0 + ) + ) ) - ) + } + + is NetworkResponse.NetworkError -> { + _postDetail.postValue(Resource.error(response.exception.toString(), null)) + } + + is NetworkResponse.ApiError -> { + _postDetail.postValue(Resource.error(response.error.toString(), null)) + } + + is NetworkResponse.UnknownError -> { + _postDetail.postValue(Resource.error(response.throwable.toString(), null)) + } } - is NetworkResponse.NetworkError -> { _postLiked.postValue(Resource.error(ApiResponse.exception.toString(), null)) } - is NetworkResponse.ApiError -> { _postLiked.postValue(Resource.error(ApiResponse.error.toString(), null)) } - is NetworkResponse.UnknownError -> { _postLiked.postValue(Resource.error(ApiResponse.throwable.toString(), null)) } } } } - fun requestUnlikePost(postId: Int) = viewModelScope.launch { - requestUnlikePostUseCase(postId).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _postDetail.postValue( - Resource.success( - _postDetail.value?.data?.apply { - heart = false - heartCount = ApiResponse.body?.allCount?: 0 - } - ) - ) + fun toggleBookmarkPostDetail(postId: Long, currentBookmark: Boolean?) { + viewModelScope.launch { + currentBookmark?.let { bookmark -> + if (bookmark) { + requestDeleteBookmarkPostUseCase(postId = postId) + } else { + requestBookmarkPostUseCase(postId = postId) + }.let { response -> + when (response) { + is NetworkResponse.Success -> { + _postDetail.postValue( + Resource.success( + _postDetail.value?.data?.copy( + bookmark = !currentBookmark + ) + ) + ) + } + + is NetworkResponse.NetworkError -> { + _postDetail.postValue(Resource.error(response.exception.toString(), null)) + } + + is NetworkResponse.ApiError -> { + _postDetail.postValue(Resource.error(response.error.toString(), null)) + } + + is NetworkResponse.UnknownError -> { + _postDetail.postValue(Resource.error(response.throwable.toString(), null)) + } + } } - is NetworkResponse.NetworkError -> {} - is NetworkResponse.ApiError -> {} - is NetworkResponse.UnknownError -> {} } - } } - fun requestBookmarkPost(postId: Int) = viewModelScope.launch { - requestBookmarkPostUseCase(postId = postId).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { _postBookmarked.postValue(Resource.success(ApiResponse.body)) } - is NetworkResponse.NetworkError -> { _postBookmarked.postValue(Resource.error(ApiResponse.exception.toString(), null)) } - is NetworkResponse.ApiError -> { _postBookmarked.postValue(Resource.error(ApiResponse.error.toString(), null)) } - is NetworkResponse.UnknownError -> { _postBookmarked.postValue(Resource.error(ApiResponse.throwable.toString(), null)) } + fun requestPostComment(postId: Long) { + viewModelScope.launch { + _postComments.postValue(Resource.loading(null)) + requestPostCommentUseCase(postId).let { response -> + when (response) { + is NetworkResponse.Success -> { + _postComments.postValue(Resource.success(response.body)) + } + + is NetworkResponse.NetworkError -> { + _postComments.postValue(Resource.error(response.exception.toString(), null)) + } + + is NetworkResponse.ApiError -> { + _postComments.postValue(Resource.error(response.error.toString(), null)) + } + + is NetworkResponse.UnknownError -> { + _postComments.postValue(Resource.error(response.throwable.toString(), null)) + } + } } } } - fun requestDeleteBookmarkPost(postId: Int) = viewModelScope.launch { - requestDeleteBookmarkPostUseCase(postId) + private fun getMentionList(contents: String, mentionedUser: List): List { + val pattern = Pattern.compile("@\\w+") + val matcher = pattern.matcher(contents) + val usernames = mutableListOf() + while (matcher.find()) { + usernames.add(matcher.group()) + } + return mentionedUser.filter { user -> + usernames.any { + user.nickname == it.drop(1) + } + }.map { + MentionUser( + memberId = it.memberId, + nickname = it.nickname + ) + } } - fun requestPostComment(postId: Int) = viewModelScope.launch { - _postComment.postValue(Resource.loading(null)) - requestPostCommentUseCase(postId)?.let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { _postComment.postValue(Resource.success(ApiResponse.body?.data)) } - is NetworkResponse.NetworkError -> { _postComment.postValue(Resource.error(ApiResponse.exception.toString(), null)) } - is NetworkResponse.ApiError -> { _postComment.postValue(Resource.error(ApiResponse.error.toString(), null)) } - is NetworkResponse.UnknownError -> { _postComment.postValue(Resource.error(ApiResponse.throwable.toString(), null)) } + fun requestCreatePostComment(contents: String, postId: Long, mentionedUser: List) { + if (contents.isEmpty()) return + viewModelScope.launch { + val mentionList = getMentionList(contents, mentionedUser) + requestCreatePostCommentUseCase(contents = contents, postId = postId, mentionList = mentionList).let { response -> + when (response) { + is NetworkResponse.Success -> { + _postCommentCreateSuccess.postValue(Event(true)) + } + + else -> { + _postCommentCreateSuccess.postValue(Event(false)) + } + } } } } - fun requestCreatePostComment(contents: String, postId: Int) = viewModelScope.launch { - requestCreatePostCommentUseCase(contents = contents, postId = postId)?.let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { _postCommentCreateSuccess.postValue(Event(true)) } - else -> { _postCommentCreateSuccess.postValue(Event(false)) } + fun requestCreatePostCommentReply(reply: Pair, contents: String, postId: Long, mentionedUser: List) { + if (contents.isEmpty()) return + viewModelScope.launch { + val mentionList = getMentionList(contents, mentionedUser).toMutableList() + val (parentCommentId, comment) = reply + mentionList.add(MentionUser(comment.memberId, comment.nickname)) // ์–ธ๊ธ‰๋œ ์œ ์ € ๋ฆฌ์ŠคํŠธ์— ์›๋ณธ ๋Œ“๊ธ€ ์œ ์ € ์ถ”๊ฐ€ (ํŒ”๋กœ์šฐํ•˜์ง€ ์•Š์•„๋„ ๋‹ต๊ธ€ ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ๋”ฐ๋กœ ์ถ”๊ฐ€) + requestCreatePostCommentReplyUseCase(commentId = parentCommentId, contents = contents, postId = postId, mentionList = mentionList).let { response -> + when (response) { + is NetworkResponse.Success -> { + _postCommentCreateSuccess.postValue(Event(true)) + } + + else -> { + _postCommentCreateSuccess.postValue(Event(false)) + } + } } } } - fun requestDeletePostComment(commentId: Int) = viewModelScope.launch { + fun requestDeletePostComment(commentId: Long) = viewModelScope.launch { requestDeletePostCommentUseCase(commentId)?.let { ApiResponse -> when (ApiResponse) { - is NetworkResponse.Success -> { _postCommentDeleteSuccess.postValue(Event(true)) } - else -> { _postCommentDeleteSuccess.postValue(Event(false)) } + is NetworkResponse.Success -> { + _postCommentDeleteSuccess.postValue(Event(true)) + } + + else -> { + _postCommentDeleteSuccess.postValue(Event(false)) + } } } } @@ -182,13 +293,18 @@ class PostViewModel @Inject constructor( fun requestBlockMember(memberId: String) = viewModelScope.launch { requestBlockMemberUseCase(memberId)?.let { ApiResponse -> when (ApiResponse) { - is NetworkResponse.Success -> { _blockSuccess.postValue(Event(true)) } - else -> { _blockSuccess.postValue(Event(false)) } + is NetworkResponse.Success -> { + _blockSuccess.postValue(Event(true)) + } + + else -> { + _blockSuccess.postValue(Event(false)) + } } } } - fun requestPostLikeUsers(postId: Int) = viewModelScope.launch(Dispatchers.IO) { + fun requestPostLikeUsers(postId: Long) = viewModelScope.launch(Dispatchers.IO) { requestPostLikeUsersUseCase(postId = postId) .cachedIn(viewModelScope) .collectLatest { _postLikeUsers.emit(it) } diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileSettingViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileSettingViewModel.kt index 7e4de8a2..40c50f95 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileSettingViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileSettingViewModel.kt @@ -1,42 +1,54 @@ package daily.dayo.presentation.viewmodel +import android.graphics.Bitmap import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import daily.dayo.presentation.common.Event -import daily.dayo.presentation.common.Resource +import dagger.hilt.android.lifecycle.HiltViewModel import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.model.Profile import daily.dayo.domain.model.UserBlocked import daily.dayo.domain.usecase.block.RequestBlockListUseCase import daily.dayo.domain.usecase.member.RequestCheckNicknameDuplicateUseCase -import daily.dayo.domain.usecase.member.RequestOtherProfileUseCase +import daily.dayo.domain.usecase.member.RequestMyProfileUseCase import daily.dayo.domain.usecase.member.RequestUpdateMyProfileUseCase -import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.presentation.common.Event +import daily.dayo.presentation.common.Resource +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.common.image.ImageResizeUtil.cropCenterBitmap +import daily.dayo.presentation.common.toFile +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import java.io.File import javax.inject.Inject @HiltViewModel class ProfileSettingViewModel @Inject constructor( - private val requestOtherProfileUseCase: RequestOtherProfileUseCase, + private val requestMyProfileUseCase: RequestMyProfileUseCase, private val requestUpdateMyProfileUseCase: RequestUpdateMyProfileUseCase, private val requestBlockListUseCase: RequestBlockListUseCase, private val requestCheckNicknameDuplicateUseCase: RequestCheckNicknameDuplicateUseCase ) : ViewModel() { - private val _profileInfo = MutableLiveData() - val profileInfo: LiveData get() = _profileInfo + private val _profileInfo = MutableLiveData>() + val profileInfo: LiveData> = _profileInfo - private val _updateSuccess = MutableLiveData>() - val updateSuccess: LiveData> get() = _updateSuccess + private val _updateSuccess = MutableSharedFlow() + val updateSuccess = _updateSuccess.asSharedFlow() - private val _blockList = MutableLiveData>>() - val blockList: LiveData>> get() = _blockList + private val _isUpdateSuccess = MutableStateFlow(null) + val isUpdateSuccess: StateFlow get() = _isUpdateSuccess - private val _isNicknameDuplicate = MutableLiveData() - val isNicknameDuplicate: LiveData get() = _isNicknameDuplicate + private val _blockList = + MutableStateFlow>>(Resource.loading(emptyList())) + val blockList: StateFlow>> get() = _blockList + + private val _isNicknameDuplicate = MutableSharedFlow() + val isNicknameDuplicate = _isNicknameDuplicate.asSharedFlow() private val _isErrorExceptionOccurred = MutableLiveData>() val isErrorExceptionOccurred get() = _isErrorExceptionOccurred @@ -44,86 +56,117 @@ class ProfileSettingViewModel @Inject constructor( private val _isApiErrorExceptionOccurred = MutableLiveData>() val isApiErrorExceptionOccurred get() = _isApiErrorExceptionOccurred - fun requestProfile(memberId: String) = viewModelScope.launch { - requestOtherProfileUseCase(memberId = memberId).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - ApiResponse.body?.let { profile -> - _profileInfo.postValue(profile) - } - } - is NetworkResponse.NetworkError -> { - - } - is NetworkResponse.ApiError -> { + init { + requestMyProfile() + } - } - is NetworkResponse.UnknownError -> { + fun requestUpdateMyProfile( + nickname: String, + profileImg: File?, + isReset: Boolean = false + ) { + viewModelScope.launch { + val response = requestUpdateMyProfileUseCase( + nickname = nickname, + profileImg = profileImg, + onBasicProfileImg = isReset + ) - } + when (response) { + is NetworkResponse.Success -> _updateSuccess.emit(true) + else -> _updateSuccess.emit(false) } } } - fun requestUpdateMyProfile(nickname: String?, profileImg: File?, isReset: Boolean) = + fun requestUpdateMyProfileWithResizedFile( + nickname: String, + profileImg: Bitmap? = null, + profileImgTempDir: String? = null, + isReset: Boolean = false + ) { viewModelScope.launch { - requestUpdateMyProfileUseCase( + _isUpdateSuccess.emit(Status.LOADING) + val resizedImage = profileImg?.let { selectedImage -> + if (profileImgTempDir != null) { + return@let selectedImage.cropCenterBitmap().toFile(profileImgTempDir) + } else { + return@let null + } + } + + val response = requestUpdateMyProfileUseCase( nickname = nickname, - profileImg = profileImg, + profileImg = resizedImage, onBasicProfileImg = isReset - ).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _updateSuccess.postValue(Event(true)) - } - is NetworkResponse.NetworkError -> { - _updateSuccess.postValue(Event(false)) - } - is NetworkResponse.ApiError -> { - _updateSuccess.postValue(Event(false)) - } - is NetworkResponse.UnknownError -> { - _updateSuccess.postValue(Event(false)) - } - } + ) + + when (response) { + is NetworkResponse.Success -> _isUpdateSuccess.emit(Status.SUCCESS) + else -> _isUpdateSuccess.emit(Status.ERROR) } } + } fun requestBlockList() = viewModelScope.launch { - requestBlockListUseCase().let { ApiResponse -> - when (ApiResponse) { + val currentData = _blockList.value.data + _blockList.emit(Resource.loading(currentData)) + + requestBlockListUseCase().let { response -> + when (response) { is NetworkResponse.Success -> { - _blockList.postValue(Resource.success(ApiResponse.body?.data)) + _blockList.emit(Resource.success(response.body?.data)) } + is NetworkResponse.NetworkError -> { - _blockList.postValue(Resource.error(ApiResponse.exception.toString(), null)) + _blockList.emit(Resource.error(response.exception.toString(), null)) } + is NetworkResponse.ApiError -> { - _blockList.postValue(Resource.error(ApiResponse.error.toString(), null)) + _blockList.emit(Resource.error(response.error.toString(), null)) } + is NetworkResponse.UnknownError -> { - _blockList.postValue(Resource.error(ApiResponse.throwable.toString(), null)) + _blockList.emit(Resource.error(response.throwable.toString(), null)) } } } } - fun requestCheckNicknameDuplicate(nickname: String) = viewModelScope.launch { - requestCheckNicknameDuplicateUseCase(nickname).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _isNicknameDuplicate.postValue(true) - } - is NetworkResponse.NetworkError -> { - _isErrorExceptionOccurred.postValue(Event(true)) - _isNicknameDuplicate.postValue(false) + fun requestCheckNicknameDuplicate(nickname: String) { + if (nickname == profileInfo.value?.data?.nickname) return + viewModelScope.launch { + requestCheckNicknameDuplicateUseCase(nickname).let { response -> + when (response) { + is NetworkResponse.Success -> { + _isNicknameDuplicate.emit(false) + } + + is NetworkResponse.ApiError -> { + _isNicknameDuplicate.emit(true) + } + + else -> { + _isErrorExceptionOccurred.postValue(Event(true)) + } } - is NetworkResponse.ApiError -> { - _isApiErrorExceptionOccurred.postValue(Event(true)) - _isNicknameDuplicate.postValue(false) + } + } + } + + private fun requestMyProfile() { + viewModelScope.launch { + requestMyProfileUseCase().let { response -> + when (response) { + is NetworkResponse.Success -> { + _profileInfo.postValue(Resource.success(response.body)) + } + + else -> { + _isErrorExceptionOccurred.postValue(Event(true)) + } } - else -> {} } } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileViewModel.kt index 11fe58fa..3601d84e 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/ProfileViewModel.kt @@ -6,26 +6,36 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn -import daily.dayo.presentation.common.Event -import daily.dayo.presentation.common.Resource -import daily.dayo.domain.model.* +import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.domain.model.Folder +import daily.dayo.domain.model.LikePost +import daily.dayo.domain.model.NetworkResponse +import daily.dayo.domain.model.Profile import daily.dayo.domain.usecase.block.RequestBlockMemberUseCase import daily.dayo.domain.usecase.block.RequestUnblockMemberUseCase -import daily.dayo.domain.usecase.bookmark.RequestAllMyBookmarkPostListUseCase import daily.dayo.domain.usecase.folder.RequestAllFolderListUseCase import daily.dayo.domain.usecase.folder.RequestAllMyFolderListUseCase import daily.dayo.domain.usecase.follow.RequestCreateFollowUseCase import daily.dayo.domain.usecase.follow.RequestDeleteFollowUseCase import daily.dayo.domain.usecase.like.RequestAllMyLikePostListUseCase +import daily.dayo.domain.usecase.member.RequestCurrentUserInfoUseCase import daily.dayo.domain.usecase.member.RequestMyProfileUseCase import daily.dayo.domain.usecase.member.RequestOtherProfileUseCase -import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.presentation.common.Event +import daily.dayo.presentation.common.Resource +import daily.dayo.presentation.common.Status +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class ProfileViewModel @Inject constructor( + private val requestCurrentUserInfoUseCase: RequestCurrentUserInfoUseCase, private val requestAllFolderListUseCase: RequestAllFolderListUseCase, private val requestAllMyFolderListUseCase: RequestAllMyFolderListUseCase, private val requestMyProfileUseCase: RequestMyProfileUseCase, @@ -33,36 +43,35 @@ class ProfileViewModel @Inject constructor( private val requestCreateFollowUseCase: RequestCreateFollowUseCase, private val requestDeleteFollowUseCase: RequestDeleteFollowUseCase, private val requestAllMyLikePostListUseCase: RequestAllMyLikePostListUseCase, - private val requestAllMyBookmarkPostListUseCase: RequestAllMyBookmarkPostListUseCase, private val requestBlockMemberUseCase: RequestBlockMemberUseCase, private val requestUnblockMemberUseCase: RequestUnblockMemberUseCase ) : ViewModel() { - lateinit var profileMemberId: String + val currentMemberId get() = requestCurrentUserInfoUseCase().memberId private val _profileInfo = MutableLiveData>() - val profileInfo: LiveData> get() = _profileInfo + val profileInfo: LiveData> = _profileInfo - private val _followSuccess = MutableLiveData>() - val followSuccess: LiveData> get() = _followSuccess + private val _followSuccess = MutableSharedFlow() + val followSuccess = _followSuccess.asSharedFlow() - private val _unfollowSuccess = MutableLiveData>() - val unfollowSuccess: LiveData> get() = _unfollowSuccess + private val _unfollowSuccess = MutableSharedFlow() + val unfollowSuccess = _unfollowSuccess.asSharedFlow() private val _folderList = MutableLiveData>>() val folderList: LiveData>> get() = _folderList + private val _folders = MutableStateFlow>>(Resource.loading(null)) + val folders get() = _folders.asStateFlow() + private val _likePostList = MutableLiveData>() val likePostList: LiveData> get() = _likePostList - private val _bookmarkPostList = MutableLiveData>() - val bookmarkPostList: LiveData> get() = _bookmarkPostList - - private val _blockSuccess = MutableLiveData>() - val blockSuccess: LiveData> get() = _blockSuccess + private val _blockSuccess = MutableStateFlow(null) + val blockSuccess: StateFlow get() = _blockSuccess - private val _unblockSuccess = MutableLiveData>() - val unblockSuccess: LiveData> get() = _unblockSuccess + private val _unblockSuccess = MutableStateFlow(null) + val unblockSuccess: StateFlow get() = _unblockSuccess fun requestMyProfile() = viewModelScope.launch { requestMyProfileUseCase().let { ApiResponse -> @@ -86,61 +95,71 @@ class ProfileViewModel @Inject constructor( } } - fun requestOtherProfile(memberId: String) = viewModelScope.launch { - requestOtherProfileUseCase(memberId = memberId).let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _profileInfo.postValue(Resource.success(ApiResponse.body)) - } - - is NetworkResponse.NetworkError -> { - } + fun requestOtherProfile(memberId: String) { + viewModelScope.launch { + requestOtherProfileUseCase(memberId = memberId).let { apiResponse -> + when (apiResponse) { + is NetworkResponse.Success -> { + _profileInfo.value = Resource.success(apiResponse.body) + } - is NetworkResponse.ApiError -> { + else -> _profileInfo.value = Resource.error(apiResponse.toString(), null) } + } + } + } - is NetworkResponse.UnknownError -> { + private fun requestFollow(followerId: String) { + viewModelScope.launch { + requestCreateFollowUseCase(followerId = followerId).let { response -> + when (response) { + is NetworkResponse.Success -> { + _followSuccess.emit(true) + } + else -> { + _followSuccess.emit(false) + } } } } } - fun requestCreateFollow(followerId: String) = viewModelScope.launch { - requestCreateFollowUseCase(followerId = followerId)?.let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _followSuccess.postValue(Event(true)) - } + private fun requestUnfollow(followerId: String) { + viewModelScope.launch { + requestDeleteFollowUseCase(followerId).let { response -> + when (response) { + is NetworkResponse.Success -> { + _unfollowSuccess.emit(true) + } - else -> { - _followSuccess.postValue(Event(false)) + else -> { + _unfollowSuccess.emit(false) + } } } } } - fun requestDeleteFollow(followerId: String) = viewModelScope.launch { - requestDeleteFollowUseCase(followerId = followerId)?.let { ApiResponse -> - when (ApiResponse) { - is NetworkResponse.Success -> { - _unfollowSuccess.postValue(Event(true)) - } - - else -> { - _unfollowSuccess.postValue(Event(false)) - } + fun toggleFollow(memberId: String, isFollow: Boolean) { + viewModelScope.launch { + if (isFollow) { + requestUnfollow(memberId) + } else { + requestFollow(memberId) } } } fun requestFolderList(memberId: String, isMine: Boolean) = viewModelScope.launch { _folderList.postValue(Resource.loading(null)) + _folders.emit(Resource.loading(null)) if (isMine) { requestAllMyFolderListUseCase()?.let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { _folderList.postValue(Resource.success(ApiResponse.body?.data)) + _folders.emit(Resource.success(ApiResponse.body?.data)) } is NetworkResponse.NetworkError -> { @@ -150,10 +169,17 @@ class ProfileViewModel @Inject constructor( null ) ) + _folders.emit( + Resource.error( + ApiResponse.exception.toString(), + null + ) + ) } is NetworkResponse.ApiError -> { _folderList.postValue(Resource.error(ApiResponse.error.toString(), null)) + _folders.emit(Resource.error(ApiResponse.error.toString(), null)) } is NetworkResponse.UnknownError -> { @@ -163,6 +189,12 @@ class ProfileViewModel @Inject constructor( null ) ) + _folders.emit( + Resource.error( + ApiResponse.throwable.toString(), + null + ) + ) } } } @@ -171,6 +203,7 @@ class ProfileViewModel @Inject constructor( when (ApiResponse) { is NetworkResponse.Success -> { _folderList.postValue(Resource.success(ApiResponse.body?.data)) + _folders.emit(Resource.success(ApiResponse.body?.data)) } is NetworkResponse.NetworkError -> { @@ -180,10 +213,17 @@ class ProfileViewModel @Inject constructor( null ) ) + _folders.emit( + Resource.error( + ApiResponse.exception.toString(), + null + ) + ) } is NetworkResponse.ApiError -> { _folderList.postValue(Resource.error(ApiResponse.error.toString(), null)) + _folders.emit(Resource.error(ApiResponse.error.toString(), null)) } is NetworkResponse.UnknownError -> { @@ -193,6 +233,12 @@ class ProfileViewModel @Inject constructor( null ) ) + _folders.emit( + Resource.error( + ApiResponse.throwable.toString(), + null + ) + ) } } } @@ -205,58 +251,57 @@ class ProfileViewModel @Inject constructor( .collectLatest { _likePostList.postValue(it) } } - fun requestAllMyBookmarkPostList() = viewModelScope.launch { - requestAllMyBookmarkPostListUseCase() - .cachedIn(viewModelScope) - .collectLatest { _bookmarkPostList.postValue(it) } - } - fun requestBlockMember(memberId: String) = viewModelScope.launch { + _blockSuccess.emit(Status.LOADING) requestBlockMemberUseCase(memberId)?.let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _blockSuccess.postValue(Event(true)) + _blockSuccess.emit(Status.SUCCESS) } is NetworkResponse.NetworkError -> { - _blockSuccess.postValue(Event(false)) + _blockSuccess.emit(Status.ERROR) } is NetworkResponse.ApiError -> { - _blockSuccess.postValue(Event(false)) + _blockSuccess.emit(Status.ERROR) } is NetworkResponse.UnknownError -> { - _blockSuccess.postValue(Event(false)) + _blockSuccess.emit(Status.ERROR) } } } } fun requestUnblockMember(memberId: String) = viewModelScope.launch { + _unblockSuccess.emit(Status.LOADING) requestUnblockMemberUseCase(memberId).let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { - _unblockSuccess.postValue(Event(true)) + _unblockSuccess.emit(Status.SUCCESS) } is NetworkResponse.NetworkError -> { - _unblockSuccess.postValue(Event(false)) + _unblockSuccess.emit(Status.ERROR) } is NetworkResponse.ApiError -> { - _unblockSuccess.postValue(Event(false)) + _unblockSuccess.emit(Status.ERROR) } is NetworkResponse.UnknownError -> { - _unblockSuccess.postValue(Event(false)) + _unblockSuccess.emit(Status.ERROR) } } } } fun cleanUpFolders() { - _profileInfo.postValue(Resource.loading(null)) - _folderList.postValue(Resource.loading(null)) + viewModelScope.launch { + _profileInfo.postValue(Resource.loading(null)) + _folderList.postValue(Resource.loading(null)) + _folders.emit(Resource.loading(null)) + } } } \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/ReportViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/ReportViewModel.kt index f863ef06..db81cae6 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/ReportViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/ReportViewModel.kt @@ -8,13 +8,15 @@ import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.usecase.report.RequestSaveMemberReportUseCase import daily.dayo.domain.usecase.report.RequestSavePostReportUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.domain.usecase.report.RequestSaveCommentReportUseCase import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class ReportViewModel @Inject constructor( private val requestSaveMemberReportUseCase: RequestSaveMemberReportUseCase, - private val requestSavePostReportUseCase: RequestSavePostReportUseCase + private val requestSavePostReportUseCase: RequestSavePostReportUseCase, + private val requestSaveCommentReportUseCase: RequestSaveCommentReportUseCase ) : ViewModel() { private val _reportMemberSuccess = MutableLiveData() @@ -23,6 +25,9 @@ class ReportViewModel @Inject constructor( private val _reportPostSuccess = MutableLiveData() val reportPostSuccess: LiveData get() = _reportPostSuccess + private val _reportCommentSuccess = MutableLiveData() + val reportCommentSuccess: LiveData get() = _reportCommentSuccess + fun requestSaveMemberReport(comment: String, memberId: String) = viewModelScope.launch { requestSaveMemberReportUseCase(comment = comment, memberId = memberId)?.let { ApiResponse -> when (ApiResponse) { @@ -32,7 +37,7 @@ class ReportViewModel @Inject constructor( } } - fun requestSavePostReport(comment: String, postId: Int) = viewModelScope.launch { + fun requestSavePostReport(comment: String, postId: Long) = viewModelScope.launch { requestSavePostReportUseCase(comment = comment, postId = postId)?.let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { _reportPostSuccess.postValue(true) } @@ -40,4 +45,13 @@ class ReportViewModel @Inject constructor( } } } + + fun requestSaveCommentReport(comment: String, commentId: Long) = viewModelScope.launch { + requestSaveCommentReportUseCase(comment = comment, commentId = commentId).let { ApiResponse -> + when (ApiResponse) { + is NetworkResponse.Success -> { _reportCommentSuccess.postValue(true) } + else -> { _reportCommentSuccess.postValue(false) } + } + } + } } diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/SearchViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/SearchViewModel.kt index d70d0692..d52aad62 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/SearchViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/SearchViewModel.kt @@ -1,14 +1,24 @@ package daily.dayo.presentation.viewmodel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn -import daily.dayo.domain.model.Search -import daily.dayo.domain.usecase.search.* import dagger.hilt.android.lifecycle.HiltViewModel +import daily.dayo.domain.model.Search +import daily.dayo.domain.model.SearchHistory +import daily.dayo.domain.model.SearchHistoryType +import daily.dayo.domain.model.SearchUser +import daily.dayo.domain.usecase.search.ClearSearchKeywordRecentUseCase +import daily.dayo.domain.usecase.search.DeleteSearchKeywordRecentUseCase +import daily.dayo.domain.usecase.search.RequestSearchFollowUserUseCase +import daily.dayo.domain.usecase.search.RequestSearchKeywordRecentUseCase +import daily.dayo.domain.usecase.search.RequestSearchKeywordUserUseCase +import daily.dayo.domain.usecase.search.RequestSearchTagUseCase +import daily.dayo.domain.usecase.search.RequestSearchTotalCountUseCase +import daily.dayo.domain.usecase.search.UpdateSearchKeywordRecentUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import javax.inject.Inject @@ -18,32 +28,103 @@ class SearchViewModel @Inject constructor( private val deleteSearchKeywordRecentUseCase: DeleteSearchKeywordRecentUseCase, private val clearSearchKeywordRecentUseCase: ClearSearchKeywordRecentUseCase, private val requestSearchKeywordRecentUseCase: RequestSearchKeywordRecentUseCase, - private val requestSearchKeywordUseCase: RequestSearchKeywordUseCase, + private val updateSearchKeywordRecentUseCase: UpdateSearchKeywordRecentUseCase, + private val requestSearchTagUseCase: RequestSearchTagUseCase, + private val requestSearchUserUseCase: RequestSearchKeywordUserUseCase, + private val requestSearchFollowUserUseCase: RequestSearchFollowUserUseCase, private val requestSearchTotalCountUseCase: RequestSearchTotalCountUseCase ) : ViewModel() { var searchKeyword = "" - private val _searchTotalCount = MutableLiveData(0) - val searchTotalCount: LiveData get() = _searchTotalCount + private val _searchTagTotalCount = MutableStateFlow(0) + val searchTagTotalCount get() = _searchTagTotalCount + + private val _searchUserTotalCount = MutableStateFlow(0) + val searchUserTotalCount get() = _searchUserTotalCount + + private val _searchTagList = MutableStateFlow>(PagingData.empty()) + val searchTagList get() = _searchTagList.asStateFlow() + + private val _searchUserList = MutableStateFlow>(PagingData.empty()) + val searchUserList get() = _searchUserList.asStateFlow() + + private val _searchFollowUserList = MutableStateFlow>(PagingData.empty()) + val searchFollowUserList get() = _searchFollowUserList.asStateFlow() + + private val _searchHistory = MutableStateFlow(SearchHistory(0, emptyList())) + val searchHistory get() = _searchHistory + + suspend fun getSearchKeywordRecent() = requestSearchKeywordRecentUseCase().let { + _searchHistory.emit(it) + } - private val _searchTagList = MutableLiveData>() - val searchTagList: LiveData> get() = _searchTagList + fun searchKeyword(keyword: String, keywordType: SearchHistoryType = SearchHistoryType.TAG) = + viewModelScope.launch { + updateSearchKeywordRecentUseCase(keyword, keywordType) + when (keywordType) { + SearchHistoryType.TAG -> { + requestSearchTotalCountUseCase(keyword, SearchHistoryType.TAG).let { + _searchTagTotalCount.emit(it) + } + requestSearchTagUseCase(tag = keyword) + .cachedIn(viewModelScope) + .collectLatest { + _searchTagList.emit(it) + requestSearchKeywordRecentUseCase().let { + _searchHistory.emit(it) + } + } + } - fun getSearchKeywordRecent() = requestSearchKeywordRecentUseCase() + SearchHistoryType.USER -> { + requestSearchTotalCountUseCase(keyword, SearchHistoryType.USER).let { + _searchUserTotalCount.emit(it) + } - fun searchKeyword(keyword: String) = viewModelScope.launch { - requestSearchTotalCountUseCase(keyword).let { - _searchTotalCount.postValue(it) + requestSearchUserUseCase(nickname = keyword) + .cachedIn(viewModelScope) + .collectLatest { + _searchUserList.emit(it) + requestSearchKeywordRecentUseCase().let { + _searchHistory.emit(it) + } + } + } + } } - requestSearchKeywordUseCase(keyword = keyword) + suspend fun searchFollowUser(keyword: String) { + requestSearchFollowUserUseCase(nickname = keyword) .cachedIn(viewModelScope) - .collectLatest { _searchTagList.postValue(it) } + .collectLatest { + _searchFollowUserList.emit(it) + } } - fun deleteSearchKeywordRecent(keyword: String) = - deleteSearchKeywordRecentUseCase(keyword) + suspend fun deleteSearchKeywordRecent(keyword: String, deleteKeywordType: SearchHistoryType) { + deleteSearchKeywordRecentUseCase(keyword, deleteKeywordType).let { + _searchHistory.emit(it) + } + } + + suspend fun clearSearchKeywordRecent() { + clearSearchKeywordRecentUseCase().let { + _searchHistory.emit(it) + } + } + + fun searchHashtag(hashtag: String) { + viewModelScope.launch { + requestSearchTotalCountUseCase(tag = hashtag, SearchHistoryType.TAG).let { + _searchTagTotalCount.emit(it) + } - fun clearSearchKeywordRecent() = clearSearchKeywordRecentUseCase() + requestSearchTagUseCase(tag = hashtag) + .cachedIn(viewModelScope) + .collectLatest { + _searchTagList.emit(it) + } + } + } } \ No newline at end of file diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/SettingNotificationViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/SettingNotificationViewModel.kt index 09f0b986..78baf3e1 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/SettingNotificationViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/SettingNotificationViewModel.kt @@ -1,13 +1,13 @@ package daily.dayo.presentation.viewmodel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import daily.dayo.domain.model.NetworkResponse import daily.dayo.domain.usecase.member.* import daily.dayo.presentation.service.firebase.FirebaseMessagingService import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -18,15 +18,15 @@ class SettingNotificationViewModel @Inject constructor( private val requestChangeReceiveAlarmUseCase: RequestChangeReceiveAlarmUseCase, ) : ViewModel() { - private val _notiReactionPermit = MutableLiveData() - val notiReactionPermit: LiveData get() = _notiReactionPermit + private val _isReactionNotificationEnabled = MutableStateFlow(false) + val isReactionNotificationEnabled: StateFlow get() = _isReactionNotificationEnabled fun requestReceiveAlarm() = viewModelScope.launch { requestReceiveAlarmUseCase().let { ApiResponse -> when (ApiResponse) { is NetworkResponse.Success -> { ApiResponse.body?.let { - _notiReactionPermit.postValue(it) + _isReactionNotificationEnabled.emit(it) } } else -> { @@ -37,8 +37,16 @@ class SettingNotificationViewModel @Inject constructor( } fun requestReceiveChangeReceiveAlarm(onReceiveAlarm: Boolean) = viewModelScope.launch { - val response = - requestChangeReceiveAlarmUseCase(onReceiveAlarm = onReceiveAlarm) + requestChangeReceiveAlarmUseCase(onReceiveAlarm = onReceiveAlarm).let { ApiResponse -> + when (ApiResponse) { + is NetworkResponse.Success -> { + _isReactionNotificationEnabled.emit(onReceiveAlarm) + } + else -> { + + } + } + } } fun registerDeviceToken() = viewModelScope.launch { diff --git a/presentation/src/main/java/daily/dayo/presentation/viewmodel/WriteViewModel.kt b/presentation/src/main/java/daily/dayo/presentation/viewmodel/WriteViewModel.kt index a3c77d40..d18724a0 100644 --- a/presentation/src/main/java/daily/dayo/presentation/viewmodel/WriteViewModel.kt +++ b/presentation/src/main/java/daily/dayo/presentation/viewmodel/WriteViewModel.kt @@ -2,41 +2,54 @@ package daily.dayo.presentation.viewmodel import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.net.Uri +import android.util.Log import androidx.core.net.toUri -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import daily.dayo.presentation.BuildConfig -import daily.dayo.presentation.common.Event -import daily.dayo.presentation.common.image.ImageResizeUtil -import daily.dayo.presentation.common.image.ImageResizeUtil.POST_IMAGE_RESIZE_SIZE -import daily.dayo.presentation.common.image.ImageResizeUtil.cropCenterBitmap -import daily.dayo.presentation.common.ListLiveData -import daily.dayo.presentation.common.Resource -import daily.dayo.presentation.common.toBitmap -import daily.dayo.presentation.common.toFile +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import daily.dayo.domain.model.Category import daily.dayo.domain.model.Folder import daily.dayo.domain.model.NetworkResponse -import daily.dayo.domain.model.PostDetail import daily.dayo.domain.model.Privacy import daily.dayo.domain.usecase.folder.RequestAllMyFolderListUseCase import daily.dayo.domain.usecase.folder.RequestCreateFolderInPostUseCase import daily.dayo.domain.usecase.post.RequestEditPostUseCase import daily.dayo.domain.usecase.post.RequestPostDetailUseCase import daily.dayo.domain.usecase.post.RequestUploadPostUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext +import daily.dayo.presentation.common.Event +import daily.dayo.presentation.common.Resource +import daily.dayo.presentation.common.Status +import daily.dayo.presentation.common.image.ImageResizeUtil.POST_IMAGE_RESIZE_SIZE +import daily.dayo.presentation.common.image.ImageResizeUtil.cropCenterBitmap +import daily.dayo.presentation.common.image.ImageResizeUtil.resizeBitmap +import daily.dayo.presentation.common.image.applyExif +import daily.dayo.presentation.common.image.readExifInfo +import daily.dayo.presentation.common.toFile +import daily.dayo.presentation.screen.home.CategoryMenu +import daily.dayo.presentation.screen.write.ImageAsset +import daily.dayo.presentation.screen.write.ImageCropStateHolder import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream +import java.io.IOException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import javax.inject.Inject +import kotlin.math.roundToInt @SuppressLint("StaticFieldLeak") @HiltViewModel @@ -44,6 +57,7 @@ class WriteViewModel @Inject constructor( // There isn't really a leak here in the context constructor, it is just the lint check doesn't know that is the application context @ApplicationContext private val applicationContext: Context, + savedStateHandle: SavedStateHandle, private val requestUploadPostUseCase: RequestUploadPostUseCase, private val requestEditPostUseCase: RequestEditPostUseCase, private val requestPostDetailUseCase: RequestPostDetailUseCase, @@ -54,40 +68,45 @@ class WriteViewModel @Inject constructor( // Show Dialog val showWriteOptionDialog = MutableLiveData>() + private val _errorEvent = MutableSharedFlow() + val errorEvent = _errorEvent.asSharedFlow() + // WriteInfo - private val _postId = MutableLiveData(0) - val postId get() = _postId - private val _postCategory = MutableLiveData(null) - val postCategory get() = _postCategory - private val _postContents = MutableLiveData("") - val postContents get() = _postContents - private val _postFolderId = MutableLiveData("") - val postFolderId get() = _postFolderId - private val _postFolderName = MutableLiveData("") - val postFolderName get() = _postFolderName - private val _postImageUriList = ListLiveData() // ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ๋ถˆ๋Ÿฌ์˜จ ์ด๋ฏธ์ง€ ๋ฆฌ์ŠคํŠธ - val postImageUriList get() = _postImageUriList - private val _postTagList = ListLiveData() - val postTagList: ListLiveData get() = _postTagList + private val _postEditId = MutableStateFlow(null as Long?) + val postEditId = _postEditId.asStateFlow() + private val _writeCategory = MutableStateFlow>(Pair(null, -1)) + val writeCategory get() = _writeCategory + private val _writeText = MutableStateFlow("") + val writeText get() = _writeText + private val _writeFolderId = MutableStateFlow(null) + val writeFolderId get() = _writeFolderId + private val _writeFolderName = MutableStateFlow(null as String?) + val writeFolderName get() = _writeFolderName + private val _writeImagesUri = MutableStateFlow>(emptyList()) + val writeImagesUri = _writeImagesUri.asStateFlow() + private val _writeTags = MutableStateFlow(emptyList()) + val writeTags get() = _writeTags + private val _postInfoSuccess = MutableSharedFlow() + val postInfoSuccess = _postInfoSuccess.asSharedFlow() // WritePost - private val _writePostId = MutableLiveData>() - val writePostId: LiveData> get() = _writePostId - private val _writeSuccess = MutableLiveData>() - val writeSuccess: LiveData> get() = _writeSuccess - private val _writeCurrentPostDetail = MutableLiveData() - val writeCurrentPostDetail: LiveData get() = _writeCurrentPostDetail - private val _writeEditSuccess = MutableLiveData>() - val writeEditSuccess: LiveData> get() = _writeEditSuccess + private val _writePostId = MutableSharedFlow() + val writePostId = _writePostId.asSharedFlow() + private val _uploadSuccess: MutableStateFlow = MutableStateFlow(null) + val uploadSuccess get() = _uploadSuccess // WriteFolder - private val _folderList = MutableLiveData>>() - val folderList: LiveData>> get() = _folderList - private val _folderAddSuccess = MutableLiveData>() - val folderAddAccess: LiveData> get() = _folderAddSuccess + private val _writeFolderAddSuccess = MutableStateFlow(Event(false)) + val writeFolderAddSuccess get() = _writeFolderAddSuccess + + init { + savedStateHandle.get("folderId")?.toLongOrNull()?.let { folderId -> + setInitFolder(folderId) + } + } fun requestUploadPost() { - if (this@WriteViewModel.postId.value != 0) { + if (postEditId.value != null) { requestUploadEditingPost() } else { requestUploadNewPost() @@ -104,167 +123,306 @@ class WriteViewModel @Inject constructor( return "${applicationContext.cacheDir}/$fileName" } + /** + * ์ƒˆ ๊ฒŒ์‹œ๊ธ€ ์—…๋กœ๋“œ + * ์—…๋กœ๋“œ ์‹œ์ ์—๋งŒ Uri๋กœ๋ถ€ํ„ฐ Bitmap์„ ๋กœ๋“œํ•˜์—ฌ ์‚ฌ์šฉ (OOM ๋ฐฉ์ง€) + */ private fun requestUploadNewPost() = viewModelScope.launch(Dispatchers.IO) { - _writeSuccess.postValue(Event(false)) - val resizedImages = async { - postImageUriList.value?.map { item -> - val postImageBitmap = - item.toUri().toBitmap(applicationContext.contentResolver) - ?.cropCenterBitmap() - val resizedImageBitmap = postImageBitmap?.let { - ImageResizeUtil.resizeBitmap( - originalBitmap = it, - resizedWidth = POST_IMAGE_RESIZE_SIZE, - resizedHeight = POST_IMAGE_RESIZE_SIZE + _uploadSuccess.emit(Status.LOADING) + val imageFiles = _writeImagesUri.value.mapNotNull { imageAsset -> + // ๊ฐ Uri๋กœ๋ถ€ํ„ฐ ๋น„ํŠธ๋งต์„ ์—ด๊ณ , ๋ฆฌ์‚ฌ์ด์ฆˆํ•˜๊ณ , ํŒŒ์ผ๋กœ ๋ณ€ํ™˜ + applicationContext.contentResolver.openInputStream(Uri.parse(imageAsset.uriString)) + ?.use { inputStream -> + val bitmap = BitmapFactory.decodeStream(inputStream) + val resizedBitmap = resizeBitmap( + bitmap.cropCenterBitmap(), + POST_IMAGE_RESIZE_SIZE, + POST_IMAGE_RESIZE_SIZE ) + val file = resizedBitmap.toFile(uploadImagePath) + bitmap.recycle() + resizedBitmap.recycle() + file } - resizedImageBitmap.toFile(uploadImagePath) - } } - resizedImages.await()?.let { - requestUploadPostUseCase( - category = this@WriteViewModel.postCategory.value!!, - contents = this@WriteViewModel.postContents.value!!, - files = it.toTypedArray(), - folderId = this@WriteViewModel.postFolderId.value!!.toInt(), - tags = this@WriteViewModel.postTagList.value!!.toTypedArray() - ).let { ApiResponse -> - _writeSuccess.postValue(Event(ApiResponse is NetworkResponse.Success)) - if (ApiResponse is NetworkResponse.Success) { - resetWriteInfoValue() - _writePostId.postValue(ApiResponse.body?.let { Event(it.id) }) + if (imageFiles.isEmpty() && _writeImagesUri.value.isNotEmpty()) { + _errorEvent.emit("์ด๋ฏธ์ง€๋ฅผ ํŒŒ์ผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.") + _uploadSuccess.emit(Status.ERROR) + return@launch + } + + requestUploadPostUseCase( + category = writeCategory.value.first!!, + contents = writeText.value, + files = imageFiles.toTypedArray(), + folderId = writeFolderId.value ?: 0L, + tags = if (writeTags.value.isNotEmpty()) writeTags.value.toTypedArray() else emptyArray() + ).let { apiResponse -> + when (apiResponse) { + is NetworkResponse.Success -> { + _writePostId.emit(apiResponse.body?.id) + _uploadSuccess.emit(Status.SUCCESS) + } + + else -> { + _uploadSuccess.emit(Status.ERROR) } } } } private fun requestUploadEditingPost() = viewModelScope.launch(Dispatchers.IO) { + _uploadSuccess.emit(Status.LOADING) requestEditPostUseCase( - postId = this@WriteViewModel.postId.value!!, - category = this@WriteViewModel.postCategory.value!!, - contents = this@WriteViewModel.postContents.value!!, - folderId = this@WriteViewModel.postFolderId.value!!.toInt(), - hashtags = this@WriteViewModel.postTagList.value!!.toList() - ).let { ApiResponse -> - _writeEditSuccess.postValue(Event(ApiResponse is NetworkResponse.Success)) - if (ApiResponse is NetworkResponse.Success) { - resetWriteInfoValue() - _writePostId.postValue(ApiResponse.body?.let { Event(it.postId) }) - } - } - } + postId = postEditId.value!!, + category = writeCategory.value.first!!, + contents = writeText.value, + folderId = writeFolderId.value!!, + hashtags = writeTags.value.ifEmpty { emptyList() } + ).let { apiResponse -> + when (apiResponse) { + is NetworkResponse.Success -> { + _uploadSuccess.emit(Status.SUCCESS) + } - fun requestPostDetail(postId: Int) = viewModelScope.launch(Dispatchers.IO) { - withContext(Dispatchers.Main) { - resetWriteInfoValue() - requestPostDetailUseCase(postId = postId) - }.let { ApiResponse -> - if (ApiResponse is NetworkResponse.Success) { - ApiResponse.body.let { postDetail -> - postDetail?.let { - withContext(Dispatchers.Main) { - setOriginalPostDetail(postDetail) - } - } + else -> { + _uploadSuccess.emit(Status.ERROR) } } } } - private fun setOriginalPostDetail(postDetail: PostDetail) { - _writeCurrentPostDetail.postValue(postDetail) - postDetail.images.map { element -> - addUploadImage( - Uri.parse("${BuildConfig.BASE_URL}/images/$element") - .toString(), - true - ) + private suspend fun requestAllMyFolderList(): List? { + val resource = when (val response = requestAllMyFolderListUseCase()) { + is NetworkResponse.Success -> Resource.success(response.body?.data) + is NetworkResponse.NetworkError -> Resource.error(response.exception.toString(), null) + is NetworkResponse.ApiError -> Resource.error(response.error.toString(), null) + is NetworkResponse.UnknownError -> Resource.error(response.throwable.toString(), null) } - postDetail.hashtags.let { _postTagList.addAll(it, false) } - _postFolderId.postValue(postDetail.folderId.toString()) - _postFolderName.postValue(postDetail.folderName) + if (resource.data == null) { + Log.e("WriteViewModel", "ํด๋” ๋ฆฌ์ŠคํŠธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ: ${resource.message}") + } + + return resource.data } - fun requestAllMyFolderList() = viewModelScope.launch(Dispatchers.IO) { - _folderList.postValue(Resource.loading(null)) - requestAllMyFolderListUseCase()?.let { ApiResponse -> - _folderList.postValue( - when (ApiResponse) { - is NetworkResponse.Success -> { - Resource.success(ApiResponse.body?.data) - } + fun requestCreateFolderInPost( + name: String, + description: String, + privacy: Privacy + ) = + viewModelScope.launch(Dispatchers.IO) { + requestCreateFolderInPostUseCase( + name = name, + description = description, + privacy = privacy + ).let { ApiResponse -> + _writeFolderAddSuccess.emit(Event(ApiResponse is NetworkResponse.Success)) + } + } - is NetworkResponse.NetworkError -> { - Resource.error(ApiResponse.exception.toString(), null) - } + /** + * ์ƒํƒœ ์ดˆ๊ธฐํ™” + */ + fun resetWriteInfoValue() = viewModelScope.launch { + _postEditId.emit(null) + _writeFolderId.emit(null) + _writeFolderName.emit("") + _writeTags.emit(emptyList()) + _writeImagesUri.emit(emptyList()) + } - is NetworkResponse.ApiError -> { - Resource.error(ApiResponse.error.toString(), null) + fun requestPostDetail(postId: Long, categoryMenus: List) { + viewModelScope.launch { + requestPostDetailUseCase(postId).let { response -> + when (response) { + is NetworkResponse.Success -> { + response.body?.run { + setPostEditId(postId) + setPostCategory( + categoryMenus + .withIndex() + .firstOrNull { it.value.category == category } + ?.let { it.value.category to it.index } + ?: Pair(null, -1) + ) + setWriteText(contents) + setFolderId(folderId) + setFolderName(folderName) + updatePostTags(hashtags) + setPostEditImages(images) + } } - is NetworkResponse.UnknownError -> { - Resource.error(ApiResponse.throwable.toString(), null) + else -> { + _postInfoSuccess.emit(false) } } - ) + } } } - fun requestCreateFolderInPost(name: String, privacy: Privacy) = - viewModelScope.launch(Dispatchers.IO) { - requestCreateFolderInPostUseCase(name = name, privacy = privacy).let { ApiResponse -> - _folderAddSuccess.postValue(Event(ApiResponse is NetworkResponse.Success)) - } - } + private fun setPostEditId(id: Long) { + _postEditId.value = id + } - fun resetWriteInfoValue() { - _postId.postValue(0) - _postContents.postValue("") - _postFolderId.postValue("") - _postFolderName.postValue("") - _postImageUriList.postValue(arrayListOf()) - _postTagList.clear(notify = false) + fun setWriteText(text: String) { + _writeText.value = text } - fun setPostId(id: Int) { - _postId.value = id + fun setPostCategory(category: Pair) = viewModelScope.launch { + _writeCategory.emit(category) } - fun setPostContents(contents: String) { - _postContents.value = contents + private fun setInitFolder(id: Long) { + viewModelScope.launch { + _writeFolderId.emit(id) + val folders = requestAllMyFolderList() + val folderTitle = folders?.find { it.folderId == id }?.title + _writeFolderName.emit(folderTitle) + } } - fun setPostCategory(category: Category?) { - _postCategory.value = category + fun setFolderId(id: Long) = viewModelScope.launch { + _writeFolderId.emit(id) } - fun setFolderId(id: String) { - _postFolderId.value = id + fun setFolderName(name: String) = viewModelScope.launch { + _writeFolderName.emit(name) } - fun setFolderName(name: String) { - _postFolderName.value = name + private fun setPostEditImages(images: List) { + viewModelScope.launch { + _writeImagesUri.emit( + images.map { + ImageAsset(uriString = it, exifInfo = null) + } + ) + } } - fun addUploadImage(uriPath: String, notify: Boolean = false) { - _postImageUriList.add(uriPath, notify) + /** + * ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ๊ฐ€์ ธ์˜จ ์ด๋ฏธ์ง€๋“ค์„ ViewModel์— ์ถ”๊ฐ€ + * Bitmap์„ ๋กœ๋“œํ•˜์ง€ ์•Š๊ณ  Uri์™€ EXIF ์ •๋ณด๋ฅผ ImageAsset ํ˜•ํƒœ๋กœ ์ €์žฅ + */ + fun addOriginalImages(uris: List) { + viewModelScope.launch { + val newImageAssets = withContext(Dispatchers.IO) { + uris.map { uri -> + val exifInfo = applicationContext.contentResolver.readExifInfo(uri) + ImageAsset(uriString = uri.toString(), exifInfo = exifInfo) + } + } + _writeImagesUri.emit(newImageAssets) + } } - fun deleteUploadImage(pos: Int, notify: Boolean = false) { - _postImageUriList.removeAt(pos, notify) + /** + * ์ด๋ฏธ์ง€๋ฅผ ํŽธ์ง‘ํ•˜๊ณ , ์™„๋ฃŒ๋˜๋ฉด ๋ฆฌ์ŠคํŠธ์˜ ํ•ด๋‹น ImageAsset์„ ์ƒˆ ์ •๋ณด๋กœ ๊ต์ฒด + */ + fun cropImageAndUpdate(imageIndex: Int, stateHolder: ImageCropStateHolder) { + val imageAsset = _writeImagesUri.value.getOrNull(imageIndex) ?: return + + viewModelScope.launch { + try { + val newUri = withContext(Dispatchers.IO) { + val originalUri = Uri.parse(imageAsset.uriString) + + // ์ „์ฒด ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•˜๊ณ  EXIF ์ •๋ณด๋ฅผ ์ ์šฉ + val originalBitmap = applicationContext.contentResolver.openInputStream(originalUri)?.use { inputStream -> + BitmapFactory.decodeStream(inputStream) + } ?: throw IOException("์›๋ณธ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ") + + val exifInfo = stateHolder.exifInfo + + // EXIF ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฉด ์ ์šฉํ•˜์—ฌ ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์œผ๋กœ ํšŒ์ „ + val rotatedBitmap = if (exifInfo != null) { + originalBitmap.applyExif(exifInfo) + } else { + originalBitmap + } + + // ์›๋ณธ ๋น„ํŠธ๋งต์ด ๋‹ค๋ฅด๋ฉด ๋ฆฌ์‚ฌ์ดํด + if (rotatedBitmap != originalBitmap) { + originalBitmap.recycle() + } + + val cropProps = stateHolder.cropProperties.value + + // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋น„ํŠธ๋งต๊ณผ ์‹ค์ œ ๋กœ๋“œ๋œ ๋น„ํŠธ๋งต ๊ฐ„์˜ ์Šค์ผ€์ผ ๊ณ„์‚ฐ + val scaleX = rotatedBitmap.width.toFloat() / stateHolder.imageWidth + val scaleY = rotatedBitmap.height.toFloat() / stateHolder.imageHeight + + // ํฌ๋กญ ์ขŒํ‘œ๋ฅผ ์‹ค์ œ ๋น„ํŠธ๋งต ํฌ๊ธฐ์— ๋งž๊ฒŒ ๋ณ€ํ™˜ + val left = (cropProps.cropOffset.x * scaleX).roundToInt() + val top = ((cropProps.cropOffset.y - stateHolder.imageTop) * scaleY).roundToInt() + val size = (cropProps.cropSize * scaleX).roundToInt() + + // ํฌ๋กญ ์˜์—ญ์ด ๋น„ํŠธ๋งต ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ๋ณด์ • + val finalLeft = left.coerceIn(0, rotatedBitmap.width) + val finalTop = top.coerceIn(0, rotatedBitmap.height) + val finalRight = (left + size).coerceIn(0, rotatedBitmap.width) + val finalBottom = (top + size).coerceIn(0, rotatedBitmap.height) + val finalWidth = finalRight - finalLeft + val finalHeight = finalBottom - finalTop + + // ํฌ๋กญ๋œ ๋น„ํŠธ๋งต ์ƒ์„ฑ + val croppedBitmap = Bitmap.createBitmap( + rotatedBitmap, + finalLeft, + finalTop, + finalWidth, + finalHeight + ) + + // ์›๋ณธ ํšŒ์ „๋œ ๋น„ํŠธ๋งต ๋ฆฌ์‚ฌ์ดํด + rotatedBitmap.recycle() + + // ํฌ๋กญ๋œ ์ด๋ฏธ์ง€๋ฅผ ์ƒˆ ์บ์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ (EXIF ์ •๋ณด๋Š” ์ œ๊ฑฐ๋จ) + val cacheFile = File( + applicationContext.cacheDir, + "cropped_${System.currentTimeMillis()}.jpg" + ) + FileOutputStream(cacheFile).use { out -> + croppedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) + } + croppedBitmap.recycle() + cacheFile.toUri() + } + + _writeImagesUri.getAndUpdate { currentList -> + currentList.toMutableList().apply { + // ํŽธ์ง‘๋œ ์ด๋ฏธ์ง€๋Š” EXIF ์ •๋ณด๊ฐ€ ์ œ๊ฑฐ๋˜๋ฏ€๋กœ null๋กœ ์„ค์ • + this[imageIndex] = ImageAsset(uriString = newUri.toString(), exifInfo = null) + } + } + } catch (e: Exception) { + e.printStackTrace() + _errorEvent.emit("์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.") + } + } } - fun clearUploadImage() { - _postImageUriList.clear(notify = true) + /** + * ์ง€์ •๋œ ์ธ๋ฑ์Šค์˜ ์ด๋ฏธ์ง€ ์ œ๊ฑฐ + */ + fun removeUploadImage(pos: Int) { + viewModelScope.launch { + _writeImagesUri.getAndUpdate { currentList -> + currentList.filterIndexed { index, _ -> index != pos } + } + } } - fun addPostTag(tagText: String, notify: Boolean = false) { - _postTagList.add(tagText, notify) + fun clearUploadImage() { + viewModelScope.launch { + _writeImagesUri.emit(emptyList()) + } } - fun removePostTag(tagText: String, notify: Boolean = false) { - _postTagList.remove(tagText, notify) + fun updatePostTags(tagTextList: List) = viewModelScope.launch { + _writeTags.emit(tagTextList) } } \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_arrow_category_green.xml b/presentation/src/main/res/drawable/ic_arrow_category_green.xml new file mode 100644 index 00000000..7aadf841 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_arrow_category_green.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/presentation/src/main/res/drawable/ic_arrow_left.xml b/presentation/src/main/res/drawable/ic_arrow_left.xml new file mode 100644 index 00000000..a3270856 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_arrow_left.xml @@ -0,0 +1,11 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_arrow_right.xml b/presentation/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 00000000..56e1a121 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_arrow_tag_gray.xml b/presentation/src/main/res/drawable/ic_arrow_tag_gray.xml new file mode 100644 index 00000000..054d7b8c --- /dev/null +++ b/presentation/src/main/res/drawable/ic_arrow_tag_gray.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/presentation/src/main/res/drawable/ic_back_sign.xml b/presentation/src/main/res/drawable/ic_back_sign.xml index fb40eb3a..ad98e0af 100644 --- a/presentation/src/main/res/drawable/ic_back_sign.xml +++ b/presentation/src/main/res/drawable/ic_back_sign.xml @@ -1,13 +1,10 @@ - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_blocked_users_empty.xml b/presentation/src/main/res/drawable/ic_blocked_users_empty.xml new file mode 100644 index 00000000..648d468e --- /dev/null +++ b/presentation/src/main/res/drawable/ic_blocked_users_empty.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_bookmark_checked.xml b/presentation/src/main/res/drawable/ic_bookmark_checked.xml index 5394f7f2..6ecc45b3 100644 --- a/presentation/src/main/res/drawable/ic_bookmark_checked.xml +++ b/presentation/src/main/res/drawable/ic_bookmark_checked.xml @@ -1,12 +1,12 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M4.5 4.53c0-0.96 0.77-1.73 1.73-1.73h11.54c0.96 0 1.73 0.77 1.73 1.73v14.7c0 1.23-1.24 2.07-2.38 1.6l-4.47-1.8c-0.42-0.17-0.88-0.17-1.3 0l-4.47 1.8c-1.14 0.47-2.38-0.37-2.38-1.6V4.54Z"/> \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_bookmark_default.xml b/presentation/src/main/res/drawable/ic_bookmark_default.xml index 53c7f619..5e211f4e 100644 --- a/presentation/src/main/res/drawable/ic_bookmark_default.xml +++ b/presentation/src/main/res/drawable/ic_bookmark_default.xml @@ -1,12 +1,11 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> - + android:pathData="M4.5 4.53c0-0.96 0.77-1.73 1.73-1.73h11.54c0.96 0 1.73 0.77 1.73 1.73v14.7c0 1.23-1.24 2.07-2.38 1.6l-4.47-1.8c-0.42-0.17-0.88-0.17-1.3 0l-4.47 1.8c-1.14 0.47-2.38-0.37-2.38-1.6V4.54Z"/> + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category.xml b/presentation/src/main/res/drawable/ic_category.xml new file mode 100644 index 00000000..66247fe1 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_category_all.xml b/presentation/src/main/res/drawable/ic_category_all.xml new file mode 100644 index 00000000..b5a7c13e --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_all.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_all_checked.xml b/presentation/src/main/res/drawable/ic_category_all_checked.xml new file mode 100644 index 00000000..130900dc --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_all_checked.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_digital.xml b/presentation/src/main/res/drawable/ic_category_digital.xml new file mode 100644 index 00000000..347859d0 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_digital.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_digital_checked.xml b/presentation/src/main/res/drawable/ic_category_digital_checked.xml new file mode 100644 index 00000000..6174b019 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_digital_checked.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_etc.xml b/presentation/src/main/res/drawable/ic_category_etc.xml new file mode 100644 index 00000000..23c6e193 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_etc.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_etc_checked.xml b/presentation/src/main/res/drawable/ic_category_etc_checked.xml new file mode 100644 index 00000000..ca178dec --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_etc_checked.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_pocketbook.xml b/presentation/src/main/res/drawable/ic_category_pocketbook.xml new file mode 100644 index 00000000..4c3916f0 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_pocketbook.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_pocketbook_checked.xml b/presentation/src/main/res/drawable/ic_category_pocketbook_checked.xml new file mode 100644 index 00000000..0292d8ce --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_pocketbook_checked.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_scheduler.xml b/presentation/src/main/res/drawable/ic_category_scheduler.xml new file mode 100644 index 00000000..0f1c1fd6 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_scheduler.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_scheduler_checked.xml b/presentation/src/main/res/drawable/ic_category_scheduler_checked.xml new file mode 100644 index 00000000..3c39e1f8 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_scheduler_checked.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_sixholediary.xml b/presentation/src/main/res/drawable/ic_category_sixholediary.xml new file mode 100644 index 00000000..cf24a826 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_sixholediary.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_sixholediary_checked.xml b/presentation/src/main/res/drawable/ic_category_sixholediary_checked.xml new file mode 100644 index 00000000..d5426667 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_sixholediary_checked.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_studyplanner.xml b/presentation/src/main/res/drawable/ic_category_studyplanner.xml new file mode 100644 index 00000000..7f98c563 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_studyplanner.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_category_studyplanner_checked.xml b/presentation/src/main/res/drawable/ic_category_studyplanner_checked.xml new file mode 100644 index 00000000..5ff5a2b8 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_category_studyplanner_checked.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_check_ok_sign.xml b/presentation/src/main/res/drawable/ic_check_ok_sign.xml index ba955da7..d5070f74 100644 --- a/presentation/src/main/res/drawable/ic_check_ok_sign.xml +++ b/presentation/src/main/res/drawable/ic_check_ok_sign.xml @@ -1,14 +1,19 @@ + android:width="22dp" + android:height="22dp" + android:viewportWidth="22" + android:viewportHeight="22"> + android:fillColor="#FF23C882" + android:pathData="M1 11c0-5.52 4.48-10 10-10h0c5.52 0 10 4.48 10 10v0c0 5.52-4.48 10-10 10h0c-5.52 0-10-4.48-10-10z"/> + - + android:strokeLineCap="round" + android:strokeLineJoin="round" + android:pathData="M7 10.64l2.53 2.89 5.77-5.06"/> + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_check_sign_gray.xml b/presentation/src/main/res/drawable/ic_check_sign_gray.xml new file mode 100644 index 00000000..76477708 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_check_sign_gray.xml @@ -0,0 +1,14 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_chevron_r.xml b/presentation/src/main/res/drawable/ic_chevron_r.xml new file mode 100644 index 00000000..c8f4923b --- /dev/null +++ b/presentation/src/main/res/drawable/ic_chevron_r.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_comment.xml b/presentation/src/main/res/drawable/ic_comment.xml index a6065797..4fc9f09c 100644 --- a/presentation/src/main/res/drawable/ic_comment.xml +++ b/presentation/src/main/res/drawable/ic_comment.xml @@ -1,11 +1,12 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:strokeMiterLimit="10" + android:pathData="M19.16 3H4.86C3.27 3 2 4.24 2 5.78v9.08c0 1.52 1.27 2.77 2.84 2.77H9c0.15 0 0.3 0.07 0.39 0.19l1.98 2.5c0.27 0.36 0.8 0.37 1.08 0.03l2.18-2.55c0.1-0.11 0.23-0.18 0.38-0.18h4.16c1.57 0 2.84-1.24 2.84-2.76V5.78c0-1.53-1.27-2.77-2.84-2.77V3Z"/> \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_comment_empty.xml b/presentation/src/main/res/drawable/ic_comment_empty.xml new file mode 100644 index 00000000..2686a556 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_comment_empty.xml @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_crop.xml b/presentation/src/main/res/drawable/ic_crop.xml new file mode 100644 index 00000000..6b8feed3 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_crop.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_dayo_pick.xml b/presentation/src/main/res/drawable/ic_dayo_pick.xml new file mode 100644 index 00000000..75510533 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_dayo_pick.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_feed.xml b/presentation/src/main/res/drawable/ic_feed.xml index a6fb0f9a..e8fd9a63 100644 --- a/presentation/src/main/res/drawable/ic_feed.xml +++ b/presentation/src/main/res/drawable/ic_feed.xml @@ -1,22 +1,26 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:strokeColor="#FF767B83" + android:strokeWidth="1.2" + android:strokeLineJoin="round" + android:pathData="M2.4 4c0-0.83 0.67-1.5 1.5-1.5h4.6c0.83 0 1.5 0.67 1.5 1.5v4.6c0 0.83-0.67 1.5-1.5 1.5h-4.6c-0.83 0-1.5-0.67-1.5-1.5z"/> + android:strokeColor="#FF767B83" + android:strokeWidth="1.2" + android:strokeLineJoin="round" + android:pathData="M13.8 4c0-0.83 0.67-1.5 1.5-1.5h4.6c0.83 0 1.5 0.67 1.5 1.5v4.6c0 0.83-0.67 1.5-1.5 1.5h-4.6c-0.83 0-1.5-0.67-1.5-1.5z"/> + android:strokeColor="#FF767B83" + android:strokeWidth="1.2" + android:strokeLineJoin="round" + android:pathData="M2.4 15.4c0-0.83 0.67-1.5 1.5-1.5h4.6c0.83 0 1.5 0.67 1.5 1.5v4.6c0 0.83-0.67 1.5-1.5 1.5h-4.6c-0.83 0-1.5-0.67-1.5-1.5z"/> - + android:strokeColor="#FF767B83" + android:strokeWidth="1.2" + android:strokeLineJoin="round" + android:pathData="M13.8 15.4c0-0.83 0.67-1.5 1.5-1.5h4.6c0.83 0 1.5 0.67 1.5 1.5v4.6c0 0.83-0.67 1.5-1.5 1.5h-4.6c-0.83 0-1.5-0.67-1.5-1.5z"/> + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_feed_filled.xml b/presentation/src/main/res/drawable/ic_feed_filled.xml index ec9b3aee..648b91a1 100644 --- a/presentation/src/main/res/drawable/ic_feed_filled.xml +++ b/presentation/src/main/res/drawable/ic_feed_filled.xml @@ -1,18 +1,30 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:fillColor="#FF313131" + android:strokeColor="#FF313131" + android:strokeWidth="1.2" + android:strokeLineJoin="round" + android:pathData="M2.4 4c0-0.83 0.67-1.5 1.5-1.5h4.6c0.83 0 1.5 0.67 1.5 1.5v4.6c0 0.83-0.67 1.5-1.5 1.5h-4.6c-0.83 0-1.5-0.67-1.5-1.5z"/> + android:fillColor="#FF313131" + android:strokeColor="#FF313131" + android:strokeWidth="1.2" + android:strokeLineJoin="round" + android:pathData="M13.8 4c0-0.83 0.67-1.5 1.5-1.5h4.6c0.83 0 1.5 0.67 1.5 1.5v4.6c0 0.83-0.67 1.5-1.5 1.5h-4.6c-0.83 0-1.5-0.67-1.5-1.5z"/> + android:fillColor="#FF313131" + android:strokeColor="#FF313131" + android:strokeWidth="1.2" + android:strokeLineJoin="round" + android:pathData="M2.4 15.4c0-0.83 0.67-1.5 1.5-1.5h4.6c0.83 0 1.5 0.67 1.5 1.5v4.6c0 0.83-0.67 1.5-1.5 1.5h-4.6c-0.83 0-1.5-0.67-1.5-1.5z"/> - + android:fillColor="#FF313131" + android:strokeColor="#FF313131" + android:strokeWidth="1.2" + android:strokeLineJoin="round" + android:pathData="M13.8 15.4c0-0.83 0.67-1.5 1.5-1.5h4.6c0.83 0 1.5 0.67 1.5 1.5v4.6c0 0.83-0.67 1.5-1.5 1.5h-4.6c-0.83 0-1.5-0.67-1.5-1.5z"/> + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_folder_new.xml b/presentation/src/main/res/drawable/ic_folder_new.xml new file mode 100644 index 00000000..0ff49301 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_folder_new.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_folder_private_my_page.xml b/presentation/src/main/res/drawable/ic_folder_private_my_page.xml new file mode 100644 index 00000000..0f8eada5 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_folder_private_my_page.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/presentation/src/main/res/drawable/ic_folder_public_my_page.xml b/presentation/src/main/res/drawable/ic_folder_public_my_page.xml new file mode 100644 index 00000000..2872d175 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_folder_public_my_page.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/presentation/src/main/res/drawable/ic_follow_empty.xml b/presentation/src/main/res/drawable/ic_follow_empty.xml index 534dd781..8285e8b0 100644 --- a/presentation/src/main/res/drawable/ic_follow_empty.xml +++ b/presentation/src/main/res/drawable/ic_follow_empty.xml @@ -1,54 +1,54 @@ - - - - - - - - - - + android:width="137dp" + android:height="100dp" + android:viewportWidth="137" + android:viewportHeight="100"> + + + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_follower.xml b/presentation/src/main/res/drawable/ic_follower.xml new file mode 100644 index 00000000..63861dea --- /dev/null +++ b/presentation/src/main/res/drawable/ic_follower.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_following.xml b/presentation/src/main/res/drawable/ic_following.xml new file mode 100644 index 00000000..87b57912 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_following.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_hashtag.xml b/presentation/src/main/res/drawable/ic_hashtag.xml new file mode 100644 index 00000000..8e36f36a --- /dev/null +++ b/presentation/src/main/res/drawable/ic_hashtag.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_heart.xml b/presentation/src/main/res/drawable/ic_heart.xml new file mode 100644 index 00000000..f99c327f --- /dev/null +++ b/presentation/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_heart_filled.xml b/presentation/src/main/res/drawable/ic_heart_filled.xml new file mode 100644 index 00000000..ff3d57b2 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_heart_filled.xml @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_heart_outlined.xml b/presentation/src/main/res/drawable/ic_heart_outlined.xml new file mode 100644 index 00000000..c694427c --- /dev/null +++ b/presentation/src/main/res/drawable/ic_heart_outlined.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_home.xml b/presentation/src/main/res/drawable/ic_home.xml index 224f14e9..d624a061 100644 --- a/presentation/src/main/res/drawable/ic_home.xml +++ b/presentation/src/main/res/drawable/ic_home.xml @@ -1,10 +1,10 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:strokeColor="#FF767B83" + android:strokeWidth="1.2" + android:pathData="M21.4 12.34l-8.63-9.13c-0.06-0.07-0.13-0.12-0.21-0.16C12.48 3.02 12.39 3 12.3 3s-0.17 0.02-0.26 0.05c-0.08 0.04-0.15 0.1-0.21 0.16l-8.64 9.13c-0.25 0.27-0.39 0.63-0.39 1.01 0 0.78 0.6 1.42 1.34 1.42h0.91v5.66c0 0.39 0.3 0.7 0.67 0.7H9.9c0.38 0 0.68-0.3 0.68-0.68v-2.77c0-0.84 0.68-1.52 1.51-1.52 0.84 0 1.52 0.68 1.52 1.52v2.77c0 0.38 0.3 0.69 0.68 0.69h4.6c0.37 0 0.67-0.32 0.67-0.71v-5.66h0.9c0.37 0 0.7-0.15 0.96-0.42 0.52-0.55 0.52-1.45 0-2Z"/> \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_home_filled.xml b/presentation/src/main/res/drawable/ic_home_filled.xml index 6fde572e..91b9b131 100644 --- a/presentation/src/main/res/drawable/ic_home_filled.xml +++ b/presentation/src/main/res/drawable/ic_home_filled.xml @@ -1,11 +1,11 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> - + android:fillColor="#FF313131" + android:strokeColor="#FF313131" + android:strokeWidth="1.2" + android:pathData="M21.4 12.34l-8.63-9.13c-0.06-0.07-0.13-0.12-0.21-0.16C12.48 3.02 12.39 3 12.3 3s-0.17 0.02-0.26 0.05c-0.08 0.04-0.15 0.1-0.21 0.16l-8.64 9.13c-0.25 0.27-0.39 0.63-0.39 1.01 0 0.78 0.6 1.42 1.34 1.42h0.91v5.66c0 0.39 0.3 0.7 0.67 0.7H9.9c0.38 0 0.68-0.3 0.68-0.68v-2.77c0-0.84 0.68-1.52 1.51-1.52 0.84 0 1.52 0.68 1.52 1.52v2.77c0 0.38 0.3 0.69 0.68 0.69h4.6c0.37 0 0.67-0.32 0.67-0.71v-5.66h0.9c0.37 0 0.7-0.15 0.96-0.42 0.52-0.55 0.52-1.45 0-2Z"/> + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_img_delete.xml b/presentation/src/main/res/drawable/ic_img_delete.xml new file mode 100644 index 00000000..82d082cf --- /dev/null +++ b/presentation/src/main/res/drawable/ic_img_delete.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_kakao.xml b/presentation/src/main/res/drawable/ic_kakao.xml new file mode 100644 index 00000000..3a8cfd69 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_kakao.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_like_default.xml b/presentation/src/main/res/drawable/ic_like_default.xml deleted file mode 100644 index 89a162ee..00000000 --- a/presentation/src/main/res/drawable/ic_like_default.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/presentation/src/main/res/drawable/ic_like_pressed.xml b/presentation/src/main/res/drawable/ic_like_pressed.xml deleted file mode 100644 index e7c94d79..00000000 --- a/presentation/src/main/res/drawable/ic_like_pressed.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/presentation/src/main/res/drawable/ic_menu_block.xml b/presentation/src/main/res/drawable/ic_menu_block.xml new file mode 100644 index 00000000..b905adfc --- /dev/null +++ b/presentation/src/main/res/drawable/ic_menu_block.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_menu_delete.xml b/presentation/src/main/res/drawable/ic_menu_delete.xml new file mode 100644 index 00000000..4e9a3028 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_menu_delete.xml @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_menu_edit.xml b/presentation/src/main/res/drawable/ic_menu_edit.xml new file mode 100644 index 00000000..698d9181 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_menu_edit.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_menu_folder.xml b/presentation/src/main/res/drawable/ic_menu_folder.xml new file mode 100644 index 00000000..21a001a7 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_menu_folder.xml @@ -0,0 +1,11 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_menu_post.xml b/presentation/src/main/res/drawable/ic_menu_post.xml new file mode 100644 index 00000000..6d9d4f2f --- /dev/null +++ b/presentation/src/main/res/drawable/ic_menu_post.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_menu_report.xml b/presentation/src/main/res/drawable/ic_menu_report.xml new file mode 100644 index 00000000..9b69d85d --- /dev/null +++ b/presentation/src/main/res/drawable/ic_menu_report.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_my_page.xml b/presentation/src/main/res/drawable/ic_my_page.xml index 407a186a..9ad37d3b 100644 --- a/presentation/src/main/res/drawable/ic_my_page.xml +++ b/presentation/src/main/res/drawable/ic_my_page.xml @@ -1,14 +1,14 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:strokeColor="#FF767B83" + android:strokeWidth="1.2" + android:pathData="M3.2 19.88c0-2.98 2.42-5.4 5.4-5.4h7.37c2.99 0 5.4 2.42 5.4 5.4 0 0.9-0.72 1.62-1.62 1.62H4.82c-0.9 0-1.62-0.73-1.62-1.62Z"/> + android:strokeColor="#FF767B83" + android:strokeWidth="1.2" + android:pathData="M12.28 2.5c-0.98 0-1.94 0.3-2.75 0.84S8.08 4.66 7.71 5.56c-0.38 0.9-0.48 1.9-0.28 2.87 0.19 0.96 0.66 1.84 1.35 2.53 0.7 0.7 1.58 1.17 2.54 1.36 0.96 0.19 1.96 0.1 2.86-0.28 0.91-0.38 1.68-1.02 2.23-1.83 0.54-0.82 0.83-1.77 0.83-2.75 0-0.65-0.13-1.3-0.37-1.9-0.25-0.6-0.62-1.15-1.08-1.61-0.46-0.46-1-0.83-1.61-1.07-0.6-0.25-1.25-0.38-1.9-0.38Z"/> \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_my_page_filled.xml b/presentation/src/main/res/drawable/ic_my_page_filled.xml index 3b0ada4f..73702756 100644 --- a/presentation/src/main/res/drawable/ic_my_page_filled.xml +++ b/presentation/src/main/res/drawable/ic_my_page_filled.xml @@ -1,16 +1,16 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:fillColor="#FF313131" + android:strokeColor="#FF313131" + android:strokeWidth="1.2" + android:pathData="M3.2 19.88c0-2.98 2.42-5.4 5.4-5.4h7.37c2.99 0 5.4 2.42 5.4 5.4 0 0.9-0.72 1.62-1.62 1.62H4.82c-0.9 0-1.62-0.73-1.62-1.62Z"/> - + android:fillColor="#FF313131" + android:strokeColor="#FF313131" + android:strokeWidth="1.2" + android:pathData="M12.28 2.5c-0.98 0-1.94 0.3-2.75 0.84S8.08 4.66 7.71 5.56c-0.38 0.9-0.48 1.9-0.28 2.87 0.19 0.96 0.66 1.84 1.35 2.53 0.7 0.7 1.58 1.17 2.54 1.36 0.96 0.19 1.96 0.1 2.86-0.28 0.91-0.38 1.68-1.02 2.23-1.83 0.54-0.82 0.83-1.77 0.83-2.75 0-0.65-0.13-1.3-0.37-1.9-0.25-0.6-0.62-1.15-1.08-1.61-0.46-0.46-1-0.83-1.61-1.07-0.6-0.25-1.25-0.38-1.9-0.38Z"/> + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_next.xml b/presentation/src/main/res/drawable/ic_next.xml new file mode 100644 index 00000000..ca98b673 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_next.xml @@ -0,0 +1,13 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_notification.xml b/presentation/src/main/res/drawable/ic_notification.xml index 08fc3dab..520bf322 100644 --- a/presentation/src/main/res/drawable/ic_notification.xml +++ b/presentation/src/main/res/drawable/ic_notification.xml @@ -1,14 +1,17 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:strokeColor="#FF767B83" + android:strokeWidth="1.2" + android:strokeLineJoin="round" + android:strokeMiterLimit="10" + android:pathData="M5.73 13.8l-2.05 2.3c-0.26 0.54 0.13 1.17 0.74 1.17h15.55c0.6 0 1-0.63 0.74-1.17l-2.05-2.3V9.2c0-3.52-2.72-6.6-6.26-6.7-3.54-0.1-6.67 2.82-6.67 6.44v4.87Z"/> + android:strokeColor="#FF767B83" + android:strokeWidth="1.2" + android:strokeMiterLimit="10" + android:pathData="M16.02 18.59c0 1.6-1.71 2.91-3.82 2.91-2.1 0-3.82-1.3-3.82-2.91"/> \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_notification_filled.xml b/presentation/src/main/res/drawable/ic_notification_filled.xml index 40edf3ba..e64edddb 100644 --- a/presentation/src/main/res/drawable/ic_notification_filled.xml +++ b/presentation/src/main/res/drawable/ic_notification_filled.xml @@ -1,24 +1,21 @@ - - - - - - - - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_plus_sign_green.xml b/presentation/src/main/res/drawable/ic_plus_sign_green.xml new file mode 100644 index 00000000..c78b5377 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_plus_sign_green.xml @@ -0,0 +1,11 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_post_like_checked.xml b/presentation/src/main/res/drawable/ic_post_like_checked.xml deleted file mode 100644 index b69d0dd7..00000000 --- a/presentation/src/main/res/drawable/ic_post_like_checked.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/presentation/src/main/res/drawable/ic_post_like_default.xml b/presentation/src/main/res/drawable/ic_post_like_default.xml deleted file mode 100644 index 32bee7b1..00000000 --- a/presentation/src/main/res/drawable/ic_post_like_default.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/presentation/src/main/res/drawable/ic_profile_default_small.xml b/presentation/src/main/res/drawable/ic_profile_default_small.xml new file mode 100644 index 00000000..4ffaef31 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_profile_default_small.xml @@ -0,0 +1,14 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_profile_default_user_profile.xml b/presentation/src/main/res/drawable/ic_profile_default_user_profile.xml index 955c3fbc..f9f7e78a 100644 --- a/presentation/src/main/res/drawable/ic_profile_default_user_profile.xml +++ b/presentation/src/main/res/drawable/ic_profile_default_user_profile.xml @@ -1,15 +1,13 @@ + android:width="100dp" + android:height="100dp" + android:viewportWidth="100" + android:viewportHeight="100"> + android:fillColor="#FFE8EAEE" + android:pathData="M50 100c27.61 0 50-22.39 50-50S77.61 0 50 0 0 22.39 0 50s22.39 50 50 50Z"/> - + android:fillColor="#FFC5CAD2" + android:fillType="evenOdd" + android:pathData="M29.56 21.92c0.06-1.66 1.45-2.95 3.11-2.88l11.94 0.45c1.66 0.06 2.95 1.46 2.89 3.11-0.07 1.66-1.46 2.95-3.11 2.89l-11.95-0.46c-1.65-0.06-2.94-1.45-2.88-3.11Zm5.9 18.44c3.25 0 5.88-2.63 5.88-5.87 0-3.25-2.63-5.88-5.87-5.88-3.25 0-5.88 2.63-5.88 5.88 0 3.24 2.63 5.87 5.88 5.87Zm-8.93 14.2l0.14-0.02c2.25-0.46 4.53-0.65 6.82-0.57 1.91 0.14 3.82 0.47 5.66 1 2.25 0.73 4.4 1.71 6.42 2.92 0.35 0.23 0.7 0.46 1.03 0.7-0.8 0.73-1.54 1.53-2.21 2.39-1.65 2.13-2.8 4.61-3.36 7.25-0.23 1.27-0.32 2.58-0.25 3.87 0.07 1.3 0.32 2.58 0.72 3.82 0.15 0.47 0.34 0.91 0.56 1.34 0.34 0.74 0.75 1.46 1.2 2.13 0.47 0.64 1 1.26 1.58 1.83 0.3 0.31 0.65 0.61 1.01 0.87 0.97 0.74 2.05 1.33 3.2 1.74 0.71 0.26 1.45 0.47 2.2 0.61 0.75 0.12 1.51 0.17 2.27 0.17 0.46 0 0.91-0.02 1.37-0.09 0.75-0.1 1.49-0.28 2.21-0.5 0.73-0.28 1.42-0.62 2.08-1.02 0.4-0.24 0.78-0.51 1.13-0.82 0.61-0.5 1.17-1.08 1.68-1.7 0.48-0.63 0.9-1.3 1.26-2 0.23-0.42 0.41-0.85 0.57-1.3 0.26-0.76 0.48-1.52 0.64-2.3 0.44-2.65 0.19-5.38-0.74-7.91-0.5-1.34-1.1-2.63-1.83-3.86-0.45-0.78-0.93-1.54-1.46-2.28l-0.22-0.3c0.56-0.17 1.13-0.32 1.7-0.45 1.75-0.34 3.53-0.49 5.3-0.45 1.08 0.04 2.13 0.03 3.06-0.67 0.88-0.63 1.47-1.57 1.66-2.63 0.18-1.07-0.07-2.16-0.7-3.05-0.58-0.84-1.47-1.42-2.47-1.63-1.2-0.16-2.42-0.2-3.62-0.12-1.25 0.05-2.5 0.18-3.72 0.4-2.18 0.39-4.33 0.97-6.4 1.75-0.33 0.13-0.65 0.25-0.97 0.39-1.91-1.59-3.98-2.98-6.16-4.17-2.96-1.62-6.13-2.77-9.41-3.44-1.92-0.38-3.88-0.6-5.84-0.63-1.76-0.06-3.53 0.06-5.26 0.32-1.23 0.18-2.45 0.45-3.64 0.74-0.52 0.16-0.99 0.46-1.35 0.87-0.43 0.33-0.76 0.76-0.96 1.26-0.44 0.98-0.47 2.1-0.1 3.12 0.38 1 1.15 1.83 2.13 2.28l1 0.3c0.69 0.14 1.4 0.1 2.07-0.13v-0.02Zm0.02 0c0.26-0.06 0.52-0.12 0.8-0.16l-0.68 0.14-0.12 0.03Zm0 0l-0.23 0.05 0.22-0.04Zm6.2-0.64l0.4 0.02-0.4-0.02Zm7.11 1.26l-0.4-0.12 0.4 0.12Zm12.19 9.42l0.67-0.55c1 1.24 1.91 2.57 2.69 3.98 0.42 0.88 0.75 1.8 1 2.73 0.11 0.65 0.17 1.3 0.17 1.96-0.04 0.42-0.12 0.84-0.21 1.25-0.15 0.39-0.31 0.76-0.5 1.13-0.19 0.25-0.38 0.48-0.59 0.7-0.18 0.15-0.37 0.29-0.57 0.41-0.23 0.1-0.46 0.18-0.7 0.25-0.3 0.04-0.6 0.06-0.9 0.06-0.33-0.04-0.67-0.1-1-0.18-0.36-0.13-0.71-0.3-1.05-0.48-0.29-0.2-0.56-0.43-0.82-0.67-0.26-0.33-0.5-0.68-0.72-1.04-0.2-0.45-0.37-0.91-0.5-1.39-0.11-0.66-0.17-1.33-0.16-2 0.05-0.54 0.15-1.07 0.27-1.6 0.29-0.8 0.64-1.57 1.06-2.3 0.54-0.82 1.17-1.57 1.86-2.26Zm3.04 2.8l0.16 0.3-0.16-0.3ZM56.64 72l-0.02 0.28 0.02-0.28Zm-3.03 4.55h0.07-0.07Zm-4.8-5.14v-0.2 0.2Zm0.5-2.9l-0.04 0.13 0.04-0.13Zm4.28 8.04h0.02-0.02Zm7.6-16.33l0.45-0.1-0.46 0.1ZM72.72 38.3c0 3.24-2.63 5.87-5.88 5.87-3.24 0-5.87-2.63-5.87-5.87 0-3.25 2.63-5.88 5.87-5.88 3.25 0 5.88 2.63 5.88 5.88Zm-9.86-17.2c-1.64-0.22-3.15 0.93-3.37 2.57-0.21 1.65 0.94 3.15 2.58 3.37L74 28.63c1.64 0.22 3.15-0.93 3.37-2.58 0.22-1.64-0.94-3.15-2.58-3.37l-11.9-1.58Z"/> \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_search_empty.xml b/presentation/src/main/res/drawable/ic_search_empty.xml new file mode 100644 index 00000000..7ab7f028 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_search_empty.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_sort.xml b/presentation/src/main/res/drawable/ic_sort.xml new file mode 100644 index 00000000..fb26eb37 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,11 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_swap_vertical.xml b/presentation/src/main/res/drawable/ic_swap_vertical.xml new file mode 100644 index 00000000..acc4b2f0 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_swap_vertical.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_tag.xml b/presentation/src/main/res/drawable/ic_tag.xml new file mode 100644 index 00000000..7a48d84f --- /dev/null +++ b/presentation/src/main/res/drawable/ic_tag.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_tag_small.xml b/presentation/src/main/res/drawable/ic_tag_small.xml new file mode 100644 index 00000000..6b74f9dc --- /dev/null +++ b/presentation/src/main/res/drawable/ic_tag_small.xml @@ -0,0 +1,34 @@ + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_trailing_check.xml b/presentation/src/main/res/drawable/ic_trailing_check.xml new file mode 100644 index 00000000..d152068c --- /dev/null +++ b/presentation/src/main/res/drawable/ic_trailing_check.xml @@ -0,0 +1,14 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_trailing_delete.xml b/presentation/src/main/res/drawable/ic_trailing_delete.xml new file mode 100644 index 00000000..58728cb2 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_trailing_delete.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_trailing_error.xml b/presentation/src/main/res/drawable/ic_trailing_error.xml new file mode 100644 index 00000000..5cde91e7 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_trailing_error.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_trailing_invisible.xml b/presentation/src/main/res/drawable/ic_trailing_invisible.xml new file mode 100644 index 00000000..0cdbfc81 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_trailing_invisible.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_trailing_visible.xml b/presentation/src/main/res/drawable/ic_trailing_visible.xml new file mode 100644 index 00000000..8d42d051 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_trailing_visible.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_write.xml b/presentation/src/main/res/drawable/ic_write.xml index 86895e05..4ded424f 100644 --- a/presentation/src/main/res/drawable/ic_write.xml +++ b/presentation/src/main/res/drawable/ic_write.xml @@ -1,14 +1,15 @@ + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + android:strokeColor="#FF767B83" + android:strokeWidth="1.2" + android:strokeLineJoin="round" + android:pathData="M1.93 18c0-4.26 1.69-8.35 4.71-11.36 3.01-3.01 7.1-4.71 11.36-4.71h0c4.26 0 8.35 1.69 11.36 4.71 3.01 3.01 4.71 7.1 4.71 11.36v0c0 4.26-1.69 8.35-4.71 11.36-3.01 3.01-7.1 4.71-11.36 4.71h0c-4.26 0-8.35-1.69-11.36-4.71-3.01-3.01-4.71-7.1-4.71-11.36z"/> - + android:strokeColor="#FF767B83" + android:strokeWidth="1.2" + android:pathData="M18 11.57v12.86M11.57 18h12.86"/> + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_write_filled.xml b/presentation/src/main/res/drawable/ic_write_filled.xml index f8c63fa4..f154bb07 100644 --- a/presentation/src/main/res/drawable/ic_write_filled.xml +++ b/presentation/src/main/res/drawable/ic_write_filled.xml @@ -1,14 +1,16 @@ + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + android:fillColor="#FF23C882" + android:strokeColor="#FF23C882" + android:strokeWidth="1.2" + android:strokeLineJoin="round" + android:pathData="M1.93 18c0-4.26 1.69-8.35 4.71-11.36 3.01-3.01 7.1-4.71 11.36-4.71h0c4.26 0 8.35 1.69 11.36 4.71 3.01 3.01 4.71 7.1 4.71 11.36v0c0 4.26-1.69 8.35-4.71 11.36-3.01 3.01-7.1 4.71-11.36 4.71h0c-4.26 0-8.35-1.69-11.36-4.71-3.01-3.01-4.71-7.1-4.71-11.36z"/> - + android:strokeColor="#FFFFFFFF" + android:strokeWidth="1.2" + android:pathData="M18 11.57v12.86M11.57 18h12.86"/> + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_x_sign_circle_gray.xml b/presentation/src/main/res/drawable/ic_x_sign_circle_gray.xml new file mode 100644 index 00000000..0249f49d --- /dev/null +++ b/presentation/src/main/res/drawable/ic_x_sign_circle_gray.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/presentation/src/main/res/drawable/selector_button_post_like.xml b/presentation/src/main/res/drawable/selector_button_post_like.xml index a58a2224..890700e7 100644 --- a/presentation/src/main/res/drawable/selector_button_post_like.xml +++ b/presentation/src/main/res/drawable/selector_button_post_like.xml @@ -1,7 +1,7 @@ - - - - + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/selector_button_post_like_outlined.xml b/presentation/src/main/res/drawable/selector_button_post_like_outlined.xml new file mode 100644 index 00000000..edab61d7 --- /dev/null +++ b/presentation/src/main/res/drawable/selector_button_post_like_outlined.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/font/font.xml b/presentation/src/main/res/font/font.xml new file mode 100644 index 00000000..11141671 --- /dev/null +++ b/presentation/src/main/res/font/font.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/font/pretendard_black.otf b/presentation/src/main/res/font/pretendard_black.otf new file mode 100644 index 00000000..8429a921 Binary files /dev/null and b/presentation/src/main/res/font/pretendard_black.otf differ diff --git a/presentation/src/main/res/font/pretendard_bold.otf b/presentation/src/main/res/font/pretendard_bold.otf new file mode 100644 index 00000000..e6d6ce88 Binary files /dev/null and b/presentation/src/main/res/font/pretendard_bold.otf differ diff --git a/presentation/src/main/res/font/pretendard_extra_bold.otf b/presentation/src/main/res/font/pretendard_extra_bold.otf new file mode 100644 index 00000000..07296ec6 Binary files /dev/null and b/presentation/src/main/res/font/pretendard_extra_bold.otf differ diff --git a/presentation/src/main/res/font/pretendard_extra_light.otf b/presentation/src/main/res/font/pretendard_extra_light.otf new file mode 100644 index 00000000..ad7dbef7 Binary files /dev/null and b/presentation/src/main/res/font/pretendard_extra_light.otf differ diff --git a/presentation/src/main/res/font/pretendard_light.otf b/presentation/src/main/res/font/pretendard_light.otf new file mode 100644 index 00000000..b29ee0d8 Binary files /dev/null and b/presentation/src/main/res/font/pretendard_light.otf differ diff --git a/presentation/src/main/res/font/pretendard_medium.otf b/presentation/src/main/res/font/pretendard_medium.otf new file mode 100644 index 00000000..ff907c42 Binary files /dev/null and b/presentation/src/main/res/font/pretendard_medium.otf differ diff --git a/presentation/src/main/res/font/pretendard_regular.otf b/presentation/src/main/res/font/pretendard_regular.otf new file mode 100644 index 00000000..858cdd30 Binary files /dev/null and b/presentation/src/main/res/font/pretendard_regular.otf differ diff --git a/presentation/src/main/res/font/pretendard_semi_bold.otf b/presentation/src/main/res/font/pretendard_semi_bold.otf new file mode 100644 index 00000000..fe81db7d Binary files /dev/null and b/presentation/src/main/res/font/pretendard_semi_bold.otf differ diff --git a/presentation/src/main/res/font/pretendard_thin.otf b/presentation/src/main/res/font/pretendard_thin.otf new file mode 100644 index 00000000..409558cd Binary files /dev/null and b/presentation/src/main/res/font/pretendard_thin.otf differ diff --git a/presentation/src/main/res/layout/activity_login.xml b/presentation/src/main/res/layout/activity_login.xml deleted file mode 100644 index 0cf9dde9..00000000 --- a/presentation/src/main/res/layout/activity_login.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_find_account_password_check_email.xml b/presentation/src/main/res/layout/fragment_find_account_password_check_email.xml deleted file mode 100644 index a0178def..00000000 --- a/presentation/src/main/res/layout/fragment_find_account_password_check_email.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_find_account_password_check_email_certificate.xml b/presentation/src/main/res/layout/fragment_find_account_password_check_email_certificate.xml deleted file mode 100644 index 8b72b0a1..00000000 --- a/presentation/src/main/res/layout/fragment_find_account_password_check_email_certificate.xml +++ /dev/null @@ -1,215 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/presentation/src/main/res/layout/fragment_find_account_password_complete.xml b/presentation/src/main/res/layout/fragment_find_account_password_complete.xml deleted file mode 100644 index e5a1ed71..00000000 --- a/presentation/src/main/res/layout/fragment_find_account_password_complete.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_find_account_password_new_password.xml b/presentation/src/main/res/layout/fragment_find_account_password_new_password.xml deleted file mode 100644 index acb30630..00000000 --- a/presentation/src/main/res/layout/fragment_find_account_password_new_password.xml +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_find_account_password_new_password_confirmation.xml b/presentation/src/main/res/layout/fragment_find_account_password_new_password_confirmation.xml deleted file mode 100644 index 83a9ef75..00000000 --- a/presentation/src/main/res/layout/fragment_find_account_password_new_password_confirmation.xml +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_information.xml b/presentation/src/main/res/layout/fragment_information.xml deleted file mode 100644 index 1222bd81..00000000 --- a/presentation/src/main/res/layout/fragment_information.xml +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_login.xml b/presentation/src/main/res/layout/fragment_login.xml deleted file mode 100644 index f156bfb2..00000000 --- a/presentation/src/main/res/layout/fragment_login.xml +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_login_email.xml b/presentation/src/main/res/layout/fragment_login_email.xml deleted file mode 100644 index 875af702..00000000 --- a/presentation/src/main/res/layout/fragment_login_email.xml +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_notice_detail.xml b/presentation/src/main/res/layout/fragment_notice_detail.xml deleted file mode 100644 index 5da95fb8..00000000 --- a/presentation/src/main/res/layout/fragment_notice_detail.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_notice_list.xml b/presentation/src/main/res/layout/fragment_notice_list.xml deleted file mode 100644 index bf7bc78d..00000000 --- a/presentation/src/main/res/layout/fragment_notice_list.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_notification.xml b/presentation/src/main/res/layout/fragment_notification.xml deleted file mode 100644 index 4cac9abf..00000000 --- a/presentation/src/main/res/layout/fragment_notification.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_on_boarding_first.xml b/presentation/src/main/res/layout/fragment_on_boarding_first.xml deleted file mode 100644 index 9a5d10fc..00000000 --- a/presentation/src/main/res/layout/fragment_on_boarding_first.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_on_boarding_fourth.xml b/presentation/src/main/res/layout/fragment_on_boarding_fourth.xml deleted file mode 100644 index f2430339..00000000 --- a/presentation/src/main/res/layout/fragment_on_boarding_fourth.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_on_boarding_second.xml b/presentation/src/main/res/layout/fragment_on_boarding_second.xml deleted file mode 100644 index c8911340..00000000 --- a/presentation/src/main/res/layout/fragment_on_boarding_second.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_on_boarding_third.xml b/presentation/src/main/res/layout/fragment_on_boarding_third.xml deleted file mode 100644 index 94329c12..00000000 --- a/presentation/src/main/res/layout/fragment_on_boarding_third.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_policy.xml b/presentation/src/main/res/layout/fragment_policy.xml deleted file mode 100644 index 3459062e..00000000 --- a/presentation/src/main/res/layout/fragment_policy.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_post.xml b/presentation/src/main/res/layout/fragment_post.xml deleted file mode 100644 index 268a234c..00000000 --- a/presentation/src/main/res/layout/fragment_post.xml +++ /dev/null @@ -1,435 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_report_post.xml b/presentation/src/main/res/layout/fragment_report_post.xml deleted file mode 100644 index 676f1e57..00000000 --- a/presentation/src/main/res/layout/fragment_report_post.xml +++ /dev/null @@ -1,202 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_report_user.xml b/presentation/src/main/res/layout/fragment_report_user.xml deleted file mode 100644 index c164bcad..00000000 --- a/presentation/src/main/res/layout/fragment_report_user.xml +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_search_result.xml b/presentation/src/main/res/layout/fragment_search_result.xml index 93f7f16a..14cb6961 100644 --- a/presentation/src/main/res/layout/fragment_search_result.xml +++ b/presentation/src/main/res/layout/fragment_search_result.xml @@ -114,7 +114,7 @@ android:layout_height="wrap_content" android:layout_marginTop="25dp" android:lineHeight="15.45dp" - android:text="@string/search_result_empty" + android:text="@string/search_result_empty_title" android:textAlignment="center" android:textColor="@color/gray_3_9FA5AE" android:textFontWeight="400" diff --git a/presentation/src/main/res/layout/fragment_setting.xml b/presentation/src/main/res/layout/fragment_setting.xml deleted file mode 100644 index 3df232cd..00000000 --- a/presentation/src/main/res/layout/fragment_setting.xml +++ /dev/null @@ -1,414 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_setting_block.xml b/presentation/src/main/res/layout/fragment_setting_block.xml deleted file mode 100644 index 5aee26fb..00000000 --- a/presentation/src/main/res/layout/fragment_setting_block.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_setting_change_password_current.xml b/presentation/src/main/res/layout/fragment_setting_change_password_current.xml deleted file mode 100644 index 91299067..00000000 --- a/presentation/src/main/res/layout/fragment_setting_change_password_current.xml +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_setting_change_password_new.xml b/presentation/src/main/res/layout/fragment_setting_change_password_new.xml deleted file mode 100644 index 2f9ffc27..00000000 --- a/presentation/src/main/res/layout/fragment_setting_change_password_new.xml +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_setting_notification.xml b/presentation/src/main/res/layout/fragment_setting_notification.xml deleted file mode 100644 index fa7ee5f3..00000000 --- a/presentation/src/main/res/layout/fragment_setting_notification.xml +++ /dev/null @@ -1,181 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_signup_email_complete.xml b/presentation/src/main/res/layout/fragment_signup_email_complete.xml deleted file mode 100644 index ccfd374d..00000000 --- a/presentation/src/main/res/layout/fragment_signup_email_complete.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_signup_email_set_email_address.xml b/presentation/src/main/res/layout/fragment_signup_email_set_email_address.xml deleted file mode 100644 index 7c16d0e9..00000000 --- a/presentation/src/main/res/layout/fragment_signup_email_set_email_address.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_signup_email_set_email_address_certificate.xml b/presentation/src/main/res/layout/fragment_signup_email_set_email_address_certificate.xml deleted file mode 100644 index f15e49e7..00000000 --- a/presentation/src/main/res/layout/fragment_signup_email_set_email_address_certificate.xml +++ /dev/null @@ -1,216 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_signup_email_set_password.xml b/presentation/src/main/res/layout/fragment_signup_email_set_password.xml deleted file mode 100644 index a4cba261..00000000 --- a/presentation/src/main/res/layout/fragment_signup_email_set_password.xml +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_signup_email_set_password_confirmation.xml b/presentation/src/main/res/layout/fragment_signup_email_set_password_confirmation.xml deleted file mode 100644 index 864015cb..00000000 --- a/presentation/src/main/res/layout/fragment_signup_email_set_password_confirmation.xml +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_signup_email_set_profile.xml b/presentation/src/main/res/layout/fragment_signup_email_set_profile.xml deleted file mode 100644 index 9558111e..00000000 --- a/presentation/src/main/res/layout/fragment_signup_email_set_profile.xml +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_signup_email_set_profile_image_option.xml b/presentation/src/main/res/layout/fragment_signup_email_set_profile_image_option.xml deleted file mode 100644 index d52e736d..00000000 --- a/presentation/src/main/res/layout/fragment_signup_email_set_profile_image_option.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_withdraw.xml b/presentation/src/main/res/layout/fragment_withdraw.xml deleted file mode 100644 index 52ed9c90..00000000 --- a/presentation/src/main/res/layout/fragment_withdraw.xml +++ /dev/null @@ -1,193 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_write.xml b/presentation/src/main/res/layout/fragment_write.xml deleted file mode 100644 index 3e80b8c1..00000000 --- a/presentation/src/main/res/layout/fragment_write.xml +++ /dev/null @@ -1,265 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_write_folder.xml b/presentation/src/main/res/layout/fragment_write_folder.xml deleted file mode 100644 index 7dc1af21..00000000 --- a/presentation/src/main/res/layout/fragment_write_folder.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_write_folder_add.xml b/presentation/src/main/res/layout/fragment_write_folder_add.xml deleted file mode 100644 index c4132682..00000000 --- a/presentation/src/main/res/layout/fragment_write_folder_add.xml +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_write_image_option.xml b/presentation/src/main/res/layout/fragment_write_image_option.xml deleted file mode 100644 index ecc5a440..00000000 --- a/presentation/src/main/res/layout/fragment_write_image_option.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_write_option.xml b/presentation/src/main/res/layout/fragment_write_option.xml deleted file mode 100644 index 78dd4f27..00000000 --- a/presentation/src/main/res/layout/fragment_write_option.xml +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_write_tag.xml b/presentation/src/main/res/layout/fragment_write_tag.xml deleted file mode 100644 index 22ec1698..00000000 --- a/presentation/src/main/res/layout/fragment_write_tag.xml +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/item_block.xml b/presentation/src/main/res/layout/item_block.xml deleted file mode 100644 index 12e1c38c..00000000 --- a/presentation/src/main/res/layout/item_block.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/item_feed_post.xml b/presentation/src/main/res/layout/item_feed_post.xml index 6d581722..8c5f70ee 100644 --- a/presentation/src/main/res/layout/item_feed_post.xml +++ b/presentation/src/main/res/layout/item_feed_post.xml @@ -157,11 +157,11 @@ android:layout_marginVertical="6dp" android:layout_marginStart="9dp" android:background="@android:color/transparent" - android:src="@{heart == true ? @drawable/ic_post_like_checked : @drawable/ic_post_like_default}" + android:src="@{heart == true ? @drawable/ic_heart_filled : @drawable/ic_heart_outlined}" app:layout_constraintBottom_toTopOf="@id/view_feed_post_horizontal_line" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:src="@drawable/ic_post_like_default" /> + tools:src="@drawable/ic_heart_outlined" /> - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/item_notice_post.xml b/presentation/src/main/res/layout/item_notice_post.xml deleted file mode 100644 index b11538ce..00000000 --- a/presentation/src/main/res/layout/item_notice_post.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/item_notification.xml b/presentation/src/main/res/layout/item_notification.xml deleted file mode 100644 index fae11b00..00000000 --- a/presentation/src/main/res/layout/item_notification.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/menu/menu_main_bottom.xml b/presentation/src/main/res/menu/menu_main_bottom.xml index c16ab47c..a8ba1f2e 100644 --- a/presentation/src/main/res/menu/menu_main_bottom.xml +++ b/presentation/src/main/res/menu/menu_main_bottom.xml @@ -15,11 +15,6 @@ android:enabled="true" android:icon="@drawable/selector_menu_icon_write" android:title="@string/write" /> - - - - - + android:id="@+id/SettingNotificationFragment" + android:name="daily.dayo.presentation.fragment.setting.notification.SettingNotificationFragment" + android:label="fragment_withdraw" + tools:layout="@layout/fragment_setting_notification"> - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/navigation/nav_graph_login.xml b/presentation/src/main/res/navigation/nav_graph_login.xml deleted file mode 100644 index 2faa4550..00000000 --- a/presentation/src/main/res/navigation/nav_graph_login.xml +++ /dev/null @@ -1,259 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/raw/dayo_loading.json b/presentation/src/main/res/raw/dayo_loading.json index 0e43493e..e9da614f 100644 --- a/presentation/src/main/res/raw/dayo_loading.json +++ b/presentation/src/main/res/raw/dayo_loading.json @@ -1,18 +1,18 @@ { "v": "4.8.0", "meta": { - "g": "LottieFiles AE 1.0.0", + "g": "LottieFiles AE 3.6.0", "a": "", "k": "", "d": "", "tc": "" }, - "fr": 29.9700012207031, + "fr": 60, "ip": 0, - "op": 43.0000017514259, - "w": 400, - "h": 400, - "nm": "Comp 5", + "op": 210, + "w": 246, + "h": 255, + "nm": "dayo-logo", "ddd": 0, "assets": [], "layers": [ @@ -20,7 +20,7 @@ "ddd": 0, "ind": 1, "ty": 4, - "nm": "Shape Layer 4", + "nm": "pink Outlines", "sr": 1, "ks": { "o": { @@ -34,126 +34,31 @@ "ix": 10 }, "p": { - "a": 1, + "a": 0, "k": [ - { - "i": { - "x": 0.667, - "y": 1 - }, - "o": { - "x": 0.167, - "y": 0.167 - }, - "t": 14, - "s": [ - 257.5, - 171.5, - 0 - ], - "to": [ - 0, - 0, - 0 - ], - "ti": [ - 0, - 0, - 0 - ] - }, - { - "i": { - "x": 0.058, - "y": 1 - }, - "o": { - "x": 0.333, - "y": 0 - }, - "t": 21, - "s": [ - 257.5, - 132.5, - 0 - ], - "to": [ - 0, - 0, - 0 - ], - "ti": [ - 0, - 0, - 0 - ] - }, - { - "t": 32.0000013033867, - "s": [ - 257.5, - 171.5, - 0 - ] - } + 84.378, + 108.802, + 0 ], "ix": 2 }, "a": { "a": 0, "k": [ - -58.75, - -28.5, + 66.242, + 105.052, 0 ], "ix": 1 }, "s": { - "a": 1, + "a": 0, "k": [ - { - "i": { - "x": [ - 0.833, - 0.833, - 0.833 - ], - "y": [ - 0.833, - 0.833, - 0.833 - ] - }, - "o": { - "x": [ - 0.167, - 0.167, - 0.167 - ], - "y": [ - 0.167, - 0.167, - 0.167 - ] - }, - "t": 14, - "s": [ - 0, - 0, - 100 - ] - }, - { - "t": 24.00000097754, - "s": [ - 100, - 100, - 100 - ] - } + 100, + 100, + 100 ], - "ix": 6, - "x": "var $bm_rt;\nvar n, n, t, t, v, amp, freq, decay;\n$bm_rt = n = 0;\nif (numKeys > 0) {\n $bm_rt = n = nearestKey(time).index;\n if (key(n).time > time) {\n n--;\n }\n}\nif (n == 0) {\n $bm_rt = t = 0;\n} else {\n $bm_rt = t = $bm_sub(time, key(n).time);\n}\nif (n > 0) {\n v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 15)));\n amp = 0.05;\n freq = 4;\n decay = 8;\n $bm_rt = $bm_sum(value, $bm_div($bm_mul($bm_mul(v, amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))));\n} else {\n $bm_rt = value;\n}" + "ix": 6 } }, "ao": 0, @@ -170,56 +75,56 @@ "k": { "i": [ [ - 5.915, + 0, 0 ], [ 0, - -5.915 + 0 ], [ - -5.915, + 0, 0 ], [ 0, - 5.915 + 0 ] ], "o": [ [ - -5.915, + 0, 0 ], [ 0, - 5.915 + 0 ], [ - 5.915, + 0, 0 ], [ 0, - -5.915 + 0 ] ], "v": [ [ - -0.375, - -10.835 + -10.184, + -11.523 ], [ - -11.085, - -0.125 + -13.758, + 6.745 ], [ - -0.375, - 10.585 + 10.184, + 11.523 ], [ - 10.335, - -0.125 + 13.758, + -6.744 ] ], "c": true @@ -233,68 +138,197 @@ { "ty": "fl", "c": { + "a": 0, + "k": [ + 0.952941236309, + 0.301960784314, + 0.478431402468, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 14.008, + 198.33 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { "a": 1, "k": [ { "i": { "x": [ + 0.667, 0.667 ], "y": [ + 1, 1 ] }, "o": { "x": [ - 0.333 - ], - "y": [ - 0 - ] - }, - "t": 14, - "s": [ - 0.827450990677, - 0.823529422283, - 0.823529422283, - 1 - ] - }, - { - "i": { - "x": [ - 0.667 - ], - "y": [ + 1, 1 - ] - }, - "o": { - "x": [ - 0.333 ], "y": [ + 0, 0 ] }, - "t": 21, + "t": 90, "s": [ - 0.827450990677, - 0.823529422283, - 0.823529422283, - 1 + 0, + 0 ] }, { - "t": 27.0000010997325, + "t": 110, "s": [ - 0.137254901961, - 0.78431372549, - 0.509803921569, - 1 + 100, + 100 ] } ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 4.333, + -16.031 + ], + [ + -13.122, + -10.027 + ], + [ + -4.333, + 16.031 + ], + [ + 13.122, + 10.027 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.952941236309, + 0.301960784314, + 0.478431402468, + 1 + ], "ix": 4 }, "o": { @@ -313,8 +347,8 @@ "p": { "a": 0, "k": [ - -58.54, - -28.79 + 119.112, + 16.281 ], "ix": 2 }, @@ -327,10 +361,700 @@ "ix": 1 }, "s": { - "a": 0, + "a": 1, "k": [ - 100, - 100 + { + "i": { + "x": [ + 0.667, + 0.667 + ], + "y": [ + 1, + 1 + ] + }, + "o": { + "x": [ + 1, + 1 + ], + "y": [ + 0, + 0 + ] + }, + "t": 120, + "s": [ + 0, + 0 + ] + }, + { + "t": 140, + "s": [ + 100, + 100 + ] + } + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 210, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "blue Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 57.518, + 89.682, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 26.926, + 64.665, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 0.108, + -17.03 + ], + [ + -15.483, + 6.749 + ], + [ + -0.108, + 17.03 + ], + [ + 15.483, + -6.749 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.349019607843, + 0.486274539723, + 0.949019667682, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 15.733, + 17.28 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667 + ], + "y": [ + 1, + 1 + ] + }, + "o": { + "x": [ + 1, + 1 + ], + "y": [ + 0, + 0 + ] + }, + "t": 100, + "s": [ + 0, + 0 + ] + }, + { + "t": 120, + "s": [ + 100, + 100 + ] + } + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -6.855, + -13.222 + ], + [ + -11.247, + 9.682 + ], + [ + 6.855, + 13.222 + ], + [ + 11.247, + -9.682 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.349019607843, + 0.486274539723, + 0.949019667682, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 42.355, + 115.857 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667 + ], + "y": [ + 1, + 1 + ] + }, + "o": { + "x": [ + 1, + 1 + ], + "y": [ + 0, + 0 + ] + }, + "t": 130, + "s": [ + 0, + 0 + ] + }, + { + "t": 150, + "s": [ + 100, + 100 + ] + } + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 210, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "yellow Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 112.929, + 90.216, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 111.179, + 23.677, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 6.232, + -14.956 + ], + [ + -16.047, + -0.809 + ], + [ + -6.231, + 14.956 + ], + [ + 16.047, + 0.809 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 0.921568687289, + 0.352941176471, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 206.062, + 15.206 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667 + ], + "y": [ + 1, + 1 + ] + }, + "o": { + "x": [ + 1, + 1 + ], + "y": [ + 0, + 0 + ] + }, + "t": 110, + "s": [ + 0, + 0 + ] + }, + { + "t": 130, + "s": [ + 100, + 100 + ] + } ], "ix": 3 }, @@ -357,162 +1081,14 @@ "nm": "Transform" } ], - "nm": "Ellipse 1", - "np": 3, + "nm": "Group 1", + "np": 2, "cix": 2, "bm": 0, "ix": 1, "mn": "ADBE Vector Group", "hd": false - } - ], - "ip": 0, - "op": 150.000006109625, - "st": 0, - "bm": 0 - }, - { - "ddd": 0, - "ind": 2, - "ty": 4, - "nm": "Shape Layer 5", - "sr": 1, - "ks": { - "o": { - "a": 0, - "k": 100, - "ix": 11 - }, - "r": { - "a": 0, - "k": 0, - "ix": 10 - }, - "p": { - "a": 1, - "k": [ - { - "i": { - "x": 0.667, - "y": 1 - }, - "o": { - "x": 0.167, - "y": 0.167 - }, - "t": 7, - "s": [ - 199.375, - 171.5, - 0 - ], - "to": [ - 0, - 0, - 0 - ], - "ti": [ - 0, - 0, - 0 - ] - }, - { - "i": { - "x": 0.058, - "y": 1 - }, - "o": { - "x": 0.333, - "y": 0 - }, - "t": 14, - "s": [ - 199.375, - 132.5, - 0 - ], - "to": [ - 0, - 0, - 0 - ], - "ti": [ - 0, - 0, - 0 - ] - }, - { - "t": 25.0000010182709, - "s": [ - 199.375, - 171.5, - 0 - ] - } - ], - "ix": 2 - }, - "a": { - "a": 0, - "k": [ - -58.75, - -28.5, - 0 - ], - "ix": 1 }, - "s": { - "a": 1, - "k": [ - { - "i": { - "x": [ - 0.833, - 0.833, - 0.833 - ], - "y": [ - 0.833, - 0.833, - 0.833 - ] - }, - "o": { - "x": [ - 0.167, - 0.167, - 0.167 - ], - "y": [ - 0.167, - 0.167, - 0.167 - ] - }, - "t": 7, - "s": [ - 0, - 0, - 100 - ] - }, - { - "t": 17.0000006924242, - "s": [ - 100, - 100, - 100 - ] - } - ], - "ix": 6, - "x": "var $bm_rt;\nvar n, n, t, t, v, amp, freq, decay;\n$bm_rt = n = 0;\nif (numKeys > 0) {\n $bm_rt = n = nearestKey(time).index;\n if (key(n).time > time) {\n n--;\n }\n}\nif (n == 0) {\n $bm_rt = t = 0;\n} else {\n $bm_rt = t = $bm_sub(time, key(n).time);\n}\nif (n > 0) {\n v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 15)));\n amp = 0.05;\n freq = 4;\n decay = 8;\n $bm_rt = $bm_sum(value, $bm_div($bm_mul($bm_mul(v, amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))));\n} else {\n $bm_rt = value;\n}" - } - }, - "ao": 0, - "shapes": [ { "ty": "gr", "it": [ @@ -525,56 +1101,56 @@ "k": { "i": [ [ - 5.915, + 0, 0 ], [ 0, - -5.915 + 0 ], [ - -5.915, + 0, 0 ], [ 0, - 5.915 + 0 ] ], "o": [ [ - -5.915, + 0, 0 ], [ 0, - 5.915 + 0 ], [ - 5.915, + 0, 0 ], [ 0, - -5.915 + 0 ] ], "v": [ [ - -0.375, - -10.835 + 14.12, + -9.311 ], [ - -11.085, - -0.125 + -14.12, + -9.311 ], [ - -0.375, - 10.585 + -14.12, + 9.311 ], [ - 10.335, - -0.125 + 14.12, + 9.311 ] ], "c": true @@ -588,67 +1164,12 @@ { "ty": "fl", "c": { - "a": 1, + "a": 0, "k": [ - { - "i": { - "x": [ - 0.667 - ], - "y": [ - 1 - ] - }, - "o": { - "x": [ - 0.333 - ], - "y": [ - 0 - ] - }, - "t": 7, - "s": [ - 0.827450990677, - 0.823529422283, - 0.823529422283, - 1 - ] - }, - { - "i": { - "x": [ - 0.667 - ], - "y": [ - 1 - ] - }, - "o": { - "x": [ - 0.333 - ], - "y": [ - 0 - ] - }, - "t": 15, - "s": [ - 0.827450990677, - 0.823529422283, - 0.823529422283, - 1 - ] - }, - { - "t": 21.0000008553475, - "s": [ - 0.137254901961, - 0.78431372549, - 0.509803921569, - 1 - ] - } + 1, + 0.921568687289, + 0.352941176471, + 1 ], "ix": 4 }, @@ -668,8 +1189,8 @@ "p": { "a": 0, "k": [ - -58.54, - -28.79 + 14.37, + 37.793 ], "ix": 2 }, @@ -682,10 +1203,42 @@ "ix": 1 }, "s": { - "a": 0, + "a": 1, "k": [ - 100, - 100 + { + "i": { + "x": [ + 0.667, + 0.667 + ], + "y": [ + 1, + 1 + ] + }, + "o": { + "x": [ + 1, + 1 + ], + "y": [ + 0, + 0 + ] + }, + "t": 80, + "s": [ + 0, + 0 + ] + }, + { + "t": 100, + "s": [ + 100, + 100 + ] + } ], "ix": 3 }, @@ -712,25 +1265,25 @@ "nm": "Transform" } ], - "nm": "Ellipse 1", - "np": 3, + "nm": "Group 2", + "np": 2, "cix": 2, "bm": 0, - "ix": 1, + "ix": 2, "mn": "ADBE Vector Group", "hd": false } ], "ip": 0, - "op": 150.000006109625, + "op": 210, "st": 0, "bm": 0 }, { "ddd": 0, - "ind": 3, + "ind": 4, "ty": 4, - "nm": "Shape Layer 1", + "nm": "main Outlines", "sr": 1, "ks": { "o": { @@ -744,126 +1297,31 @@ "ix": 10 }, "p": { - "a": 1, + "a": 0, "k": [ - { - "i": { - "x": 0.667, - "y": 1 - }, - "o": { - "x": 0.167, - "y": 0.167 - }, - "t": 0, - "s": [ - 141.25, - 171.5, - 0 - ], - "to": [ - 0, - 0, - 0 - ], - "ti": [ - 0, - 0, - 0 - ] - }, - { - "i": { - "x": 0.058, - "y": 1 - }, - "o": { - "x": 0.333, - "y": 0 - }, - "t": 7, - "s": [ - 141.25, - 132.5, - 0 - ], - "to": [ - 0, - 0, - 0 - ], - "ti": [ - 0, - 0, - 0 - ] - }, - { - "t": 18.000000733155, - "s": [ - 141.25, - 171.5, - 0 - ] - } + 164.113, + 151.484, + 0 ], "ix": 2 }, "a": { "a": 0, "k": [ - -58.75, - -28.5, + 144.67, + 165.957, 0 ], "ix": 1 }, "s": { - "a": 1, + "a": 0, "k": [ - { - "i": { - "x": [ - 0.833, - 0.833, - 0.833 - ], - "y": [ - 0.833, - 0.833, - 0.833 - ] - }, - "o": { - "x": [ - 0.167, - 0.167, - 0.167 - ], - "y": [ - 0.167, - 0.167, - 0.167 - ] - }, - "t": 0, - "s": [ - 0, - 0, - 100 - ] - }, - { - "t": 10.0000004073083, - "s": [ - 100, - 100, - 100 - ] - } + 100, + 100, + 100 ], - "ix": 6, - "x": "var $bm_rt;\nvar n, n, t, t, v, amp, freq, decay;\n$bm_rt = n = 0;\nif (numKeys > 0) {\n $bm_rt = n = nearestKey(time).index;\n if (key(n).time > time) {\n n--;\n }\n}\nif (n == 0) {\n $bm_rt = t = 0;\n} else {\n $bm_rt = t = $bm_sub(time, key(n).time);\n}\nif (n > 0) {\n v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 15)));\n amp = 0.05;\n freq = 4;\n decay = 8;\n $bm_rt = $bm_sum(value, $bm_div($bm_mul($bm_mul(v, amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))));\n} else {\n $bm_rt = value;\n}" + "ix": 6 } }, "ao": 0, @@ -880,59 +1338,83 @@ "k": { "i": [ [ - 5.915, + 0, 0 ], [ - 0, - -5.915 + 19.81, + -76.482 ], [ - -5.915, + 19.528, 0 ], [ - 0, - 5.915 + 2.283, + 18.81 + ], + [ + -13.622, + 8.87 + ], + [ + -35.756, + -29.043 ] ], "o": [ [ - -5.915, - 0 + 30.162, + 14.446 ], [ - 0, - 5.915 + -3.117, + 12.035 ], [ - 5.915, + -9.035, 0 ], + [ + -1.958, + -16.137 + ], + [ + 18.789, + -12.235 + ], [ 0, - -5.915 + 0 ] ], "v": [ [ - -0.375, - -10.835 + -63.408, + -84.695 + ], + [ + 7.491, + 55.949 ], [ - -11.085, - -0.125 + -26.865, + 84.695 ], [ - -0.375, - 10.585 + -55.005, + 58.717 ], [ - 10.335, - -0.125 + -35.199, + 16.357 + ], + [ + 63.408, + 29.644 ] ], - "c": true + "c": false }, "ix": 2 }, @@ -941,81 +1423,33 @@ "hd": false }, { - "ty": "fl", + "ty": "st", "c": { - "a": 1, + "a": 0, "k": [ - { - "i": { - "x": [ - 0.667 - ], - "y": [ - 1 - ] - }, - "o": { - "x": [ - 0.333 - ], - "y": [ - 0 - ] - }, - "t": 0, - "s": [ - 0.827450990677, - 0.823529422283, - 0.823529422283, - 1 - ] - }, - { - "i": { - "x": [ - 0.667 - ], - "y": [ - 1 - ] - }, - "o": { - "x": [ - 0.333 - ], - "y": [ - 0 - ] - }, - "t": 9, - "s": [ - 0.827450990677, - 0.823529422283, - 0.823529422283, - 1 - ] - }, - { - "t": 16.0000006516934, - "s": [ - 0.137254901961, - 0.78431372549, - 0.509803921569, - 1 - ] - } + 0.364705882353, + 0.823529471603, + 0.603921568627, + 1 ], - "ix": 4 + "ix": 3 }, "o": { "a": 0, "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 32.505, "ix": 5 }, - "r": 1, + "lc": 2, + "lj": 1, + "ml": 10, "bm": 0, - "nm": "Fill 1", - "mn": "ADBE Vector Graphic - Fill", + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", "hd": false }, { @@ -1023,8 +1457,8 @@ "p": { "a": 0, "k": [ - -58.54, - -28.79 + 144.67, + 165.957 ], "ix": 2 }, @@ -1067,17 +1501,69 @@ "nm": "Transform" } ], - "nm": "Ellipse 1", - "np": 3, + "nm": "Group 1", + "np": 2, "cix": 2, "bm": 0, "ix": 1, "mn": "ADBE Vector Group", "hd": false + }, + { + "ty": "tm", + "s": { + "a": 0, + "k": 0, + "ix": 1 + }, + "e": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.331 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.153 + ], + "y": [ + 0 + ] + }, + "t": 20, + "s": [ + 0 + ] + }, + { + "t": 80, + "s": [ + 100 + ] + } + ], + "ix": 2 + }, + "o": { + "a": 0, + "k": 0, + "ix": 3 + }, + "m": 1, + "ix": 2, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false } ], "ip": 0, - "op": 150.000006109625, + "op": 210, "st": 0, "bm": 0 } diff --git a/presentation/src/main/res/values/colors.xml b/presentation/src/main/res/values/colors.xml index 49bf635b..6ee584cc 100644 --- a/presentation/src/main/res/values/colors.xml +++ b/presentation/src/main/res/values/colors.xml @@ -18,6 +18,7 @@ #FF000000 #FFFFFFFF + #4DFFFFFF #FF313131 #66313131 #FF767B83 diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 96276955..59f0ed44 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -1,24 +1,40 @@ DAYO + Dayo Logo + dayo.jkhj@gmail.com ํ™ˆ ํ”ผ๋“œ ๊ธ€์“ฐ๊ธฐ ์•Œ๋ฆผ - ๋งˆ์ด ํŽ˜์ด์ง€ + MY + ํƒœ๊ทธ + ์ด์ „ ๋‹ค์Œ ํ™•์ธ ์™„๋ฃŒ + ์ œ์ถœ + ์ €์žฅ ์ทจ์†Œ + ๋ชจ๋‘ ์„ ํƒ ์‚ญ์ œ + ์ด๋™ ์ „์ฒด์‚ญ์ œ ์ˆ˜์ • ๋‹ซ๊ธฐ + ๋กœ๊ทธ์ธ ๋กœ๊ทธ์ธ ์ข‹์•„์š” ๋Œ“๊ธ€ + ์ถ”๊ฐ€ + ์นดํ…Œ๊ณ ๋ฆฌ ๋ฒ„์ „ ์ •๋ณด ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค + ๋’ค๋กœ๊ฐ€๊ธฐ + ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค ์ฃผ์„ธ์š” + ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š” + ๋‹ซ๊ธฐ + ์žฌ์‹œ๋„ %d%s @@ -46,6 +62,17 @@ ๋‚ด๊ฐ€ ์—ด์‹ฌํžˆ ๊พธ๋ฏผ ๋‹ค์ด์–ด๋ฆฌ, ์— ๊ณต์œ ํ•˜๋Ÿฌ ๊ฐ€๋ณผ๊นŒ์š”? + + ์นด์นด์˜ค ๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์–ด์š” + ๋กœ๊ทธ์ธ + ์ด๋ฉ”์ผ + ์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š” + ํŒจ์Šค์›Œ๋“œ + ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š” + ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ + ์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž˜๋ชป ์ž…๋ ฅํ–ˆ์–ด์š” + ์•„์ง ๊ณ„์ •์ด ์—†์œผ์‹ ๊ฐ€์š”? + ์•„์ง ๊ณ„์ •์ด ์—†์œผ์‹ ๊ฐ€์š”? ํšŒ์›๊ฐ€์ž…ํ•˜๊ธฐ ์นด์นด์˜คํ†ก์œผ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ @@ -57,27 +84,30 @@ ์•„์ง ๊ณ„์ •์ด ์—†์œผ์‹ ๊ฐ€์š”? ํšŒ์›๊ฐ€์ž…ํ•˜๊ธฐ ๊ฐ€์ž… ์‹œ DAYO์˜ ์ด์šฉ์•ฝ๊ด€ ๋ฐ ๊ฐœ์ธ์ •๋ณด ์ทจ๊ธ‰๋ฐฉ์นจ์— ๋™์˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. - - ์ด์šฉ์•ฝ๊ด€ - ๊ฐœ์ธ์ •๋ณด ์ทจ๊ธ‰๋ฐฉ์นจ + + ๊ฐœ์ธ์ •๋ณด ์ทจ๊ธ‰๋ฐฉ์นจ + ์ด์šฉ์•ฝ๊ด€ DAYO PICK New + ์ง€๊ธˆ ์ธ๊ธฐ์žˆ๋Š” ๊ฒŒ์‹œ๊ธ€์ด์—์š”! ์ธ๊ธฐ์žˆ๋Š” ๋‹ค๊พธ๊ฐ€ ์•„์ง ์—†์–ด์š” ๋‹ค๊พธ๋“ค์„ ๊ตฌ๊ฒฝํ•˜๊ณ  ๋ฐ˜์‘์„ ๋‚จ๊ฒจ๋ณด์„ธ์š” ๊ตฌ๊ฒฝํ•˜๋Ÿฌ ๊ฐ€๊ธฐ + ์‹ค์‹œ๊ฐ„์œผ๋กœ ์˜ฌ๋ผ์˜จ ๊ฒŒ์‹œ๊ธ€์ด์—์š”! ์ƒˆ๋กœ์šด ๋‹ค๊พธ๊ฐ€ ์•„์ง ์—†์–ด์š” ์—ฌ๋Ÿฌ๋ถ„์˜ ๋‹ค๊พธ๋ฅผ ๊ณต์œ ํ•ด๋ณด์„ธ์š” ๊ณต์œ ํ•˜๋Ÿฌ ๊ฐ€๊ธฐ + ํ•„ํ„ฐ ์ „์ฒด ์Šค์ผ€์ค„๋Ÿฌ ์Šคํ„ฐ๋”” ํ”Œ๋ž˜๋„ˆ ํฌ์ผ“๋ถ 6๊ณต ๋‹ค์ด์–ด๋ฆฌ - ๊ตฟ๋…ธํŠธ + ๋ชจ๋ฐ”์ผ ๋‹ค์ด์–ด๋ฆฌ ๊ธฐํƒ€ ALL SCHEDULER @@ -97,10 +127,24 @@ ์•„์ง ํ”ผ๋“œ์— ๊ฒŒ์‹œ๋ฌผ์ด ์—†์–ด์š”. - ์‚ฌ๋žŒ๋“ค์„ ํŒ”๋กœ์šฐํ•˜๋ฉด\n์ด๊ณณ์—์„œ ๊ฒŒ์‹œ๋ฌผ์„ ๋ชจ์•„๋ณผ ์ˆ˜ ์žˆ์–ด์š”. + ํŒ”๋กœ์šฐํ•˜๊ณ  ๋‚ด ์ทจํ–ฅ์˜ ๋‹ค๊พธ๋“ค์„ ๋ชจ์•„๋ณด์„ธ์š” ํŒ”๋กœ์šฐํ•˜๋Ÿฌ ๊ฐ€๊ธฐ + ์ด๋ฏธ์ง€ ํŽธ์ง‘ + ์ด๋ฏธ์ง€ ํŽธ์ง‘ + ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ + ๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ • + ์˜ฌ๋ฆฌ๊ธฐ + ๊พธ๋ฏผ ๋‹ค์ด์–ด๋ฆฌ์— ๋Œ€ํ•ด ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”. + / %d + ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ + ํƒœ๊ทธ + ํƒœ๊ทธ ์ž…๋ ฅ + #%s + ํด๋” + ํด๋” ์„ ํƒ + ๋‚ด์šฉ์„ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. / ์ตœ๋Œ€ 200์ž ์˜ฌ๋ฆฌ๊ธฐ @@ -111,11 +155,28 @@ ๊ณต๊ฐœ ์„ค์ • ํƒœ๊ทธ ์ถ”๊ฐ€ ๊ธฐ๋ก์— ๊ด€ํ•œ ํ‚ค์›Œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”. + + + ํƒœ๊ทธ + ์–ด๋–ค ์ฃผ์ œ์˜ ๋‹ค๊พธ์ธ๊ฐ€์š”? + # ํƒœ๊ทธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” / %d ํƒœ๊ทธ๋Š” %d๊ฐœ๊นŒ์ง€ ๋“ฑ๋ก ๊ฐ€๋Šฅํ•ด์š”. ํƒœ๊ทธ๋Š” %d์ž ๊นŒ์ง€๋งŒ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์–ด์š”! - ํด๋” ์„ค์ • + + + ํด๋” ์„ ํƒ + ์ƒˆ๋กœ์šด ํด๋” ์ถ”๊ฐ€ + %d๊ฐœ + + ํด๋” ์ถ”๊ฐ€ + ํด๋” ์ด๋ฆ„ + 12์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š” + ํด๋” ์†Œ๊ฐœ + 20์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š” + ๋น„๊ณต๊ฐœ ํด๋” + ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”. ์‚ฌ์ง„์„ ์ถ”๊ฐ€ํ•ด ์ฃผ์„ธ์š”. ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์„ ํƒํ•ด ์ฃผ์„ธ์š”. @@ -127,6 +188,12 @@ ๊ฒŒ์‹œ๊ธ€์„ ์˜ฌ๋ฆฌ๊ณ  ์žˆ์–ด์š”! ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”. ๊ฒŒ์‹œ๊ธ€์„ ์—…๋กœ๋“œ ํ•  ์ˆ˜ ์—†์–ด์š”. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”. + + ๊ฒŒ์‹œ๊ธ€์„ ์˜ฌ๋ฆฌ๊ณ  ์žˆ์–ด์š”! ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”. + ๊ฒŒ์‹œ๊ธ€์„ ์—…๋กœ๋“œํ•  ์ˆ˜ ์—†์–ด์š”. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”. + ๊ฒŒ์‹œ๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”. + + ๋Œ“๊ธ€์„ ๋‚จ๊ฒจ์ฃผ์„ธ์š” ๊ฒŒ์‹œ @@ -137,7 +204,12 @@ ์‚ฌ์šฉ์ž ์‹ ๊ณ  ๊ฒŒ์‹œ๋ฌผ ์‹ ๊ณ  ์ด ๊ฒŒ์‹œ๋ฌผ ์ˆจ๊ธฐ๊ธฐ - ์•„์ง ์ž‘์„ฑ๋œ ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค. + ์ด ๊ธ€์„ + ๋ช…์ด ์ข‹์•„ํ•ด์š” + ๊ฐœ์˜ ๋Œ“๊ธ€ + ๋”๋ณด๊ธฐ + ์•„์ง ๋Œ“๊ธ€์ด ์—†์–ด์š” + ์ด ๊ฒŒ์‹œ๊ธ€์— ๋Œ€ํ•ด ๋Œ“๊ธ€์„ ๋‚จ๊ฒจ์ฃผ์„ธ์š” ๋Œ“๊ธ€์„ ์‚ญ์ œํ• ๊นŒ์š”? ๋Œ“๊ธ€์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋Œ“๊ธ€ ์‚ญ์ œ๋ฅผ ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”. @@ -148,46 +220,73 @@ ๊ฒŒ์‹œ๋ฌผ ์‚ญ์ œ ๊ฒŒ์‹œ๋ฌผ์„ ์‚ญ์ œํ• ๊นŒ์š”? + + ๋‚จ๊ธฐ๊ธฐ + ๋Œ“๊ธ€์ด ์‚ญ์ œ๋˜์—ˆ์–ด์š”. + ์‹ ๊ณ ๊ฐ€ ์ ‘์ˆ˜๋˜์—ˆ์–ด์š”. + - ์‹ ๊ณ ํ•˜๊ธฐ + ๊ฒŒ์‹œ๋ฌผ ์‹ ๊ณ  ์‹ ๊ณ ํ•˜๊ธฐ ๊ฒŒ์‹œ๊ธ€์„ ์‹ ๊ณ ํ•˜์‹œ๊ฒ ์–ด์š”? ๊ฒŒ์‹œ๊ธ€์„ ์‹ ๊ณ ํ•˜๋Š” ์ด์œ ์— ๋Œ€ํ•ด ์•Œ๋ ค์ฃผ์„ธ์š”. - ์„ฑ์ ์ธ ๋‚ด์šฉ์ด ํฌํ•จ๋ผ์žˆ์–ด์š”. - ํญ๋ ฅ์ ์ด๊ฑฐ๋‚˜ ์‹ฌํ•œ ์š•์„ค์„ ์‚ฌ์šฉํ•ด์š”. - ์ž˜๋ชป๋œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ด์š”. - ์œ ํ•ดํ•˜๊ฑฐ๋‚˜ ์œ„ํ—˜ํ•œ ๋‚ด์šฉ์ด์—์š”. - ๋ถˆ๋ฒ•์ ์ธ ๋‚ด์šฉ์ด ํฌํ•จ๋ผ์žˆ์–ด์š”. - ๋„ˆ๋ฌด ๋งŽ์€ ๊ด‘๊ณ ๋ฅผ ํ•ด์š”. - ๊ฐ™์€ ๋‚ด์šฉ์œผ๋กœ ๋„๋ฐฐ๊ฐ€ ๋ผ์žˆ์–ด์š”. - ๊ธฐํƒ€ - ์‹ ๊ณ  ์‚ฌ์œ ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. (100์ž ์ด๋‚ด) + ๊ฒŒ์‹œ๋ฌผ์„ ์‹ ๊ณ ํ•˜๋Š” ๊ธฐํƒ€ ์‚ฌ์œ ๋Š” ๋ฌด์—‡์ธ๊ฐ€์š”? ์‹ ๊ณ ๊ฐ€ ์ ‘์ˆ˜๋˜์—ˆ์–ด์š”. + + ์„ฑ์ ์ธ ๋‚ด์šฉ์ด ํฌํ•จ๋˜์–ด ์žˆ์–ด์š” + ํญ๋ ฅ์ ์ด๊ฑฐ๋‚˜ ์‹ฌํ•œ ์š•์„ค์„ ์‚ฌ์šฉํ•ด์š” + ์ž˜๋ชป๋œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ด์š” + ์œ ํ•ดํ•˜๊ฑฐ๋‚˜ ์œ„ํ—˜ํ•œ ๋‚ด์šฉ์ด์—์š” + ๋ถˆ๋ฒ•์ ์ธ ๋‚ด์šฉ์ด ํฌํ•จ๋˜์–ด ์žˆ์–ด์š” + ๋„ˆ๋ฌด ๋งŽ์€ ๊ด‘๊ณ ๋ฅผ ํ•ด์š”. + ๊ฐ™์€ ๋‚ด์šฉ์œผ๋กœ ๋„๋ฐฐ๊ฐ€ ๋˜์–ด ์žˆ์–ด์š” + ๊ธฐํƒ€ + + + ๋Œ“๊ธ€ ์‹ ๊ณ  + ์‹ ๊ณ ํ•˜๊ธฐ + ๋Œ“๊ธ€์„ ์‹ ๊ณ ํ•˜์‹œ๊ฒ ์–ด์š”? + ๋Œ“๊ธ€์„ ์‹ ๊ณ ํ•˜๋Š” ์ด์œ ์— ๋Œ€ํ•ด ์•Œ๋ ค์ฃผ์„ธ์š”. + ์‹ ๊ณ ๊ฐ€ ์ ‘์ˆ˜๋˜์—ˆ์–ด์š”. + + ์„ฑ์ ์ธ ๋‚ด์šฉ์ด ํฌํ•จ๋˜์–ด ์žˆ์–ด์š” + ํญ๋ ฅ์ ์ด๊ฑฐ๋‚˜ ์‹ฌํ•œ ์š•์„ค์„ ์‚ฌ์šฉํ•ด์š” + ์ž˜๋ชป๋œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ด์š” + ์œ ํ•ดํ•˜๊ฑฐ๋‚˜ ์œ„ํ—˜ํ•œ ๋‚ด์šฉ์ด์—์š” + ๋ถˆ๋ฒ•์ ์ธ ๋‚ด์šฉ์ด ํฌํ•จ๋˜์–ด ์žˆ์–ด์š” + ๋„ˆ๋ฌด ๋งŽ์€ ๊ด‘๊ณ ๋ฅผ ํ•ด์š”. + ๊ฐ™์€ ๋‚ด์šฉ์œผ๋กœ ๋„๋ฐฐ๊ฐ€ ๋ผ์žˆ์–ด์š”. + ์‹ ๊ณ ํ•˜๊ธฐ ์‹ ๊ณ ํ•˜๊ธฐ ์‚ฌ์šฉ์ž๋ฅผ ์‹ ๊ณ ํ•˜์‹œ๊ฒ ์–ด์š”? ์‚ฌ์šฉ์ž๋ฅผ ์‹ ๊ณ ํ•˜๋Š” ์ด์œ ์— ๋Œ€ํ•ด ์•Œ๋ ค์ฃผ์„ธ์š”. - ์„ฑ์ ์ธ ๋‚ด์šฉ์„ ๊ฒŒ์‹œํ•ด์š”. - ํญ๋ ฅ์ ์ด๊ฑฐ๋‚˜ ์‹ฌํ•œ ์š•์„ค์„ ์‚ฌ์šฉํ•ด์š”. - ์ž˜๋ชป๋œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ด์š”. - ์œ ํ•ดํ•˜๊ฑฐ๋‚˜ ์œ„ํ—˜ํ•œ ๋‚ด์šฉ์„ ๊ฒŒ์‹œํ•ด์š”. - ๋ถˆ๋ฒ•์ ์ธ ๋‚ด์šฉ์„ ๊ฒŒ์‹œํ•ด์š”. - ๋„ˆ๋ฌด ๋งŽ์€ ๊ด‘๊ณ ๋ฅผ ํ•ด์š”. - ๊ฐ™์€ ๋‚ด์šฉ์œผ๋กœ ๋„๋ฐฐ๊ฐ€ ๋ผ์žˆ์–ด์š”. - ๊ธฐํƒ€ ์‹ ๊ณ  ์‚ฌ์œ ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. (100์ž ์ด๋‚ด) ์‹ ๊ณ ๊ฐ€ ์ ‘์ˆ˜๋˜์—ˆ์–ด์š”. + + ์„ฑ์ ์ธ ๋‚ด์šฉ์„ ๊ฒŒ์‹œํ•ด์š”. + ํญ๋ ฅ์ ์ด๊ฑฐ๋‚˜ ์‹ฌํ•œ ์š•์„ค์„ ์‚ฌ์šฉํ•ด์š”. + ์ž˜๋ชป๋œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ด์š”. + ์œ ํ•ดํ•˜๊ฑฐ๋‚˜ ์œ„ํ—˜ํ•œ ๋‚ด์šฉ์„ ๊ฒŒ์‹œํ•ด์š”. + ๋ถˆ๋ฒ•์ ์ธ ๋‚ด์šฉ์„ ๊ฒŒ์‹œํ•ด์š”. + ๋„ˆ๋ฌด ๋งŽ์€ ๊ด‘๊ณ ๋ฅผ ํ•ด์š”. + ๊ฐ™์€ ๋‚ด์šฉ์œผ๋กœ ๋„๋ฐฐ๊ฐ€ ๋ผ์žˆ์–ด์š”. + ๊ธฐํƒ€ + ์ข‹์•„์š” ํด๋” ์ถ”๊ฐ€ + ํด๋” ์ถ”๊ฐ€ + ํด๋” ์˜ต์…˜ ํด๋” ํŽธ์ง‘ ํด๋” ์ด๋ฆ„ ํด๋” ์ด๋ฆ„ ํด๋” ์†Œ๊ฐœ + ๋น„๊ณต๊ฐœ ํด๋” ๊ณต๊ฐœ์„ค์ • 15์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. 15์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. @@ -196,22 +295,41 @@ ์ „์ฒด๊ณต๊ฐœ ํŒ”๋กœ์ž‰ ๋‚˜๋งŒ๋ณด๊ธฐ + ๊ณต๊ฐœ ํด๋” + ๋น„๊ณต๊ฐœ ํด๋” + ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ + ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ + ๊ฐœ์˜ ๊ธ€ + %d๊ฐœ ์„ ํƒ + ์ตœ์‹ ์ˆœ + ์˜ค๋ž˜๋œ์ˆœ ์ปค๋ฒ„ ์‚ฌ์ง„ ์ดˆ๊ธฐํ™” - ํด๋”๋ฅผ ์‚ญ์ œํ• ๊นŒ์š”? - ํ•ด๋‹น ํด๋”์— ๋“ฑ๋ก๋œ ๊ฒŒ์‹œ๋ฌผ๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค. + \'%s\' ํด๋”๋ฅผ\n ์ •๋ง ์‚ญ์ œํ• ๊นŒ์š”? + \'%s\' ํด๋”๋ฅผ ์‚ญ์ œํ•˜๋ฉด\nํด๋” ์•ˆ์˜ ๊ฒŒ์‹œ๊ธ€๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋ผ์š”. + %d๊ฐœ์˜ ๊ฒŒ์‹œ๊ธ€์„\n์ •๋ง ์‚ญ์ œํ• ๊นŒ์š”? + ์„ ํƒํ•œ ๊ฒŒ์‹œ๊ธ€์ด ์‚ญ์ œ๋˜๋ฉด ๋ณต๊ตฌํ•  ์ˆ˜ ์—†์–ด์š”. + ์„ ํƒํ•œ ํด๋”๋กœ ์ด๋™ ํด๋” ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. ํด๋” ์ด๋ฆ„์—๋Š” ํ•œ๊ธ€, ์˜๋ฌธ, ์ˆซ์ž๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. ํด๋”๋ฅผ ์ƒ์„ฑ ํ•  ์ˆ˜ ์—†์–ด์š”. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”. ์•„์ง ๊ฒŒ์‹œ๋ฌผ์ด ์—†์–ด์š”. + ๊ฒŒ์‹œ๊ธ€ ํŽธ์ง‘ + ํด๋” ํŽธ์ง‘ + ํด๋” ์‚ญ์ œ ํŒ”๋กœ์›Œ ํŒ”๋กœ์ž‰ ํŒ”๋กœ์ž‰ ํŒ”๋กœ์šฐ + ํŒ”๋กœ์šฐ ์ทจ์†Œ + ํŒ”๋กœ์šฐ ํ•˜๊ธฐ ํŒ”๋กœ์šฐ๋ฅผ ์ทจ์†Œํ• ๊นŒ์š”? - ์•„์ง ํšŒ์›๋‹˜์„\nํŒ”๋กœ์šฐํ•˜๋Š” ์‚ฌ๋žŒ์ด ์—†์–ด์š” + ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค + ์•„์ง ํšŒ์›๋‹˜์„ ํŒ”๋กœ์šฐํ•˜๋Š” ์‚ฌ๋žŒ์ด ์—†์–ด์š” + ํšŒ์›๋‹˜์˜ ๋‹ค๊พธ๋ฅผ ๋” ์—…๋กœ๋“œํ•ด ๋ณด์„ธ์š” ๊ด€์‹ฌ์ด ๊ฐ€๋Š” ์‚ฌ๋žŒ๋“ค์„ ํŒ”๋กœ์šฐํ•ด๋ณด์„ธ์š” + ๋‚ด ์ทจํ–ฅ์˜ ๋‹ค๊พธ๋ฅผ ํ”ผ๋“œ์—์„œ ๋ชจ์•„๋ณผ ์ˆ˜ ์žˆ์–ด์š” ํ”„๋กœํ•„ ์ˆ˜์ • @@ -230,18 +348,31 @@ ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ• ๊นŒ์š”? ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•˜๋ฉด\n ์„œ๋กœ ๊ฒŒ์‹œ๋ฌผ์„ ๋ณผ ์ˆ˜ ์—†์–ด์š”. ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จ๋˜์—ˆ์–ด์š”. + ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•ด์ œ๋˜์—ˆ์–ด์š”. + ์ฐจ๋‹จ ํ•ด์ œ์— ์‹คํŒจํ–ˆ์–ด์š”. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š” ์‚ฌ์šฉ์ž ์‹ ๊ณ  ๋กœ๊ทธ์ธ ์ •๋ณด ํ™˜๊ฒฝ์„ค์ • + + ๋ถ๋งˆํฌ + ํŽธ์ง‘ + ๊ฐœ์˜ ๋ถ๋งˆํฌ + %d๊ฐœ ์„ ํƒ ์•„์ง ๋ถ๋งˆํฌํ•œ ๊ฒŒ์‹œ๋ฌผ์ด ์—†์–ด์š”. ์•„์ง ์ข‹์•„์š”ํ•œ ๊ฒŒ์‹œ๋ฌผ์ด ์—†์–ด์š”. ์•„์ง ํด๋”๊ฐ€ ์—†์–ด์š”. + ๋‚˜์˜ ๋‹ค์ด์–ด๋ฆฌ + ์ƒˆ ํด๋” ๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด - ์•—! ์ฐพ์œผ์‹œ๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์–ด์š”. + ์•—! ์ฐพ์œผ์‹œ๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์–ด์š”. + ๋‹ค๋ฅธ ๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด๋ณด์„ธ์š” + ๊ฒ€์ƒ‰ ๊ธฐ๋ก ์ง€์šฐ๊ธฐ + ์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์–ด์š”. + ๊ด€์‹ฌ์žˆ๋Š” ํ‚ค์›Œ๋“œ ๋˜๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์•„๋ณด์„ธ์š” ์ธ์ฆ๋ฒˆํ˜ธ ๋ฐ›๊ธฐ @@ -252,18 +383,44 @@ ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ๋ฐœ์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ์ธ์ฆ๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค. ๋‹ค์‹œ ํ™•์ธํ•ด๋ณด์„ธ์š”. ์ธ์ฆ ์‹คํŒจ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. - ์ œํ•œ ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ–ˆ์–ด์š”. + ์ œํ•œ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ–ˆ์–ด์š”! ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋‹ค์‹œ ๋ฐ›์•„์ฃผ์„ธ์š”. + + + ๋‹‰๋„ค์ž„ + + ์ด๋ฉ”์ผ ํšŒ์›๊ฐ€์ž… + ๋ฉ”์ผ๋กœ ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ๋ฐœ์†ก๋˜์—ˆ์–ด์š”! + ์ด๋ฉ”์ผ + ์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” + ์ด๋ฏธ ๊ฐ€์ž…๋œ ์ด๋ฉ”์ผ์ด์—์š” + ์ธ์ฆ๋ฒˆํ˜ธ ์žฌ๋ฐœ์†ก + ์ธ์ฆ๋ฒˆํ˜ธ ๋ฐ›๊ธฐ - - ์ด๋ฉ”์ผ ์ด๋ฉ”์ผ ์ฃผ์†Œ email@example.com ์ด๋ฉ”์ผ ์ธ์ฆ - ์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. - ์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. - ์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ ์ด๋ฉ”์ผ ์ฃผ์†Œ์˜ˆ์š”. - + ์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. + ์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. + ์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ ์ด๋ฉ”์ผ ์ฃผ์†Œ์˜ˆ์š”. + + + ์ธ์ฆ๋ฒˆํ˜ธ + ์ธ์ฆ๋ฒˆํ˜ธ + ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•˜์„ธ์š” + ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋กœ ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ „์†ก๋˜์ง€ ์•Š์•˜์–ด์š” + ์ œํ•œ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ–ˆ์–ด์š”! ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋‹ค์‹œ ๋ฐ›์•„์ฃผ์„ธ์š” + ์ธ์ฆํ•˜๊ธฐ + ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ๋ฐœ์†ก๋์–ด์š” + + + ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ • + ๋น„๋ฐ€๋ฒˆํ˜ธ + 8์ž๋ฆฌ ์ด์ƒ ์ž…๋ ฅ + 8์ž ์ด์ƒ, 16์ž ์ดํ•˜์˜ ์˜๋ฌธ ์†Œ๋ฌธ์ž์™€ ์ˆซ์ž๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š” + + ๋‹ค์Œ + ๋น„๋ฐ€๋ฒˆํ˜ธ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. ์˜๋ฌธ ์†Œ๋ฌธ์ž์™€ ์ˆซ์ž๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. @@ -273,14 +430,61 @@ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š”. + + ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ + ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์ž…๋ ฅ + ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š” + ๋‹ค์Œ + + + ๋‹‰๋„ค์ž„ + 2์ž ์ด์ƒ 10์ž ์ดํ•˜ + ์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ ๋‹‰๋„ค์ž„์ด์—์š” + ํ•œ๊ธ€, ์˜๋ฌธ, ์ˆซ์ž๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š” + ํ”„๋กœํ•„ ์‚ฌ์ง„ ์ดˆ๊ธฐํ™” + ๊ฐ€์ž…ํ•˜๊ธฐ + ์•จ๋ฒ”์—์„œ ์„ ํƒ ์‚ฌ์ง„ ์ง์ ‘์ดฌ์˜ - - ๊ฐ€์ž…์„ ์™„๋ฃŒํ•˜๋Š” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”! + + ๊ฐ€์ž…์„ ์ง„ํ–‰์ค‘ ์ž…๋‹ˆ๋‹ค. \n์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค ์ฃผ์„ธ์š”. %s๋‹˜,\n๊ฐ€์ž…์„ ์ถ•ํ•˜๋“œ๋ ค์š”! ๊ฐ€์ž…์„ ์™„๋ฃŒํ•  ์ˆ˜ ์—†์–ด์š”. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”. + + ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์ค‘์ž…๋‹ˆ๋‹ค. + ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • + ๊ฐ€์ž…ํ•  ๋•Œ ์‚ฌ์šฉํ•œ ์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. + ์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” + ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ์ด ์—†์–ด์š” + ์†Œ์…œ ๊ณ„์ •์œผ๋กœ ๊ฐ€์ž…ํ•œ ์ด๋ฉ”์ผ์ด์—์š” + ์ด๋ฉ”์ผ + ์ธ์ฆ๋ฒˆํ˜ธ ๋ฐ›๊ธฐ + + ์ธ์ฆํ•˜๊ธฐ + ์ธ์ฆ๋ฒˆํ˜ธ + ์ธ์ฆ๋ฒˆํ˜ธ + ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•˜์„ธ์š” + ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋กœ ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ „์†ก๋˜์ง€ ์•Š์•˜์–ด์š” + ์ œํ•œ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ–ˆ์–ด์š”! ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋‹ค์‹œ ๋ฐ›์•„์ฃผ์„ธ์š” + ์ธ์ฆ๋ฒˆํ˜ธ ์žฌ๋ฐœ์†ก + ๊ฐ„ํŽธ ๋กœ๊ทธ์ธ ๊ณ„์ •์ด์—์š” + ๊ฐ€์ž…ํ•˜์…จ๋˜ ์†Œ์…œ ๊ณ„์ •์„ ์ด์šฉํ•ด\n๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š” + + ์ƒˆ๋กœ์šด ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ + ๋ฉ”์ผ๋กœ ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ๋ฐœ์†ก๋˜์—ˆ์–ด์š”! + + ๋น„๋ฐ€๋ฒˆํ˜ธ + 8์ž๋ฆฌ ์ด์ƒ ์ž…๋ ฅ + 8์ž ์ด์ƒ, 16์ž ์ดํ•˜์˜ ์˜๋ฌธ ์†Œ๋ฌธ์ž์™€ ์ˆซ์ž๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š” + ๋‹ค์Œ + + ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ + ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์ž…๋ ฅ + ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š” + ๋ณ€๊ฒฝํ•˜๊ธฐ + ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ ๊ฐ€์ž…ํ•  ๋•Œ ์‚ฌ์šฉํ•œ ์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. @@ -290,18 +494,28 @@ ๋กœ๊ทธ์ธํ•˜๋Ÿฌ ๊ฐ€๊ธฐ - ๊ณ„์ •์„ ๋กœ๊ทธ์•„์›ƒ ํ• ๊นŒ์š”? + ํ™˜๊ฒฝ์„ค์ • + ๋‚˜์˜ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๊ณ„์ •์„ ์‚ญ์ œํ• ๊นŒ์š”? ๋ชจ๋“  ์ •๋ณด๊ฐ€ ์‚ญ์ œ๋˜๋ฉฐ, ๋ณต๊ตฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. - ๋ฉ”์ผ ์ฃผ์†Œ ๋ณต์‚ฌ ์™„๋ฃŒ - ํด๋ฆฝ๋ณด๋“œ์— ์ €์žฅ๋œ ๋ฉ”์ผ ์ฃผ์†Œ๋กœ ๋ฌธ์˜ ์ฃผ์„ธ์š” + ๋ฉ”์ผ ์ฃผ์†Œ ๋ณต์‚ฌ ์™„๋ฃŒ! + ํด๋ฆฝ๋ณด๋“œ์— ์ €์žฅ๋œ ๋ฉ”์ผ ์ฃผ์†Œ๋กœ ๋ฌธ์˜ํ•ด ์ฃผ์„ธ์š”. + ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ + ์ฐจ๋‹จ ๊ด€๋ฆฌ + ์•Œ๋ฆผ + ๊ณต์ง€์‚ฌํ•ญ + ์ •๋ณด + ๋ฌธ์˜ํ•˜๊ธฐ + ๋กœ๊ทธ์•„์›ƒ + ๋กœ๊ทธ์•„์›ƒ ํ• ๊นŒ์š”? + ๊ณ„์ • ์‚ญ์ œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ •ํ™•ํžˆ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ\n์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. ์ƒˆ๋กœ์šด ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ\n์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ - ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์–ด์š” ์•Œ๋ฆผ ์„ค์ • @@ -314,21 +528,57 @@ ๋ฐ˜์‘ ์•Œ๋ฆผ NOTICE ์•„์ง ๋ฐ›์€ ์•Œ๋ฆผ์ด ์—†์–ด์š”. - ์ƒˆ๋กœ์šด ์†Œ์‹๋“ค์„\n์ด๊ณณ์—์„œ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”. + ์ƒˆ๋กœ์šด ์†Œ์‹๋“ค์„ ์ด๊ณณ์—์„œ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”. + ์ง€๋‚œ ์•Œ๋ฆผ + ๊ธฐ๊ธฐ์˜ ์•Œ๋ฆผ ์„ค์ • ๋ณ€๊ฒฝ์„ ์œ„ํ•ด\nํ™˜๊ฒฝ์„ค์ •์œผ๋กœ ์ด๋™ํ• ๊ฒŒ์š” ๊ณ„์ • ์‚ญ์ œํ•˜๊ธฐ ์ •๋ง ๊ณ„์ •์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์–ด์š”? ๊ณ„์ •์„ ์‚ญ์ œํ•˜๋Š” ์ด์œ ์— ๋Œ€ํ•ด ์•Œ๋ ค์ฃผ์„ธ์š”. ์ ์–ด์ฃผ์‹  ๋‚ด์šฉ์€ ์ข€ ๋” ๋‚˜์€ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๊ณ„์ • ์‚ญ์ œ - ์ด์šฉ์ด ๋ถˆํŽธํ•ด์š”. - ๊ธฐ๋ก์„ ์‚ญ์ œํ•˜๊ณ  ์‹ถ์–ด์š”. - ์ž์ฃผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„์š”. - ๊ฐœ์ธ์ •๋ณด ์œ ์ถœ์ด ์šฐ๋ ค๋ผ์š”. - ์ฝ˜ํ…์ธ ๊ฐ€ ๋งˆ์Œ์— ๋“ค์ง€์•Š์•„์š”. + ๊ณ„์ •์„ ์‚ญ์ œํ•˜๋Š” ์ด์œ ๊ฐ€ ๋ฌด์—‡์ธ๊ฐ€์š”? + ์ด์šฉ์ด ๋ถˆํŽธํ•ด์š” + ๊ณง ์ƒˆ๋กœ์šด ๋ฒ„์ „์ด ์—…๋ฐ์ดํŠธ ๋ ๊ฑฐ์˜ˆ์š” + ๋” ์‚ฌ์šฉํ•˜๊ธฐ ํŽธ๋ฆฌํ•ด์ง„ ๋‹ค์š”๋ฅผ ๋งŒ๋‚˜๋ณผ ์ˆ˜ ์žˆ์–ด์š”. + ๊ทธ๋ž˜๋„ ์‚ญ์ œํ•˜๊ธฐ + ์—…๋ฐ์ดํŠธ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ + ๊ธฐ๋ก์„ ์‚ญ์ œํ•˜๊ณ  ์‹ถ์–ด์š” + ๊ณ„์ •์„ ์‚ญ์ œํ•˜์ง€ ์•Š์•„๋„\n์‚ฌ์šฉ ๊ธฐ๋ก์„ ์ง€์šธ ์ˆ˜ ์žˆ์–ด์š” + + ๊ทธ๋ž˜๋„ ์‚ญ์ œํ•˜๊ธฐ + ๋งˆ์ดํŽ˜์ด์ง€๋กœ ์ด๋™ + ์ž์ฃผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„์š” + ๊ณง ์ƒˆ๋กœ์šด ๋ฒ„์ „์ด ์—…๋ฐ์ดํŠธ ๋ ๊ฑฐ์˜ˆ์š” + ๋” ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์ด ์žˆ๋Š” ๋‹ค์š”๋ฅผ ๋งŒ๋‚  ์ˆ˜ ์žˆ์–ด์š”. + ๊ทธ๋ž˜๋„ ์‚ญ์ œํ•˜๊ธฐ + ์—…๋ฐ์ดํŠธ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ + ์ฝ˜ํ…์ธ ๊ฐ€ ๋งˆ์Œ์— ๋“ค์ง€์•Š์•„์š” + ์ทจํ–ฅ์— ๋งž๋Š” ์‚ฌ๋žŒ์„ ํŒ”๋กœ์šฐํ•˜๋ฉด\nํ”ผ๋“œ์—์„œ ๋ชจ์•„๋ณผ ์ˆ˜ ์žˆ์–ด์š” + + ๊ทธ๋ž˜๋„ ์‚ญ์ œํ•˜๊ธฐ + ํŒ”๋กœ์šฐํ•˜๋Ÿฌ ๊ฐ€๊ธฐ ๊ธฐํƒ€ - ํƒˆํ‡ด ์‚ฌ์œ ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. (100์ž ์ด๋‚ด) - ๊ณ„์ • ์‚ญ์ œ์‹œ ์ •๋ณด๊ฐ€ ํšŒ์›๋‹˜์˜ ๋ชจ๋“  ์ฝ˜ํ…์ธ ์™€ ํ™œ๋™๊ธฐ๋ก์ด ํ•จ๊ป˜ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค. ์‚ญ์ œ๋œ ์ •๋ณด๋Š” ๋ณต๊ตฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + ์‚ญ์ œํ•˜๋Š” ์ด์œ ๋ฅผ ์•Œ๋ ค์ฃผ์„ธ์š” + ๋‚จ๊ฒจ์ฃผ์‹  ์˜๊ฒฌ์„ ๋ฐ”ํƒ•์œผ๋กœ ๋” ๋‚˜์€ ๋‹ค์š”๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. + ์‚ญ์ œํ•˜๋Š” ์ด์œ ์— ๋Œ€ํ•ด ์ ์–ด์ฃผ์„ธ์š” + ๊ทธ๋ž˜๋„ ์‚ญ์ œํ•˜๊ธฐ + ๋‹ค์Œ์— ์‚ญ์ œํ•˜๊ธฐ + ์ž ๊น!\n๊ณ„์ • ์‚ญ์ œ ์ „์— ํ™•์ธํ–ˆ๋‚˜์š”? + + ๊ทธ๋™์•ˆ ๊ธฐ๋กํ–ˆ๋˜ ๋ชจ๋“  ๋‹ค์ด์–ด๋ฆฌ๊ฐ€ ์‚ฌ๋ผ์ ธ์š” + ๋‚˜์˜ ํ™œ๋™ ๊ธฐ๋ก์ด ๋ชจ๋‘ ์‚ฌ๋ผ์ ธ์š” + ์‚ฌ๋ผ์ง„ ๋ฐ์ดํ„ฐ๋Š” ๋ณต๊ตฌํ•  ์ˆ˜ ์—†์–ด์š” + + ํ™•์ธํ–ˆ์–ด์š” + ๊ณ„์ •์„ ์ •๋ง ์‚ญ์ œํ• ๊นŒ์š”? + ์‚ญ์ œํ•˜๊ธฐ + + + ์ฐจ๋‹จ ๊ด€๋ฆฌ + ์•„์ง ์ฐจ๋‹จํ•œ ์œ ์ €๊ฐ€ ์—†์–ด์š”. + ๋กœ๋”ฉ์— ์‹คํŒจ ํ–ˆ์–ด์š”. + ์ฐจ๋‹จ ํ•ด์ œ ์ •๋ณด @@ -339,7 +589,9 @@ ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š” + + ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. + Hello blank fragment - - \ No newline at end of file +