diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 068abf285..6faca4d95 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,8 +17,8 @@ android { minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = 11 - versionName = "v1.3.2" + versionCode = 13 + versionName = "1.3.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/team/aliens/dms/android/app/DmsApp.kt b/app/src/main/java/team/aliens/dms/android/app/DmsApp.kt index fa32d9760..40dd2d9fb 100644 --- a/app/src/main/java/team/aliens/dms/android/app/DmsApp.kt +++ b/app/src/main/java/team/aliens/dms/android/app/DmsApp.kt @@ -21,6 +21,8 @@ import team.aliens.dms.android.feature.editpassword.EditPasswordViewModel import team.aliens.dms.android.feature.editpassword.navigation.EditPasswordNavGraph import team.aliens.dms.android.feature.outing.OutingViewModel import team.aliens.dms.android.feature.outing.navigation.OutingNavGraph +import team.aliens.dms.android.feature.resetpassword.ResetPasswordViewModel +import team.aliens.dms.android.feature.resetpassword.navigation.ResetPasswordNavGraph import team.aliens.dms.android.feature.signup.SignUpViewModel import team.aliens.dms.android.feature.signup.TermsUrl import team.aliens.dms.android.feature.signup.navigation.SignUpNavGraph @@ -65,10 +67,12 @@ fun DmsApp( } hiltViewModel(parentEntry) } - dependency(OutingNavGraph) { + + dependency(ResetPasswordNavGraph) { val parentEntry = remember(navBackStackEntry) { - navController.getBackStackEntry(OutingNavGraph.route) + navController.getBackStackEntry(ResetPasswordNavGraph.route) } + hiltViewModel(parentEntry) } }, ) diff --git a/app/src/main/java/team/aliens/dms/android/app/di/network/NetworkConfigModule.kt b/app/src/main/java/team/aliens/dms/android/app/di/network/NetworkConfigModule.kt index 57c89c107..978a06827 100644 --- a/app/src/main/java/team/aliens/dms/android/app/di/network/NetworkConfigModule.kt +++ b/app/src/main/java/team/aliens/dms/android/app/di/network/NetworkConfigModule.kt @@ -105,6 +105,12 @@ object NetworkConfigModule { method = HttpMethod.GET, path = "/schools/code", ), + + // File + HttpRequest( + method = HttpMethod.GET, + path = "/files/url", + ) ) } diff --git a/buildSrc/src/main/kotlin/ProjectPaths.kt b/buildSrc/src/main/kotlin/ProjectPaths.kt index acad6d18e..bbf50a9c2 100644 --- a/buildSrc/src/main/kotlin/ProjectPaths.kt +++ b/buildSrc/src/main/kotlin/ProjectPaths.kt @@ -14,6 +14,7 @@ object ProjectPaths { const val PROJECT = ":core:project" const val SCHOOL = ":core:school" const val UI = ":core:ui" + const val FILE = ":core:file" } object Shared { @@ -22,5 +23,6 @@ object ProjectPaths { const val MODEL = ":shared:model" const val VALIDATOR = ":shared:validator" const val TIMER = ":shared:timer" + const val FILE = ":shared:file" } } diff --git a/core/design-system/src/main/java/team/aliens/dms/android/core/designsystem/AppBars.kt b/core/design-system/src/main/java/team/aliens/dms/android/core/designsystem/AppBars.kt index 6e31c4bb7..35fdfff80 100644 --- a/core/design-system/src/main/java/team/aliens/dms/android/core/designsystem/AppBars.kt +++ b/core/design-system/src/main/java/team/aliens/dms/android/core/designsystem/AppBars.kt @@ -13,8 +13,8 @@ import androidx.compose.ui.Modifier @OptIn(ExperimentalMaterial3Api::class) @Composable fun DmsTopAppBar( - title: @Composable () -> Unit, modifier: Modifier = Modifier, + title: @Composable () -> Unit = {}, navigationIcon: @Composable () -> Unit = {}, actions: @Composable RowScope.() -> Unit = {}, windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, diff --git a/core/design-system/src/main/java/team/aliens/dms/android/core/designsystem/DmsIcon.kt b/core/design-system/src/main/java/team/aliens/dms/android/core/designsystem/DmsIcon.kt index f3a870d45..a4deca8a5 100644 --- a/core/design-system/src/main/java/team/aliens/dms/android/core/designsystem/DmsIcon.kt +++ b/core/design-system/src/main/java/team/aliens/dms/android/core/designsystem/DmsIcon.kt @@ -22,7 +22,7 @@ object DmsIcon { val LogoDark = R.drawable.ic_logo_dark val LogoLight = R.drawable.ic_logo_light val Lunch = R.drawable.ic_lunch - val MyPage = R.drawable.ic_my_page + val MyPage = R.drawable.ic_person val Notice = R.drawable.ic_notice val Palette = R.drawable.ic_palette val PasswordInvisible = R.drawable.ic_password_invisible @@ -34,4 +34,6 @@ object DmsIcon { val Visible = R.drawable.ic_visible val Warning = R.drawable.ic_warning val BlueNotice = R.drawable.ic_blue_notice + val Edit = R.drawable.ic_edit + val ProfileDefault = R.drawable.ic_profile_default } \ No newline at end of file diff --git a/core/design-system/src/main/res/drawable/ic_edit.xml b/core/design-system/src/main/res/drawable/ic_edit.xml new file mode 100644 index 000000000..3d2afecc3 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_my_page.xml b/core/design-system/src/main/res/drawable/ic_my_page.xml deleted file mode 100644 index 4e4cca7a9..000000000 --- a/core/design-system/src/main/res/drawable/ic_my_page.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/core/design-system/src/main/res/drawable/ic_person.xml b/core/design-system/src/main/res/drawable/ic_person.xml new file mode 100644 index 000000000..edca5fdb9 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_person.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_plus.xml b/core/design-system/src/main/res/drawable/ic_plus.xml index 266fd871c..9004e2107 100644 --- a/core/design-system/src/main/res/drawable/ic_plus.xml +++ b/core/design-system/src/main/res/drawable/ic_plus.xml @@ -1,9 +1,9 @@ + android:width="30dp" + android:height="30dp" + android:viewportWidth="30" + android:viewportHeight="30"> + android:pathData="M8.183,15.703H13.927V21.447C13.927,22.031 14.407,22.521 15.001,22.521C15.594,22.521 16.074,22.031 16.074,21.447V15.703H21.819C22.402,15.703 22.892,15.223 22.892,14.63C22.892,14.036 22.402,13.556 21.819,13.556H16.074V7.812C16.074,7.228 15.594,6.738 15.001,6.738C14.407,6.738 13.927,7.228 13.927,7.812V13.556H8.183C7.599,13.556 7.109,14.036 7.109,14.63C7.109,15.223 7.599,15.703 8.183,15.703Z" + android:fillColor="#ffffff"/> diff --git a/core/design-system/src/main/res/drawable/ic_profile_default.xml b/core/design-system/src/main/res/drawable/ic_profile_default.xml new file mode 100644 index 000000000..be428539b --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_profile_default.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/shared/timer/.gitignore b/core/file/.gitignore similarity index 100% rename from shared/timer/.gitignore rename to core/file/.gitignore diff --git a/core/file/build.gradle.kts b/core/file/build.gradle.kts new file mode 100644 index 000000000..520f79bd7 --- /dev/null +++ b/core/file/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "team.aliens.dms.android.core.file" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = Versions.java + targetCompatibility = Versions.java + } + kotlinOptions { + jvmTarget = Versions.java.toString() + } +} + +dependencies { + + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso) +} diff --git a/core/file/consumer-rules.pro b/core/file/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/file/proguard-rules.pro b/core/file/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core/file/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/file/src/androidTest/java/team/aliens/dms/android/core/file/ExampleInstrumentedTest.kt b/core/file/src/androidTest/java/team/aliens/dms/android/core/file/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..ca04ecd37 --- /dev/null +++ b/core/file/src/androidTest/java/team/aliens/dms/android/core/file/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package team.aliens.dms.android.core.file + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("team.aliens.dms.android.core.file.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/core/file/src/main/AndroidManifest.xml b/core/file/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/core/file/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/file/src/main/java/team/aliens/dms/android/core/file/File.kt b/core/file/src/main/java/team/aliens/dms/android/core/file/File.kt new file mode 100644 index 000000000..0844488a3 --- /dev/null +++ b/core/file/src/main/java/team/aliens/dms/android/core/file/File.kt @@ -0,0 +1,78 @@ +package team.aliens.dms.android.core.file + +import android.content.Context +import android.net.Uri +import android.os.Environment +import android.provider.OpenableColumns +import java.io.File +import java.io.FileOutputStream + +object File { + fun toFile( + context: Context, + uri: Uri, + ): File { + val fileName = getFileName( + context = context, + uri = uri, + ) + val file = createTempFile( + context = context, + fileName = fileName, + ) + + copyToFile( + context = context, + uri = uri, + file = file, + ) + + return File(file.absolutePath) + } + + private fun copyToFile( + context: Context, + uri: Uri, + file: File, + ) { + val inputStream = context.contentResolver.openInputStream(uri) + val outputStream = FileOutputStream(file) + + val buffer = ByteArray(4 * 1024) + while (true) { + val byteCount = inputStream!!.read(buffer) + if (byteCount < 0) break + outputStream.write(buffer, 0, byteCount) + } + + inputStream.close() + } + + private fun getFileName( + uri: Uri, + context: Context, + ): String { + val cursor = context.contentResolver.query(uri, null, null, null, null) + var fileName = "" + + cursor?.run { + cursor.moveToFirst() + val index = getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (index != -1) { + fileName = cursor.getString(index) + } + } + cursor?.close() + + return fileName + } + + private fun createTempFile( + context: Context, + fileName: String, + ): File { + val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + + return File(storageDir, fileName) + } +} diff --git a/core/file/src/test/java/team/aliens/dms/android/core/file/ExampleUnitTest.kt b/core/file/src/test/java/team/aliens/dms/android/core/file/ExampleUnitTest.kt new file mode 100644 index 000000000..dda5930e2 --- /dev/null +++ b/core/file/src/test/java/team/aliens/dms/android/core/file/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package team.aliens.dms.android.core.file + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/interceptor/JwtInterceptor.kt b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/interceptor/JwtInterceptor.kt index 3a9d8aafe..1d9a52aee 100644 --- a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/interceptor/JwtInterceptor.kt +++ b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/interceptor/JwtInterceptor.kt @@ -7,6 +7,7 @@ import okhttp3.Response import team.aliens.dms.android.core.jwt.JwtProvider import team.aliens.dms.android.core.jwt.network.IgnoreRequests import team.aliens.dms.android.core.network.toHttpMethod +import team.aliens.dms.android.core.network.util.ResourceKeys import javax.inject.Inject class JwtInterceptor @Inject constructor( @@ -34,6 +35,10 @@ class JwtInterceptor @Inject constructor( val path = this@shouldBeIgnored.url.encodedPath val method = this@shouldBeIgnored.method.toHttpMethod() - path.contains(ignoreRequest.path) && method == ignoreRequest.method + path.contains(ignoreRequest.path) && method == ignoreRequest.method ||checkS3Request(url = this@shouldBeIgnored.url.toString()) + } + + private fun checkS3Request(url: String): Boolean { + return url.contains(ResourceKeys.IMAGE_URL) } } diff --git a/core/network/src/main/java/team/aliens/dms/android/core/network/util/RequestType.kt b/core/network/src/main/java/team/aliens/dms/android/core/network/util/RequestType.kt new file mode 100644 index 000000000..a19095e0d --- /dev/null +++ b/core/network/src/main/java/team/aliens/dms/android/core/network/util/RequestType.kt @@ -0,0 +1,5 @@ +package team.aliens.dms.android.core.network.util + +object RequestType { + const val Binary = "application/octet-stream" +} diff --git a/core/network/src/main/java/team/aliens/dms/android/core/network/util/ResourceKeys.kt b/core/network/src/main/java/team/aliens/dms/android/core/network/util/ResourceKeys.kt new file mode 100644 index 000000000..8f7169be5 --- /dev/null +++ b/core/network/src/main/java/team/aliens/dms/android/core/network/util/ResourceKeys.kt @@ -0,0 +1,5 @@ +package team.aliens.dms.android.core.network.util + +object ResourceKeys { + const val IMAGE_URL = "https://image-dms.s3.ap-northeast-2.amazonaws.com/" +} diff --git a/data/src/main/java/team/aliens/dms/android/data/file/di/RepositoryModule.kt b/data/src/main/java/team/aliens/dms/android/data/file/di/RepositoryModule.kt new file mode 100644 index 000000000..22d20104f --- /dev/null +++ b/data/src/main/java/team/aliens/dms/android/data/file/di/RepositoryModule.kt @@ -0,0 +1,18 @@ +package team.aliens.dms.android.data.file.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import team.aliens.dms.android.data.file.repository.FileRepository +import team.aliens.dms.android.data.file.repository.FileRepositoryImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class RepositoryModule { + + @Binds + @Singleton + abstract fun bindFileRepository(impl: FileRepositoryImpl): FileRepository +} diff --git a/data/src/main/java/team/aliens/dms/android/data/file/mapper/FileMapper.kt b/data/src/main/java/team/aliens/dms/android/data/file/mapper/FileMapper.kt new file mode 100644 index 000000000..0d38713cb --- /dev/null +++ b/data/src/main/java/team/aliens/dms/android/data/file/mapper/FileMapper.kt @@ -0,0 +1,9 @@ +package team.aliens.dms.android.data.file.mapper + +import team.aliens.dms.android.data.file.model.PresignedFileUrl +import team.aliens.dms.android.network.file.model.FetchPresignedUrlResponse + +internal fun FetchPresignedUrlResponse.toModel() : PresignedFileUrl = PresignedFileUrl( + fileUploadUrl = this.fileUploadUrl, + fileUrl = this.fileUrl, +) \ No newline at end of file diff --git a/data/src/main/java/team/aliens/dms/android/data/file/model/FileUrl.kt b/data/src/main/java/team/aliens/dms/android/data/file/model/FileUrl.kt new file mode 100644 index 000000000..ae879f20a --- /dev/null +++ b/data/src/main/java/team/aliens/dms/android/data/file/model/FileUrl.kt @@ -0,0 +1,3 @@ +package team.aliens.dms.android.data.file.model + +typealias FileUrl = String diff --git a/data/src/main/java/team/aliens/dms/android/data/file/model/PresignedFileUrl.kt b/data/src/main/java/team/aliens/dms/android/data/file/model/PresignedFileUrl.kt new file mode 100644 index 000000000..f722ba34f --- /dev/null +++ b/data/src/main/java/team/aliens/dms/android/data/file/model/PresignedFileUrl.kt @@ -0,0 +1,6 @@ +package team.aliens.dms.android.data.file.model + +data class PresignedFileUrl( + val fileUploadUrl: String, + val fileUrl: String, +) diff --git a/data/src/main/java/team/aliens/dms/android/data/file/repository/FileRepository.kt b/data/src/main/java/team/aliens/dms/android/data/file/repository/FileRepository.kt new file mode 100644 index 000000000..6b241d0ec --- /dev/null +++ b/data/src/main/java/team/aliens/dms/android/data/file/repository/FileRepository.kt @@ -0,0 +1,10 @@ +package team.aliens.dms.android.data.file.repository + +import team.aliens.dms.android.data.file.model.PresignedFileUrl +import java.io.File + +abstract class FileRepository { + + abstract suspend fun fetchPresignedUrl(fileName: String): PresignedFileUrl + abstract suspend fun uploadFile(presignedUrl: String, file: File) +} diff --git a/data/src/main/java/team/aliens/dms/android/data/file/repository/FileRepositoryImpl.kt b/data/src/main/java/team/aliens/dms/android/data/file/repository/FileRepositoryImpl.kt new file mode 100644 index 000000000..7aa8d8f17 --- /dev/null +++ b/data/src/main/java/team/aliens/dms/android/data/file/repository/FileRepositoryImpl.kt @@ -0,0 +1,18 @@ +package team.aliens.dms.android.data.file.repository + +import team.aliens.dms.android.data.file.mapper.toModel +import team.aliens.dms.android.data.file.model.FileUrl +import team.aliens.dms.android.data.file.model.PresignedFileUrl +import team.aliens.dms.android.network.file.datasource.NetworkFileDataSource +import java.io.File +import javax.inject.Inject + +internal class FileRepositoryImpl @Inject constructor( + private val networkFileDataSource: NetworkFileDataSource, +) : FileRepository() { + override suspend fun fetchPresignedUrl(fileName: String): PresignedFileUrl = + networkFileDataSource.fetchPresignedUrl(fileName = fileName).toModel() + + override suspend fun uploadFile(presignedUrl: String, file: File) = + networkFileDataSource.uploadFile(presignedUrl = presignedUrl, file = file) +} diff --git a/data/src/main/java/team/aliens/dms/android/data/student/repository/StudentRepository.kt b/data/src/main/java/team/aliens/dms/android/data/student/repository/StudentRepository.kt index 799afe072..abe66b737 100644 --- a/data/src/main/java/team/aliens/dms/android/data/student/repository/StudentRepository.kt +++ b/data/src/main/java/team/aliens/dms/android/data/student/repository/StudentRepository.kt @@ -18,7 +18,7 @@ abstract class StudentRepository { number: Int, accountId: String, password: String, - profileImageUrl: String? = null, + profileImageUrl: String?, ) abstract suspend fun examineStudentNumber( diff --git a/data/src/main/java/team/aliens/dms/android/data/student/repository/StudentRepositoryImpl.kt b/data/src/main/java/team/aliens/dms/android/data/student/repository/StudentRepositoryImpl.kt index ed85ab06e..f3dc5ef39 100644 --- a/data/src/main/java/team/aliens/dms/android/data/student/repository/StudentRepositoryImpl.kt +++ b/data/src/main/java/team/aliens/dms/android/data/student/repository/StudentRepositoryImpl.kt @@ -1,5 +1,6 @@ package team.aliens.dms.android.data.student.repository +import android.util.Log import team.aliens.dms.android.core.jwt.JwtProvider import team.aliens.dms.android.core.school.SchoolProvider import team.aliens.dms.android.data.student.mapper.toModel @@ -9,6 +10,7 @@ import team.aliens.dms.android.data.student.model.Student import team.aliens.dms.android.data.student.model.StudentName import team.aliens.dms.android.data.student.model.toModel import team.aliens.dms.android.network.student.datasource.NetworkStudentDataSource +import team.aliens.dms.android.network.student.model.EditProfileRequest import team.aliens.dms.android.network.student.model.ResetPasswordRequest import team.aliens.dms.android.network.student.model.SignUpRequest import team.aliens.dms.android.network.student.model.SignUpResponse @@ -35,6 +37,8 @@ internal class StudentRepositoryImpl @Inject constructor( password: String, profileImageUrl: String?, ) { + Log.d("TEST1",profileImageUrl.toString()) + Log.d("TEST1",password) val response: SignUpResponse = networkStudentDataSource.signUp( request = SignUpRequest( schoolVerificationCode = schoolVerificationCode, @@ -112,7 +116,7 @@ internal class StudentRepositoryImpl @Inject constructor( override suspend fun fetchMyPage(): MyPage = networkStudentDataSource.fetchMyPage().toModel() override suspend fun editProfile(profileImageUrl: String) { - TODO("Not yet implemented") + networkStudentDataSource.editProfile(request = EditProfileRequest(profileImageUrl)) } override suspend fun withdraw() { diff --git a/feature/build.gradle.kts b/feature/build.gradle.kts index f8948de59..933149dc5 100644 --- a/feature/build.gradle.kts +++ b/feature/build.gradle.kts @@ -55,8 +55,9 @@ dependencies { implementation(project(ProjectPaths.Shared.VALIDATOR)) implementation(project(ProjectPaths.Core.UI)) - implementation(project(ProjectPaths.Core.DESIGN_SYSTEM)) + implementation(project(ProjectPaths.Core.FILE)) + implementation(project(ProjectPaths.DATA)) implementation(libs.androidx.core) diff --git a/feature/src/main/java/team/aliens/dms/android/feature/editprofile/EditProfileImageScreen.kt b/feature/src/main/java/team/aliens/dms/android/feature/editprofile/EditProfileImageScreen.kt index 297af92e6..9a5849b0d 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/editprofile/EditProfileImageScreen.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/editprofile/EditProfileImageScreen.kt @@ -1,117 +1,168 @@ package team.aliens.dms.android.feature.editprofile +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.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.draw.clip +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.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage import com.ramcosta.composedestinations.annotation.Destination +import team.aliens.dms.android.core.designsystem.Button +import team.aliens.dms.android.core.designsystem.DmsIcon +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.DmsTopAppBar +import team.aliens.dms.android.core.designsystem.LocalToast +import team.aliens.dms.android.core.designsystem.Scaffold +import team.aliens.dms.android.core.designsystem.clickable +import team.aliens.dms.android.core.ui.PaddingDefaults +import team.aliens.dms.android.core.ui.bottomPadding +import team.aliens.dms.android.core.ui.collectInLaunchedEffectWithLifecycle +import team.aliens.dms.android.core.ui.horizontalPadding +import team.aliens.dms.android.feature.R import team.aliens.dms.android.feature.editprofile.navigation.EditProfileNavigator +@OptIn(ExperimentalMaterial3Api::class) @Destination @Composable internal fun EditProfileImageScreen( modifier: Modifier = Modifier, navigator: EditProfileNavigator, - // editProfileImageViewModel: EditProfileImageViewModel = hiltViewModel(), -) {/* - val uiState by editProfileImageViewModel.stateFlow.collectAsStateWithLifecycle() - editProfileImageViewModel.sideEffectFlow.collectInLaunchedEffectWithLifeCycle { sideEffect -> - when (sideEffect) { - UploadProfileImageSideEffect.EditProfileFailed -> {} - UploadProfileImageSideEffect.EditProfileSucceed -> navigator.popBackStack() - UploadProfileImageSideEffect.ImageNotSelected -> {} - UploadProfileImageSideEffect.UploadProfileImageFailed -> {} +) { + val viewModel: EditProfileImageViewModel = hiltViewModel() + val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() + val context = LocalContext.current + val toast = LocalToast.current + val activityResultLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { imageUrl -> + viewModel.postIntent( + EditProfileImageIntent.UpdateProfileImage( + uri = imageUrl, + context = context, + ) + ) } - } + ) - val takePhotoLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.PickVisualMedia() - ) { takenImage: Uri? -> - if (takenImage != null) { - editProfileImageViewModel.postIntent( - UploadProfileImageIntent.SelectImage(takenImage), + viewModel.sideEffectFlow.collectInLaunchedEffectWithLifecycle { sideEffect -> + when (sideEffect) { + EditProfileImageSideEffect.ProfileImageSet -> toast.showSuccessToast( + message = context.getString( + R.string.edit_profile_success + ) + ) + + EditProfileImageSideEffect.ProfileImageBadRequest -> toast.showErrorToast( + message = context.getString( + R.string.edit_profile_fail + ) ) } } - val selectImageFromGalleryLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.PickVisualMedia(), - ) { selectedImage: Uri? -> - if (selectedImage != null) { - editProfileImageViewModel.postIntent( - UploadProfileImageIntent.SelectImage(selectedImage), + + Scaffold( + topBar = { + DmsTopAppBar( + title = { Text(text = stringResource(id = R.string.edit_profile)) }, + navigationIcon = { + IconButton(onClick = navigator::navigateUp) { + Icon( + painter = painterResource(id = DmsIcon.Back), + contentDescription = stringResource(id = R.string.top_bar_back_button), + ) + } + } ) } - } - // todo - val onTakePhoto = { - *//*takePhotoLauncher.launch( - PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.) - )*//* - } - val onSelectPhoto = { - selectImageFromGalleryLauncher.launch( - PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), - ) - } - LaunchedEffect(Unit) { - if (selectImageType != null) when (selectImageType) { - SelectImageType.TAKE_PHOTO -> { - // TODO take Photo + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .imePadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.weight(1f)) + SetImage( + uiState = uiState, + onChangeImage = { + val mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly + val request = PickVisualMediaRequest(mediaType) + activityResultLauncher.launch(request) + } + ) + Spacer(modifier = Modifier.weight(1f)) + Button( + modifier = Modifier + .fillMaxWidth() + .horizontalPadding() + .bottomPadding(), + onClick = { + viewModel.postIntent( + EditProfileImageIntent.EditProfile + ) + }, + enabled = uiState.buttonEnabled, + ) { + Text(text = stringResource(id = R.string.next)) } - - SelectImageType.SELECT_FROM_GALLERY -> onSelectPhoto() - else -> {} } } +} - var selectImageTypeDialogState by remember { mutableStateOf(false) } - val onSelectImageTypeDialogShow = { selectImageTypeDialogState = true } - val onSelectImageTypeDialogDismiss = { selectImageTypeDialogState = false } - - if (selectImageTypeDialogState) { - SelectImageTypeDialog( - onCancel = onSelectImageTypeDialogDismiss, - onTakePhoto = {}, - onSelectPhoto = onSelectPhoto, - onDialogDismiss = onSelectImageTypeDialogDismiss, - ) - } - - Column( - modifier = modifier - .fillMaxSize() - .background(DormTheme.colors.background), - horizontalAlignment = Alignment.CenterHorizontally, +@Composable +private fun SetImage( + uiState: EditProfileImageUiState, + onChangeImage: () -> Unit, +) { + Box( + contentAlignment = Alignment.BottomEnd ) { - TopBar( - title = stringResource(R.string.my_page_edit_profile), - onPrevious = navigator::popBackStack, + AsyncImage( + modifier = Modifier + .size(150.dp) + .clip(CircleShape) + .clickable( + onClick = onChangeImage + ), + contentScale = ContentScale.Crop, + model = uiState.uri ?: DmsIcon.ProfileDefault, + contentDescription = stringResource(id = R.string.edit_profile_profile_image), ) - Spacer(Modifier.weight(1f)) - Box( - contentAlignment = Alignment.BottomEnd, - ) { - AsyncImage( - modifier = Modifier - .size(100.dp) - .clip(CircleShape) - .dormClickable { onSelectImageTypeDialogShow() }, - model = uiState.selectedImageUri, - contentDescription = null, - ) - Image( - modifier = Modifier.size(30.dp), - painter = painterResource(R.drawable.ic_mypage_edit), - contentDescription = null, - ) - } - Spacer(Modifier.height(80.dp)) - DormContainedLargeButton( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.Check), - color = DormButtonColor.Blue, - enabled = uiState.uploadButtonEnabled, - ) { - editProfileImageViewModel.postIntent(UploadProfileImageIntent.UploadAndEditProfile) - } - Spacer(Modifier.weight(1f)) - }*/ + Image( + modifier = Modifier + .size(46.dp) + .clip(CircleShape) + .background(DmsTheme.colorScheme.primary) + .padding(PaddingDefaults.Small) + .clickable(onClick = onChangeImage), + painter = painterResource(id = DmsIcon.Plus), + contentDescription = stringResource(id = R.string.edit_profile_profile_add), + ) + } } diff --git a/feature/src/main/java/team/aliens/dms/android/feature/editprofile/EditProfileImageViewModel.kt b/feature/src/main/java/team/aliens/dms/android/feature/editprofile/EditProfileImageViewModel.kt index 95bf9b537..46d4a4cb1 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/editprofile/EditProfileImageViewModel.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/editprofile/EditProfileImageViewModel.kt @@ -1,110 +1,114 @@ package team.aliens.dms.android.feature.editprofile -/* +import android.content.Context import android.net.Uri -import androidx.core.net.toFile import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import team.aliens.dms.android.domain.model.file.UploadFileInput -import team.aliens.dms.android.domain.model.student.EditProfileInput -import team.aliens.dms.android.domain.usecase.file.UploadFileUseCase -import team.aliens.dms.android.domain.usecase.student.EditProfileUseCase -import team.aliens.dms.android.feature._legacy.base.BaseMviViewModel -import team.aliens.dms.android.feature._legacy.base.MviIntent -import team.aliens.dms.android.feature._legacy.base.MviSideEffect -import team.aliens.dms.android.feature._legacy.base.MviState +import team.aliens.dms.android.core.ui.mvi.BaseMviViewModel +import team.aliens.dms.android.core.ui.mvi.Intent +import team.aliens.dms.android.core.ui.mvi.SideEffect +import team.aliens.dms.android.core.ui.mvi.UiState +import team.aliens.dms.android.data.file.repository.FileRepository +import team.aliens.dms.android.data.student.repository.StudentRepository +import java.io.File import javax.inject.Inject @HiltViewModel internal class EditProfileImageViewModel @Inject constructor( - private val uploadFileUseCase: UploadFileUseCase, - private val editProfileUseCase: EditProfileUseCase, -) : BaseMviViewModel( - initialState = UploadProfileImageState.initial(), + private val studentRepository: StudentRepository, + private val fileRepository: FileRepository, +) : BaseMviViewModel( + initialState = EditProfileImageUiState.initial(), ) { - override fun processIntent(intent: UploadProfileImageIntent) { + override fun processIntent(intent: EditProfileImageIntent) { when (intent) { - is UploadProfileImageIntent.SelectImage -> reduce( - newState = stateFlow.value.copy( - selectedImageUri = intent.selectedImageUri, - ), + is EditProfileImageIntent.UpdateProfileImage -> updateProfileImage( + context = intent.context, + uri = intent.uri, ) - UploadProfileImageIntent.UploadAndEditProfile -> uploadAndEditProfile() + is EditProfileImageIntent.EditProfile -> editProfile() } } - private fun uploadAndEditProfile() { - if (stateFlow.value.selectedImageUri == null) { - postSideEffect(UploadProfileImageSideEffect.ImageNotSelected) - return + private fun updateProfileImage( + context: Context, + uri: Uri?, + ) { + if (uri != null) { + reduce(newState = stateFlow.value.copy(uri = uri)) + fetchPresignedUrl( + file = team.aliens.dms.android.core.file.File.toFile( + context = context, + uri = uri, + ) + ) + } else { + postSideEffect(EditProfileImageSideEffect.ProfileImageBadRequest) } - disableUploadButton() - viewModelScope.launch(Dispatchers.IO) { - val result = kotlin.runCatching { - uploadProfile() - }.onFailure { - postSideEffect(UploadProfileImageSideEffect.UploadProfileImageFailed) - return@launch - } + } - if (result.isSuccess) kotlin.runCatching { - editProfileUseCase( - editProfileInput = EditProfileInput( - profileImageUrl = result.getOrThrow(), - ), - ) - }.onSuccess { - postSideEffect(UploadProfileImageSideEffect.EditProfileSucceed) + private fun fetchPresignedUrl( + file: File, + ) { + viewModelScope.launch(Dispatchers.IO) { + runCatching { + fileRepository.fetchPresignedUrl(fileName = file.name) + }.onSuccess { fileUrl -> + runCatching { + fileRepository.uploadFile( + presignedUrl = fileUrl.fileUploadUrl, + file = file, + ) + }.onSuccess { + reduce( + newState = stateFlow.value.copy( + profileImageUrl = fileUrl.fileUrl, + buttonEnabled = true, + ) + ) + } }.onFailure { - postSideEffect(UploadProfileImageSideEffect.EditProfileFailed) + postSideEffect(EditProfileImageSideEffect.ProfileImageBadRequest) } } } - private fun uploadProfile(): String { - return runBlocking(Dispatchers.IO) { - uploadFileUseCase( - uploadFileInput = UploadFileInput( - file = stateFlow.value.selectedImageUri!!.toFile(), // not-null asserted - ), - ) - }.fileUrl - } - - private fun disableUploadButton() { - reduce( - newState = stateFlow.value.copy( - uploadButtonEnabled = false, - ) - ) + private fun editProfile() = viewModelScope.launch(Dispatchers.IO) { + runCatching { + studentRepository.editProfile(stateFlow.value.profileImageUrl!!) + }.onSuccess { + postSideEffect(EditProfileImageSideEffect.ProfileImageSet) + reduce(newState = stateFlow.value.copy(buttonEnabled = false)) + }.onFailure { + postSideEffect(EditProfileImageSideEffect.ProfileImageBadRequest) + } } -} -internal sealed class UploadProfileImageIntent : MviIntent { - class SelectImage(val selectedImageUri: Uri) : UploadProfileImageIntent() - object UploadAndEditProfile : UploadProfileImageIntent() } -internal data class UploadProfileImageState( - val selectedImageUri: Uri?, - val uploadButtonEnabled: Boolean, -) : MviState { +data class EditProfileImageUiState( + val profileImageUrl: String?, + val uri: Uri?, + val buttonEnabled: Boolean, +) : UiState() { companion object { - fun initial() = UploadProfileImageState( - selectedImageUri = null, - uploadButtonEnabled = false, + fun initial() = EditProfileImageUiState( + profileImageUrl = null, + uri = null, + buttonEnabled = false, ) } } -internal sealed class UploadProfileImageSideEffect : MviSideEffect { - object EditProfileSucceed : UploadProfileImageSideEffect() - object EditProfileFailed : UploadProfileImageSideEffect() - object ImageNotSelected : UploadProfileImageSideEffect() - object UploadProfileImageFailed : UploadProfileImageSideEffect() +internal sealed class EditProfileImageIntent : Intent() { + class UpdateProfileImage(val uri: Uri?, val context: Context) : EditProfileImageIntent() + data object EditProfile : EditProfileImageIntent() +} + +sealed class EditProfileImageSideEffect : SideEffect() { + data object ProfileImageSet : EditProfileImageSideEffect() + data object ProfileImageBadRequest : EditProfileImageSideEffect() } -*/ diff --git a/feature/src/main/java/team/aliens/dms/android/feature/findid/FindIdViewModel.kt b/feature/src/main/java/team/aliens/dms/android/feature/findid/FindIdViewModel.kt index 7040cfd9f..e83492ca7 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/findid/FindIdViewModel.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/findid/FindIdViewModel.kt @@ -85,7 +85,7 @@ internal class FindIdViewModel @Inject constructor( runCatching { studentRepository.findId( schoolId = selectedSchool.id, - studentName = selectedSchool.name, + studentName = name, grade = grade.toInt(), classRoom = classRoom.toInt(), number = number.toInt(), diff --git a/feature/src/main/java/team/aliens/dms/android/feature/main/Main.kt b/feature/src/main/java/team/aliens/dms/android/feature/main/Main.kt index 20a2da500..95f0cc0a8 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/main/Main.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/main/Main.kt @@ -256,7 +256,7 @@ private enum class MainSections( ), MY_PAGE( route = "my_page", - iconRes = R.drawable.ic_my_page, + iconRes = R.drawable.ic_person, labelRes = team.aliens.dms.android.feature.R.string.bottom_nav_my_page, ), ; diff --git a/feature/src/main/java/team/aliens/dms/android/feature/main/mypage/MyPageScreen.kt b/feature/src/main/java/team/aliens/dms/android/feature/main/mypage/MyPageScreen.kt index bc410a4a7..3112880d3 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/main/mypage/MyPageScreen.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/main/mypage/MyPageScreen.kt @@ -2,7 +2,10 @@ package team.aliens.dms.android.feature.main.mypage import androidx.annotation.StringRes import androidx.compose.foundation.BorderStroke +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.PaddingValues import androidx.compose.foundation.layout.Row @@ -11,7 +14,9 @@ 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.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -27,15 +32,20 @@ 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.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage import com.ramcosta.composedestinations.annotation.Destination import team.aliens.dms.android.core.designsystem.AlertDialog import team.aliens.dms.android.core.designsystem.ButtonDefaults +import team.aliens.dms.android.core.designsystem.DmsIcon import team.aliens.dms.android.core.designsystem.DmsTheme import team.aliens.dms.android.core.designsystem.DmsTopAppBar import team.aliens.dms.android.core.designsystem.Gray10 @@ -243,31 +253,30 @@ private fun UserInformation( ) } // TODO: v1.2.0 - /* Box( modifier = Modifier.size(64.dp), contentAlignment = Alignment.BottomEnd, ) { - Image( + AsyncImage( modifier = Modifier - .size(64.dp) - .clip(CircleShape) - .clickable(onClick = onNavigateToEditProfileImage), - painter = rememberAsyncImagePainter( - model = profileImageUrl ?: R.drawable.img_profile_default - ), - contentDescription = stringResource(id = R.string.profile_image), + .size(64.dp) + .clip(CircleShape) + .clickable(onClick = onNavigateToEditProfileImage), + contentScale = ContentScale.Crop, + model = profileImageUrl ?: DmsIcon.ProfileDefault, + contentDescription = stringResource(id = R.string.profile_image), ) Image( modifier = Modifier .size(20.dp) .clip(CircleShape) - .clickable(onClick = onNavigateToEditProfileImage), - painter = painterResource(id = R.drawable.ic_my_page_edit), - contentDescription = null, + .clickable(onClick = onNavigateToEditProfileImage) + .background(DmsTheme.colorScheme.line) + .padding(3.dp), + painter = painterResource(id = DmsIcon.Edit), + contentDescription = stringResource(id = R.string.my_page_edit_profile), ) } - */ } } diff --git a/feature/src/main/java/team/aliens/dms/android/feature/point/PointHistoryViewModel.kt b/feature/src/main/java/team/aliens/dms/android/feature/point/PointHistoryViewModel.kt index 60d45c898..9a0f122dc 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/point/PointHistoryViewModel.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/point/PointHistoryViewModel.kt @@ -38,7 +38,7 @@ internal class PointHistoryViewModel @Inject constructor( internal fun fetchPoints(pointType: PointType) { viewModelScope.launch(Dispatchers.IO) { runCatching { - pointRepository.fetchPoints(type = pointType) + pointRepository.fetchPoints(type = PointType.ALL) }.onSuccess { pointStatus -> this@PointHistoryViewModel.allPoints = pointStatus.points this@PointHistoryViewModel.scoreOfAllPoints = pointStatus.totalPoints diff --git a/feature/src/main/java/team/aliens/dms/android/feature/signup/5_SetIdScreen.kt b/feature/src/main/java/team/aliens/dms/android/feature/signup/5_SetIdScreen.kt index 0fe8a8920..52a78e00c 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/signup/5_SetIdScreen.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/signup/5_SetIdScreen.kt @@ -15,6 +15,8 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -25,6 +27,7 @@ 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.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -33,10 +36,11 @@ import com.ramcosta.composedestinations.annotation.Destination import kotlinx.coroutines.delay import team.aliens.dms.android.core.designsystem.AlertDialog import team.aliens.dms.android.core.designsystem.ContainedButton -import team.aliens.dms.android.core.designsystem.Scaffold +import team.aliens.dms.android.core.designsystem.DmsIcon import team.aliens.dms.android.core.designsystem.DmsTheme import team.aliens.dms.android.core.designsystem.DmsTopAppBar import team.aliens.dms.android.core.designsystem.LocalToast +import team.aliens.dms.android.core.designsystem.Scaffold import team.aliens.dms.android.core.designsystem.ShadowDefaults import team.aliens.dms.android.core.designsystem.TextButton import team.aliens.dms.android.core.designsystem.TextField @@ -127,11 +131,16 @@ internal fun SetIdScreen( } Scaffold( - modifier = modifier, topBar = { DmsTopAppBar( - title = {}, - navigationIcon = {}, + navigationIcon = { + IconButton(onClick = navigator::navigateUp) { + Icon( + painter = painterResource(id = DmsIcon.Back), + contentDescription = stringResource(id = R.string.top_bar_back_button), + ) + } + }, ) }, ) { padValues -> diff --git a/feature/src/main/java/team/aliens/dms/android/feature/signup/6_SetPasswordScreen.kt b/feature/src/main/java/team/aliens/dms/android/feature/signup/6_SetPasswordScreen.kt index 3abfb767c..52954b483 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/signup/6_SetPasswordScreen.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/signup/6_SetPasswordScreen.kt @@ -24,9 +24,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.ramcosta.composedestinations.annotation.Destination import team.aliens.dms.android.core.designsystem.AlertDialog import team.aliens.dms.android.core.designsystem.ContainedButton -import team.aliens.dms.android.core.designsystem.Scaffold +import team.aliens.dms.android.core.designsystem.DmsIcon import team.aliens.dms.android.core.designsystem.DmsTopAppBar import team.aliens.dms.android.core.designsystem.LocalToast +import team.aliens.dms.android.core.designsystem.Scaffold import team.aliens.dms.android.core.designsystem.TextButton import team.aliens.dms.android.core.ui.Banner import team.aliens.dms.android.core.ui.BannerDefaults @@ -93,7 +94,7 @@ internal fun SignUpSetPasswordScreen( viewModel.sideEffectFlow.collectInLaunchedEffectWithLifecycle { sideEffect -> when (sideEffect) { // FIXME: 이미지 업로드 구현 - SignUpSideEffect.PasswordSet -> navigator.openTerms() + SignUpSideEffect.PasswordSet -> navigator.openSetProfileImage() SignUpSideEffect.PasswordMismatch -> toast.showErrorToast( message = context.getString(R.string.sign_up_set_password_error_password_mismatch), ) @@ -114,11 +115,10 @@ internal fun SignUpSetPasswordScreen( modifier = modifier, topBar = { DmsTopAppBar( - title = {}, navigationIcon = { IconButton(onClick = navigator::popUpToSetId) { Icon( - painter = painterResource(id = R.drawable.ic_baseline_arrow_back_24), + painter = painterResource(id = DmsIcon.Back), contentDescription = stringResource(id = R.string.top_bar_back_button), ) } diff --git a/feature/src/main/java/team/aliens/dms/android/feature/signup/7_SetProfileImageScreen.kt b/feature/src/main/java/team/aliens/dms/android/feature/signup/7_SetProfileImageScreen.kt index 64f7d3777..616030234 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/signup/7_SetProfileImageScreen.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/signup/7_SetProfileImageScreen.kt @@ -1,128 +1,191 @@ package team.aliens.dms.android.feature.signup -// TODO: 구현 - +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +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.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.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.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.draw.clip +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.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage import com.ramcosta.composedestinations.annotation.Destination +import team.aliens.dms.android.core.designsystem.Button +import team.aliens.dms.android.core.designsystem.DmsIcon +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.DmsTopAppBar +import team.aliens.dms.android.core.designsystem.LocalToast +import team.aliens.dms.android.core.designsystem.Scaffold +import team.aliens.dms.android.core.designsystem.clickable +import team.aliens.dms.android.core.ui.Banner +import team.aliens.dms.android.core.ui.BannerDefaults +import team.aliens.dms.android.core.ui.DefaultVerticalSpace +import team.aliens.dms.android.core.ui.PaddingDefaults +import team.aliens.dms.android.core.ui.bottomPadding +import team.aliens.dms.android.core.ui.collectInLaunchedEffectWithLifecycle +import team.aliens.dms.android.core.ui.horizontalPadding +import team.aliens.dms.android.core.ui.startPadding +import team.aliens.dms.android.core.ui.topPadding +import team.aliens.dms.android.feature.R import team.aliens.dms.android.feature.signup.navigation.SignUpNavigator +@OptIn(ExperimentalMaterial3Api::class) @Destination @Composable internal fun SetProfileImageScreen( modifier: Modifier = Modifier, navigator: SignUpNavigator, - // signUpViewModel: SignUpViewModel, -) {/* - val selectImageFromGalleryLauncher = rememberLauncherForActivityResult( + viewModel: SignUpViewModel, +) { + val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() + val context = LocalContext.current + val toast = LocalToast.current + val activityResultLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), - ) { - if (it != null) { - signUpViewModel.postIntent(SignUpIntent.SetProfileImage.SelectProfileImage(it)) + onResult = { imageUrl -> + viewModel.postIntent(SignUpIntent.UpdateProfileImage(uri = imageUrl, context = context)) } - } - - val onSelectPhoto = { - selectImageFromGalleryLauncher.launch( - PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) - ) - } - - val toast = LocalToast.current - val uploadImageFailedMessage = - stringResource(id = R.string.sign_up_profile_error_load_image_error) - val uploadImageNotSelectedMessage = - stringResource(id = R.string.sign_up_profile_error_image_not_selected) + ) - var selectImageTypeDialogState by remember { mutableStateOf(false) } - val onSelectImageTypeDialogShow = { selectImageTypeDialogState = true } - val onSelectImageTypeDialogDismiss = { selectImageTypeDialogState = false } - - val uiState by signUpViewModel.stateFlow.collectAsStateWithLifecycle() - - LaunchedEffect(Unit) { - signUpViewModel.sideEffectFlow.collect { - when (it) { - is SignUpSideEffect.SetProfileImage.UploadImageSuccess -> { - navigator.openTerms() - } - - is SignUpSideEffect.SetProfileImage.UploadImageFailed -> { - toast.showErrorToast(uploadImageFailedMessage) - } - - is SignUpSideEffect.SetProfileImage.UploadImageNotSelected -> { - toast.showErrorToast(uploadImageNotSelectedMessage) - } + viewModel.sideEffectFlow.collectInLaunchedEffectWithLifecycle { sideEffect -> + when (sideEffect) { + SignUpSideEffect.ProfileImageSet -> navigator.openTerms() + SignUpSideEffect.ProfileImageBadRequest -> toast.showErrorToast( + message = context.getString(R.string.sign_up_profile_error_load_image_error) + ) - else -> {} + else -> { /* explicit blank */ } } } - - if (selectImageTypeDialogState) { - SelectImageTypeDialog( - onCancel = onSelectImageTypeDialogDismiss, - onTakePhoto = {}, - onSelectPhoto = onSelectPhoto, - onDialogDismiss = onSelectImageTypeDialogDismiss, - ) - } - - Column( - modifier = Modifier - .fillMaxSize() - .background(DormTheme.colors.surface) - .padding( - start = 16.dp, - end = 16.dp, - ), - ) { - Spacer(modifier = Modifier.height(108.dp)) - AppLogo(darkIcon = isSystemInDarkTheme()) - Space(space = 8.dp) - Body2(text = stringResource(id = R.string.ProfileImage)) - Space(space = 80.dp) + + Scaffold( + topBar = { + DmsTopAppBar( + navigationIcon = { + IconButton(onClick = navigator::popUpToSetPassword) { + Icon( + painter = painterResource(id = DmsIcon.Back), + contentDescription = stringResource(id = R.string.top_bar_back_button), + ) + } + } + ) + } + ) { paddingValues -> Column( modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(), - horizontalAlignment = Alignment.CenterHorizontally, + .fillMaxSize() + .padding(paddingValues) + .imePadding(), ) { - Box( - modifier = Modifier.size(150.dp), - contentAlignment = Alignment.BottomEnd, + Spacer(modifier = Modifier.weight(1f)) + Banner( + modifier = Modifier + .fillMaxWidth() + .topPadding(BannerDefaults.DefaultTopSpace) + .startPadding(), + message = { BannerDefaults.DefaultText(text = stringResource(id = R.string.sign_up_set_profile)) }, + ) + Spacer(modifier = Modifier.weight(1f)) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(DefaultVerticalSpace), + horizontalAlignment = Alignment.CenterHorizontally, ) { - AsyncImage( - modifier = Modifier - .size(150.dp) - .clip(CircleShape) - .clickable(onClick = onSelectImageTypeDialogShow), - model = uiState.profileImageUri ?: defaultProfileUrl, - contentDescription = null, + Spacer(modifier = Modifier.weight(1f)) + SetImage( + uiState = uiState, + onChangeImage = { + val mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly + val request = PickVisualMediaRequest(mediaType) + activityResultLauncher.launch(request) + } ) - Image( - modifier = Modifier.size(30.dp), - painter = painterResource(R.drawable.addplusimage), - contentDescription = null, + Spacer(modifier = Modifier.weight(2f)) + Text( + modifier = Modifier + .clickable( + onClick = navigator::openTerms + ) + .padding( + vertical = PaddingDefaults.Small, + horizontal = PaddingDefaults.Large, + ), + text = stringResource( + id = R.string.sign_up_profile__set_later, + ), + style = DmsTheme.typography.button, ) - } - RatioSpace(height = 0.62f) - ButtonText( - modifier = Modifier.dormClickable( - rippleEnabled = false, + Spacer(modifier = Modifier.height(20.dp)) + Button( + modifier = Modifier + .fillMaxWidth() + .horizontalPadding() + .bottomPadding(), onClick = navigator::openTerms, - ), - text = stringResource(id = R.string.SettingLater), - ) - Space(space = 30.dp) - DormContainedLargeButton( - text = stringResource(id = R.string.Next), - color = DormButtonColor.Blue, - enabled = uiState.profileImageButtonEnabled, - ) { - signUpViewModel.postIntent(SignUpIntent.SetProfileImage.UploadImage) + enabled = uiState.setProfileButtonEnabled, + ) { + Text(text = stringResource(id = R.string.next)) + } } } - }*/ -} \ No newline at end of file + } +} + +@Composable +private fun SetImage( + uiState: SignUpUiState, + onChangeImage: () -> Unit, +) { + Box( + contentAlignment = Alignment.BottomEnd + ) { + AsyncImage( + modifier = Modifier + .size(150.dp) + .clip(CircleShape) + .clickable( + onClick = onChangeImage, + ), + contentScale = ContentScale.Crop, + model = uiState.uri ?: DmsIcon.ProfileDefault, + contentDescription = stringResource(id = R.string.sign_up_profile_image), + ) + Image( + modifier = Modifier + .size(46.dp) + .clip(CircleShape) + .background(DmsTheme.colorScheme.primary) + .padding(PaddingDefaults.Small) + .clickable( + onClick = onChangeImage, + ), + painter = painterResource(id = DmsIcon.Plus), + contentDescription = stringResource(id = R.string.sign_up_profile_add), + ) + } +} diff --git a/feature/src/main/java/team/aliens/dms/android/feature/signup/SignUpViewModel.kt b/feature/src/main/java/team/aliens/dms/android/feature/signup/SignUpViewModel.kt index 96ba64f22..d42c691cb 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/signup/SignUpViewModel.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/signup/SignUpViewModel.kt @@ -1,5 +1,7 @@ package team.aliens.dms.android.feature.signup +import android.content.Context +import android.net.Uri import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -10,11 +12,13 @@ import team.aliens.dms.android.core.ui.mvi.SideEffect import team.aliens.dms.android.core.ui.mvi.UiState import team.aliens.dms.android.data.auth.model.EmailVerificationType import team.aliens.dms.android.data.auth.repository.AuthRepository +import team.aliens.dms.android.data.file.repository.FileRepository import team.aliens.dms.android.data.school.repository.SchoolRepository import team.aliens.dms.android.data.student.repository.StudentRepository import team.aliens.dms.android.shared.validator.checkIfEmailValid import team.aliens.dms.android.shared.validator.checkIfIdValid import team.aliens.dms.android.shared.validator.checkIfPasswordValid +import java.io.File import java.util.UUID import javax.inject.Inject @@ -23,6 +27,7 @@ class SignUpViewModel @Inject constructor( private val studentRepository: StudentRepository, private val schoolRepository: SchoolRepository, private val authRepository: AuthRepository, + private val fileRepository: FileRepository, ) : BaseMviViewModel( initialState = SignUpUiState.initial(), ) { @@ -47,7 +52,12 @@ class SignUpViewModel @Inject constructor( is SignUpIntent.UpdatePassword -> updatePassword(value = intent.value) is SignUpIntent.UpdatePasswordRepeat -> updatePasswordRepeat(value = intent.value) SignUpIntent.ConfirmPassword -> confirmPassword() - SignUpIntent.SignUp -> signUp() + is SignUpIntent.UpdateProfileImage -> changeProfileImage( + uri = intent.uri, + context = intent.context + ) + + is SignUpIntent.SignUp -> signUp() } } @@ -252,6 +262,49 @@ class SignUpViewModel @Inject constructor( postSideEffect(SignUpSideEffect.PasswordSet) } + private fun changeProfileImage( + context: Context, + uri: Uri?, + ) { + if (uri != null) { + reduce(newState = stateFlow.value.copy(uri = uri)) + fetchPresignedUrl( + file = team.aliens.dms.android.core.file.File.toFile( + context = context, + uri = uri, + ) + ) + } else { + postSideEffect(SignUpSideEffect.ProfileImageBadRequest) + } + } + + private fun fetchPresignedUrl( + file: File, + ) { + viewModelScope.launch(Dispatchers.IO) { + runCatching { + fileRepository.fetchPresignedUrl(fileName = file.name) + }.onSuccess { fileUrl -> + runCatching { + fileRepository.uploadFile( + presignedUrl = fileUrl.fileUploadUrl, + file = file, + ) + }.onSuccess { + reduce( + newState = stateFlow.value.copy( + profileImageUrl = fileUrl.fileUrl, + setProfileButtonEnabled = true, + ), + ) + } + }.onFailure { + postSideEffect(SignUpSideEffect.ProfileImageBadRequest) + } + } + } + private fun signUp() = viewModelScope.launch(Dispatchers.IO) { runCatching { val capturedState = stateFlow.value @@ -303,6 +356,8 @@ data class SignUpUiState( // SetProfileImage val profileImageUrl: String?, + val uri: Uri?, + val setProfileButtonEnabled: Boolean, ) : UiState() { companion object { fun initial() = SignUpUiState( @@ -319,6 +374,8 @@ data class SignUpUiState( password = "", passwordRepeat = "", profileImageUrl = null, + uri = null, + setProfileButtonEnabled = false, ) } } @@ -354,6 +411,9 @@ sealed class SignUpIntent : Intent() { class UpdatePasswordRepeat(val value: String) : SignUpIntent() data object ConfirmPassword : SignUpIntent() + // SetProfileImage + class UpdateProfileImage(val uri: Uri?, val context: Context) : SignUpIntent() + // Terms data object SignUp : SignUpIntent() } @@ -391,6 +451,10 @@ sealed class SignUpSideEffect : SideEffect() { data object PasswordMismatch : SignUpSideEffect() data object InvalidPassword : SignUpSideEffect() + // SetProfileImage + data object ProfileImageSet : SignUpSideEffect() + data object ProfileImageBadRequest : SignUpSideEffect() + // Terms data object SignedUp : SignUpSideEffect() data object SignUpFailure : SignUpSideEffect() diff --git a/feature/src/main/res/drawable/ic_my_page_edit.xml b/feature/src/main/res/drawable/ic_my_page_edit.xml deleted file mode 100644 index 9ad436266..000000000 --- a/feature/src/main/res/drawable/ic_my_page_edit.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/feature/src/main/res/values/strings.xml b/feature/src/main/res/values/strings.xml index c161ed1ec..52108e59c 100644 --- a/feature/src/main/res/values/strings.xml +++ b/feature/src/main/res/values/strings.xml @@ -161,8 +161,10 @@ 비밀번호 형식을 확인해주세요 비밀번호를 확인해주세요 + 프로필 설정 프로필 사진 다음에 설정하기 + 프로필 추가하기 회원가입 마치기 @@ -342,4 +344,11 @@ 새로 고침 성공 외출 신청 시간이 아닙니다. + + 프로필 수정 + 프로필 사진 + 프로필 추가하기 + 프로필이 변경되었습니다 + 프로필 변경에 실패했습니다 + \ No newline at end of file diff --git a/network/src/main/java/team/aliens/dms/android/network/file/apiservice/FileApiService.kt b/network/src/main/java/team/aliens/dms/android/network/file/apiservice/FileApiService.kt new file mode 100644 index 000000000..3a4dd1e1f --- /dev/null +++ b/network/src/main/java/team/aliens/dms/android/network/file/apiservice/FileApiService.kt @@ -0,0 +1,26 @@ +package team.aliens.dms.android.network.file.apiservice + +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Query +import retrofit2.http.Url +import team.aliens.dms.android.network.file.model.FetchPresignedUrlResponse +import team.aliens.dms.android.network.file.model.UploadFileResponse +import java.io.File + +internal interface FileApiService { + + @GET("/files/url") + suspend fun fetchPresignedUrl( + @Query("file_name") fileName: String, + ): FetchPresignedUrlResponse + + @PUT + suspend fun uploadFile( + @Url presignedUrl: String, + @Body file: RequestBody, + ) +} diff --git a/network/src/main/java/team/aliens/dms/android/network/file/datasource/NetworkFileDataSource.kt b/network/src/main/java/team/aliens/dms/android/network/file/datasource/NetworkFileDataSource.kt new file mode 100644 index 000000000..fc0b600d6 --- /dev/null +++ b/network/src/main/java/team/aliens/dms/android/network/file/datasource/NetworkFileDataSource.kt @@ -0,0 +1,9 @@ +package team.aliens.dms.android.network.file.datasource + +import team.aliens.dms.android.network.file.model.FetchPresignedUrlResponse +import java.io.File + +abstract class NetworkFileDataSource { + abstract suspend fun fetchPresignedUrl(fileName: String): FetchPresignedUrlResponse + abstract suspend fun uploadFile(presignedUrl: String, file: File) +} diff --git a/network/src/main/java/team/aliens/dms/android/network/file/datasource/NetworkFileDataSourceImpl.kt b/network/src/main/java/team/aliens/dms/android/network/file/datasource/NetworkFileDataSourceImpl.kt new file mode 100644 index 000000000..4db1d17fa --- /dev/null +++ b/network/src/main/java/team/aliens/dms/android/network/file/datasource/NetworkFileDataSourceImpl.kt @@ -0,0 +1,31 @@ +package team.aliens.dms.android.network.file.datasource + +import android.os.Build +import androidx.annotation.RequiresApi +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import team.aliens.dms.android.core.network.util.RequestType +import team.aliens.dms.android.core.network.util.handleNetworkRequest +import team.aliens.dms.android.network.file.apiservice.FileApiService +import team.aliens.dms.android.network.file.model.FetchPresignedUrlResponse +import java.io.File +import java.nio.file.Files +import javax.inject.Inject + +internal class NetworkFileDataSourceImpl @Inject constructor( + private val fileApiService: FileApiService +) : NetworkFileDataSource() { + override suspend fun fetchPresignedUrl(fileName: String): FetchPresignedUrlResponse = + handleNetworkRequest { fileApiService.fetchPresignedUrl(fileName) } + + @RequiresApi(Build.VERSION_CODES.O) + override suspend fun uploadFile(presignedUrl: String, file: File) = + handleNetworkRequest { + fileApiService.uploadFile( + presignedUrl = presignedUrl, + file = Files.readAllBytes(file.toPath()).toRequestBody( + contentType = RequestType.Binary.toMediaTypeOrNull() + ), + ) + } +} diff --git a/network/src/main/java/team/aliens/dms/android/network/file/di/ApiServiceModule.kt b/network/src/main/java/team/aliens/dms/android/network/file/di/ApiServiceModule.kt new file mode 100644 index 000000000..95ffccd68 --- /dev/null +++ b/network/src/main/java/team/aliens/dms/android/network/file/di/ApiServiceModule.kt @@ -0,0 +1,21 @@ +package team.aliens.dms.android.network.file.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.create +import team.aliens.dms.android.core.network.di.GlobalRetrofitClient +import team.aliens.dms.android.network.file.apiservice.FileApiService +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object ApiServiceModule { + + @Provides + @Singleton + fun provideFileApiService(@GlobalRetrofitClient retrofit: Retrofit): FileApiService = + retrofit.create(FileApiService::class.java) +} diff --git a/network/src/main/java/team/aliens/dms/android/network/file/di/DataSourceModule.kt b/network/src/main/java/team/aliens/dms/android/network/file/di/DataSourceModule.kt new file mode 100644 index 000000000..522ca87a1 --- /dev/null +++ b/network/src/main/java/team/aliens/dms/android/network/file/di/DataSourceModule.kt @@ -0,0 +1,18 @@ +package team.aliens.dms.android.network.file.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import team.aliens.dms.android.network.file.datasource.NetworkFileDataSource +import team.aliens.dms.android.network.file.datasource.NetworkFileDataSourceImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class DataSourceModule { + + @Binds + @Singleton + abstract fun bindNetworkFileDataSource(impl: NetworkFileDataSourceImpl): NetworkFileDataSource +} diff --git a/network/src/main/java/team/aliens/dms/android/network/file/model/FetchPresignedUrlResponse.kt b/network/src/main/java/team/aliens/dms/android/network/file/model/FetchPresignedUrlResponse.kt new file mode 100644 index 000000000..7cc3619c0 --- /dev/null +++ b/network/src/main/java/team/aliens/dms/android/network/file/model/FetchPresignedUrlResponse.kt @@ -0,0 +1,8 @@ +package team.aliens.dms.android.network.file.model + +import com.google.gson.annotations.SerializedName + +data class FetchPresignedUrlResponse( + @SerializedName("file_upload_url") val fileUploadUrl: String, + @SerializedName("file_url") val fileUrl: String, +) diff --git a/network/src/main/java/team/aliens/dms/android/network/file/model/UploadFileResponse.kt b/network/src/main/java/team/aliens/dms/android/network/file/model/UploadFileResponse.kt new file mode 100644 index 000000000..34be0197f --- /dev/null +++ b/network/src/main/java/team/aliens/dms/android/network/file/model/UploadFileResponse.kt @@ -0,0 +1,7 @@ +package team.aliens.dms.android.network.file.model + +import com.google.gson.annotations.SerializedName + +data class UploadFileResponse ( + @SerializedName("file_url") val fileUrl: String, +) diff --git a/network/src/main/java/team/aliens/dms/android/network/student/apiservice/StudentApiService.kt b/network/src/main/java/team/aliens/dms/android/network/student/apiservice/StudentApiService.kt index edbc460e0..519cbc3da 100644 --- a/network/src/main/java/team/aliens/dms/android/network/student/apiservice/StudentApiService.kt +++ b/network/src/main/java/team/aliens/dms/android/network/student/apiservice/StudentApiService.kt @@ -56,7 +56,7 @@ internal interface StudentApiService { @PATCH("/students/profile") @RequiresAccessToken - suspend fun editProfile(@Body request: EditProfileRequest) + suspend fun editProfile(@Body request: EditProfileRequest): Response? @DELETE("/students") @RequiresAccessToken diff --git a/network/src/main/java/team/aliens/dms/android/network/student/datasource/NetworkStudentDataSourceImpl.kt b/network/src/main/java/team/aliens/dms/android/network/student/datasource/NetworkStudentDataSourceImpl.kt index 9f001cb74..c2f5ffaf1 100644 --- a/network/src/main/java/team/aliens/dms/android/network/student/datasource/NetworkStudentDataSourceImpl.kt +++ b/network/src/main/java/team/aliens/dms/android/network/student/datasource/NetworkStudentDataSourceImpl.kt @@ -57,7 +57,7 @@ internal class NetworkStudentDataSourceImpl @Inject constructor( override suspend fun fetchMyPage(): FetchMyPageResponse = handleNetworkRequest { studentApiService.fetchMyPage() } - override suspend fun editProfile(request: EditProfileRequest) = + override suspend fun editProfile(request: EditProfileRequest): Unit = handleNetworkRequest { studentApiService.editProfile(request) } override suspend fun withdraw() = handleNetworkRequest { studentApiService.withdraw() } diff --git a/settings.gradle.kts b/settings.gradle.kts index 881cd0ac9..26fa02469 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,7 +24,6 @@ include(":shared:date") include(":shared:exception") include(":shared:model") include(":shared:validator") -include(":shared:timer") include(":core:database") include(":core:datastore") @@ -33,8 +32,9 @@ include(":core:jwt") include(":core:network") include(":core:project") include(":core:school") - +include(":core:file") include(":core:ui") + include(":data") include(":network") include(":database") diff --git a/shared/timer/build.gradle.kts b/shared/timer/build.gradle.kts deleted file mode 100644 index f5f481860..000000000 --- a/shared/timer/build.gradle.kts +++ /dev/null @@ -1,15 +0,0 @@ -plugins { - id("java-library") - alias(libs.plugins.jetbrainsKotlinJvm) -} - -java { - sourceCompatibility = Versions.java - targetCompatibility = Versions.java - - sourceSets { - dependencies { - implementation(libs.coroutines) - } - } -} diff --git a/shared/timer/src/main/java/team/aliens/dms/android/shared/timer/Timer.kt b/shared/timer/src/main/java/team/aliens/dms/android/shared/timer/Timer.kt deleted file mode 100644 index faca0c3a4..000000000 --- a/shared/timer/src/main/java/team/aliens/dms/android/shared/timer/Timer.kt +++ /dev/null @@ -1,92 +0,0 @@ -package team.aliens.dms.android.shared.timer -/* TODO -import kotlinx.coroutines.CompletableJob -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -*//** - * Timer that can countdown or count up the time. - * [StackOverFlow original idea](https://stackoverflow.com/questions/54095875/how-to-create-a-simple-countdown-timer-in-kotlin) - *//* -class Timer constructor( - val timeMillis: Long = 0L, - private val job: CompletableJob = SupervisorJob(), - private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default + job), -) { - private val timer: Job = makeCoroutinesTimer ( - delayMillis = 0, - repeatMillis = - ){ - - } - - *//** - * Makes coroutines timer. - * @param delayMillis Gives delay before the count starts. - * @param repeatMillis Count time. - * @param action A lambda which is the main action of the timer, for example, logging can be invoked inside this lambda. - *//* - private fun makeCoroutinesTimer( - delayMillis: Long = 0, - repeatMillis: Long = 0, - action: () -> Unit, - ) = scope.launch(Dispatchers.IO) { - delay(delayMillis) - if (repeatMillis > 0) { - while (true) { - action() - delay(repeatMillis) - } - } else { - action() - } - } - - *//** - * Resumes the timer if total time is not decreased, or resets and resumes. - * @see [resume], [reset] - *//* - fun start() { - - } - - *//** - * Pauses and reset the timer. - *//* - fun stop() { - - } - - *//** - * Resets the timer. - *//* - fun reset() { - - } - - *//** - * Resumes the timer. - *//* - fun resume() { - - } - - *//** - * Pauses the timer. - *//* - fun pause() { - - } - - fun minus() { - - } - - fun plus() { - - } -}*/