diff --git a/%25%23ok%C3%A9k%C3%89%C8%A2 b/%25%23ok%C3%A9k%C3%89%C8%A2 new file mode 100644 index 00000000..e69de29b diff --git a/a%20test%20%26 b/a%20test%20%26 new file mode 100644 index 00000000..e69de29b diff --git a/data/member/src/commonMain/kotlin/com/b1nd/dodam/member/MemberRepository.kt b/data/member/src/commonMain/kotlin/com/b1nd/dodam/member/MemberRepository.kt index b21a3838..6e9cc583 100644 --- a/data/member/src/commonMain/kotlin/com/b1nd/dodam/member/MemberRepository.kt +++ b/data/member/src/commonMain/kotlin/com/b1nd/dodam/member/MemberRepository.kt @@ -9,4 +9,5 @@ interface MemberRepository { suspend fun getMyInfo(): Flow> suspend fun deactivation(): Flow> suspend fun getMemberActiveAll(): Flow>> + suspend fun editMemberInfo(name: String, email: String, phone: String, profileImage: String?): Flow> } diff --git a/data/member/src/commonMain/kotlin/com/b1nd/dodam/member/repository/MemberRepositoryImpl.kt b/data/member/src/commonMain/kotlin/com/b1nd/dodam/member/repository/MemberRepositoryImpl.kt index 8b2b9905..ae30f6a7 100644 --- a/data/member/src/commonMain/kotlin/com/b1nd/dodam/member/repository/MemberRepositoryImpl.kt +++ b/data/member/src/commonMain/kotlin/com/b1nd/dodam/member/repository/MemberRepositoryImpl.kt @@ -43,4 +43,12 @@ internal class MemberRepositoryImpl( .asResult() .flowOn(dispatcher) } + + override suspend fun editMemberInfo(name: String, email: String, phone: String, profileImage: String?): Flow> { + return flow { + emit(network.editMemberInfo(name, email, phone, profileImage)) + } + .asResult() + .flowOn(dispatcher) + } } diff --git a/data/upload/.gitignore b/data/upload/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/data/upload/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/upload/build.gradle.kts b/data/upload/build.gradle.kts new file mode 100644 index 00000000..b7a21aab --- /dev/null +++ b/data/upload/build.gradle.kts @@ -0,0 +1,29 @@ +import com.b1nd.dodam.dsl.android +import com.b1nd.dodam.dsl.kotlin +import com.b1nd.dodam.dsl.setIOS + +plugins { + alias(libs.plugins.dodam.multiplatform) + alias(libs.plugins.dodam.multiplatform.kotlin) + alias(libs.plugins.dodam.multiplatform.koin) +} + +kotlin { + setIOS("data.upload") + + sourceSets.commonMain.dependencies { + api(projects.data.core) + implementation(projects.common) + implementation(projects.network.upload) + + } +} + + +android { + namespace = "com.b1nd.dodam.data.upload" + + defaultConfig { + consumerProguardFiles("consumer-rules.pro") + } +} \ No newline at end of file diff --git a/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/UploadRepository.kt b/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/UploadRepository.kt new file mode 100644 index 00000000..c28b252a --- /dev/null +++ b/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/UploadRepository.kt @@ -0,0 +1,10 @@ +package com.b1nd.dodam.data.upload + +import com.b1nd.dodam.common.result.Result +import com.b1nd.dodam.data.upload.model.UploadModel +import kotlinx.coroutines.flow.Flow + +interface UploadRepository { + + suspend fun upload(fileName: String, fileMimeType: String, byteArray: ByteArray): Flow> +} diff --git a/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/di/RepositoryModule.kt b/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/di/RepositoryModule.kt new file mode 100644 index 00000000..dbf356a0 --- /dev/null +++ b/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/di/RepositoryModule.kt @@ -0,0 +1,16 @@ +package com.b1nd.dodam.data.upload.di + +import com.b1nd.dodam.common.DispatcherType +import com.b1nd.dodam.data.upload.UploadRepository +import com.b1nd.dodam.data.upload.repository.UploadRepositoryImpl +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val uploadRepositoryModule = module { + single { + UploadRepositoryImpl( + network = get(), + dispatcher = get(named(DispatcherType.IO)), + ) + } +} diff --git a/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/model/UploadModel.kt b/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/model/UploadModel.kt new file mode 100644 index 00000000..ffac0ef7 --- /dev/null +++ b/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/model/UploadModel.kt @@ -0,0 +1,9 @@ +package com.b1nd.dodam.data.upload.model + +data class UploadModel( + val profileImage: String, +) + +fun String.toModel() = UploadModel( + profileImage = this, +) diff --git a/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/repository/UploadRepositoryImpl.kt b/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/repository/UploadRepositoryImpl.kt new file mode 100644 index 00000000..77aeede3 --- /dev/null +++ b/data/upload/src/commonMain/kotlin/com/b1nd/dodam/data/upload/repository/UploadRepositoryImpl.kt @@ -0,0 +1,33 @@ +package com.b1nd.dodam.data.upload.repository + +import com.b1nd.dodam.common.Dispatcher +import com.b1nd.dodam.common.DispatcherType +import com.b1nd.dodam.common.result.Result +import com.b1nd.dodam.common.result.asResult +import com.b1nd.dodam.data.upload.UploadRepository +import com.b1nd.dodam.data.upload.model.UploadModel +import com.b1nd.dodam.data.upload.model.toModel +import com.b1nd.dodam.network.upload.datasource.UploadDataSource +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +class UploadRepositoryImpl( + private val network: UploadDataSource, + @Dispatcher(DispatcherType.IO) private val dispatcher: CoroutineDispatcher, +) : UploadRepository { + override suspend fun upload(fileName: String, fileMimeType: String, byteArray: ByteArray): Flow> { + return flow { + emit( + network.upload( + fileName = fileName, + fileMimeType = fileMimeType, + byteArray = byteArray, + ).toModel(), + ) + } + .asResult() + .flowOn(dispatcher) + } +} diff --git a/dodam-student/build.gradle.kts b/dodam-student/build.gradle.kts index 0e1eeaab..65b10d1e 100644 --- a/dodam-student/build.gradle.kts +++ b/dodam-student/build.gradle.kts @@ -88,4 +88,7 @@ dependencies { implementation(projects.network.login) implementation(projects.network.bundleidInfo) implementation(projects.data.bundleidInfo) + implementation(projects.feature.editMemberInfo) + implementation(projects.network.upload) + implementation(projects.data.upload) } diff --git a/dodam-student/src/main/kotlin/com/b1nd/dodam/student/DodamApp.kt b/dodam-student/src/main/kotlin/com/b1nd/dodam/student/DodamApp.kt index 36e8f0ec..2a7b0428 100644 --- a/dodam-student/src/main/kotlin/com/b1nd/dodam/student/DodamApp.kt +++ b/dodam-student/src/main/kotlin/com/b1nd/dodam/student/DodamApp.kt @@ -36,6 +36,8 @@ import com.b1nd.dodam.bus.navigation.navigateToBus import com.b1nd.dodam.dds.component.DodamErrorToast import com.b1nd.dodam.dds.component.DodamSuccessToast import com.b1nd.dodam.dds.component.DodamWarningToast +import com.b1nd.dodam.editmemberinfo.navigation.editMemberInfoScreen +import com.b1nd.dodam.editmemberinfo.navigation.navigationToEditMemberInfo import com.b1nd.dodam.login.navigation.loginScreen import com.b1nd.dodam.login.navigation.navigationToLogin import com.b1nd.dodam.onboarding.navigation.ONBOARDING_ROUTE @@ -239,6 +241,14 @@ fun DodamApp( versionInfo = "3.2.0", popBackStack = navController::popBackStack, logout = logout, + navigationToEditMemberInfo = { profileImage, name, email, phone -> + navController.navigationToEditMemberInfo( + profileImage = profileImage, + name = name, + email = email, + phone = phone, + ) + }, ) askWakeupSongScreen( popBackStack = navController::popBackStack, @@ -250,6 +260,9 @@ fun DodamApp( pointScreen( popBackStack = navController::popBackStack, ) + editMemberInfoScreen( + popBackStack = navController::popBackStack, + ) } } } diff --git a/dodam-student/src/main/kotlin/com/b1nd/dodam/student/util/DodamApplication.kt b/dodam-student/src/main/kotlin/com/b1nd/dodam/student/util/DodamApplication.kt index 689ac8f2..4e8202ba 100644 --- a/dodam-student/src/main/kotlin/com/b1nd/dodam/student/util/DodamApplication.kt +++ b/dodam-student/src/main/kotlin/com/b1nd/dodam/student/util/DodamApplication.kt @@ -17,7 +17,9 @@ import com.b1nd.dodam.data.nightstudy.di.nightStudyRepositoryModule import com.b1nd.dodam.data.outing.di.outingRepositoryModule import com.b1nd.dodam.data.point.di.pointRepositoryModule import com.b1nd.dodam.data.schedule.di.scheduleRepositoryModule +import com.b1nd.dodam.data.upload.di.uploadRepositoryModule import com.b1nd.dodam.datastore.di.dataStoreModule +import com.b1nd.dodam.editmemberinfo.di.editMemberInfoViewModelModule import com.b1nd.dodam.keystore.keystoreManagerModule import com.b1nd.dodam.login.di.loginViewModelModule import com.b1nd.dodam.member.di.memberDataSourceModule @@ -30,6 +32,7 @@ import com.b1nd.dodam.network.nightstudy.di.nightStudyDataSourceModule import com.b1nd.dodam.network.outing.di.outingDataSourceModule import com.b1nd.dodam.network.point.di.pointDataSourceModule import com.b1nd.dodam.network.schedule.di.scheduleDatasourceModule +import com.b1nd.dodam.network.upload.di.uploadDatasourceModule import com.b1nd.dodam.nightstudy.di.nightStudyViewModelModule import com.b1nd.dodam.outing.di.outingViewModelModule import com.b1nd.dodam.register.di.registerDataSourceModule @@ -96,6 +99,9 @@ class DodamApplication : Application() { outingViewModelModule, bundleIdInfoRepositoryModule, bundleIdInfoDataSourceModule, + editMemberInfoViewModelModule, + uploadDatasourceModule, + uploadRepositoryModule, ) + mainViewModelModules, ) } diff --git a/dodam-teacher-android/build.gradle.kts b/dodam-teacher-android/build.gradle.kts index f07db715..af7ecf7f 100644 --- a/dodam-teacher-android/build.gradle.kts +++ b/dodam-teacher-android/build.gradle.kts @@ -56,6 +56,9 @@ kotlin { implementation(projects.network.point) implementation(projects.network.bundleidInfo) implementation(projects.data.bundleidInfo) + implementation(projects.feature.editMemberInfo) + implementation(projects.network.upload) + implementation(projects.data.upload) } androidMain.dependencies { diff --git a/dodam-teacher-android/src/commonMain/kotlin/com/b1nd/dodam/teacher/DodamTeacherApp.kt b/dodam-teacher-android/src/commonMain/kotlin/com/b1nd/dodam/teacher/DodamTeacherApp.kt index 31f9d81b..d8f5d2a9 100644 --- a/dodam-teacher-android/src/commonMain/kotlin/com/b1nd/dodam/teacher/DodamTeacherApp.kt +++ b/dodam-teacher-android/src/commonMain/kotlin/com/b1nd/dodam/teacher/DodamTeacherApp.kt @@ -50,6 +50,8 @@ import com.b1nd.dodam.designsystem.component.DodamDialog import com.b1nd.dodam.designsystem.component.DodamNavigationBar import com.b1nd.dodam.designsystem.component.DodamNavigationBarItem import com.b1nd.dodam.designsystem.foundation.DodamIcons +import com.b1nd.dodam.editmemberinfo.navigation.editMemberInfoScreen +import com.b1nd.dodam.editmemberinfo.navigation.navigationToEditMemberInfo import com.b1nd.dodam.home.navigation.HOME_ROUTE import com.b1nd.dodam.home.navigation.homeScreen import com.b1nd.dodam.home.navigation.navigateToHome @@ -255,7 +257,19 @@ fun DodamTeacherApp(exit: () -> Unit, viewModel: DodamTeacherAppViewModel = koin settingScreen( popBackStack = navHostController::popBackStack, logout = exit, - versionInfo = getPlatformName(), + versionInfo = VERSION_INFO, + navigationToEditMemberInfo = { profileImage, name, email, phone -> + navHostController.navigationToEditMemberInfo( + profileImage = profileImage, + name = name, + email = email, + phone = phone, + ) + }, + ) + + editMemberInfoScreen( + popBackStack = navHostController::popBackStack, ) } diff --git a/dodam-teacher-android/src/commonMain/kotlin/com/b1nd/dodam/teacher/Koin.kt b/dodam-teacher-android/src/commonMain/kotlin/com/b1nd/dodam/teacher/Koin.kt index 807d9ed2..0d096514 100644 --- a/dodam-teacher-android/src/commonMain/kotlin/com/b1nd/dodam/teacher/Koin.kt +++ b/dodam-teacher-android/src/commonMain/kotlin/com/b1nd/dodam/teacher/Koin.kt @@ -13,7 +13,9 @@ import com.b1nd.dodam.data.nightstudy.di.nightStudyRepositoryModule import com.b1nd.dodam.data.outing.di.outingRepositoryModule import com.b1nd.dodam.data.point.di.pointRepositoryModule import com.b1nd.dodam.data.schedule.di.scheduleRepositoryModule +import com.b1nd.dodam.data.upload.di.uploadRepositoryModule import com.b1nd.dodam.datastore.di.dataStoreModule +import com.b1nd.dodam.editmemberinfo.di.editMemberInfoViewModelModule import com.b1nd.dodam.home.di.homeViewModelModule import com.b1nd.dodam.login.di.loginViewModelModule import com.b1nd.dodam.meal.di.mealViewModelModule @@ -27,6 +29,7 @@ import com.b1nd.dodam.network.nightstudy.di.nightStudyDataSourceModule import com.b1nd.dodam.network.outing.di.outingDataSourceModule import com.b1nd.dodam.network.point.di.pointDataSourceModule import com.b1nd.dodam.network.schedule.di.scheduleDatasourceModule +import com.b1nd.dodam.network.upload.di.uploadDatasourceModule import com.b1nd.dodam.nightstudy.di.nightStudyViewModelModule import com.b1nd.dodam.outing.di.outingViewModelModule import com.b1nd.dodam.point.di.pointViewModelModule @@ -78,6 +81,9 @@ fun initKoin(block: KoinApplication.() -> Unit = {}) { settingViewModelModule, bundleIdInfoRepositoryModule, bundleIdInfoDataSourceModule, + editMemberInfoViewModelModule, + uploadDatasourceModule, + uploadRepositoryModule, ) block() } diff --git a/feature-student/all/src/main/java/com/b1nd/dodam/member/AllScreen.kt b/feature-student/all/src/main/java/com/b1nd/dodam/member/AllScreen.kt index 03aedc5e..109851ff 100644 --- a/feature-student/all/src/main/java/com/b1nd/dodam/member/AllScreen.kt +++ b/feature-student/all/src/main/java/com/b1nd/dodam/member/AllScreen.kt @@ -24,25 +24,26 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold 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.remember 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.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage import com.b1nd.dodam.designsystem.DodamTheme import com.b1nd.dodam.designsystem.animation.rememberBounceIndication import com.b1nd.dodam.designsystem.component.ActionIcon +import com.b1nd.dodam.designsystem.component.AvatarSize import com.b1nd.dodam.designsystem.component.DividerType +import com.b1nd.dodam.designsystem.component.DodamAvatar import com.b1nd.dodam.designsystem.component.DodamDefaultTopAppBar import com.b1nd.dodam.designsystem.component.DodamDivider import com.b1nd.dodam.designsystem.foundation.DodamIcons +import com.b1nd.dodam.ui.component.modifier.`if` import com.b1nd.dodam.ui.effect.shimmerEffect import com.b1nd.dodam.ui.icons.BarChart import com.b1nd.dodam.ui.icons.ColoredBus @@ -64,6 +65,10 @@ fun AllScreen( navigateToAddWakeUpSong: () -> Unit, ) { val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(key1 = true) { + viewModel.getMyInfo() + } Scaffold( modifier = Modifier .fillMaxSize(), @@ -130,36 +135,21 @@ fun AllScreen( ) { uiState.memberInfo?.let { myInfo -> Box { - if (!myInfo.profileImage.isNullOrEmpty()) { - AsyncImage( - model = myInfo.profileImage, - contentDescription = "profile", - modifier = Modifier - .clip(shape = CircleShape) - .size(64.dp), - contentScale = ContentScale.Crop, - ) - } else { - Box( - modifier = Modifier - .clip(shape = CircleShape) - .size(64.dp) - .border( + val borderColor = DodamTheme.colors.lineAlternative + DodamAvatar( + avatarSize = AvatarSize.ExtraLarge, + contentDescription = "프로필 이미지", + model = myInfo.profileImage, + modifier = Modifier + .`if`(myInfo.profileImage.isNullOrEmpty()) { + border( width = 1.dp, - color = DodamTheme.colors.lineAlternative, + color = borderColor, shape = CircleShape, - ), - contentAlignment = Alignment.Center, - ) { - Image( - imageVector = DodamIcons.Person.value, - contentDescription = "profile", - modifier = Modifier.size(40.dp), - contentScale = ContentScale.Crop, - colorFilter = ColorFilter.tint(DodamTheme.colors.fillAlternative), - ) - } - } + ) + }, + contentScale = ContentScale.Crop, + ) } Spacer(modifier = Modifier.width(16.dp)) val classInfo = myInfo.student diff --git a/feature-student/all/src/main/java/com/b1nd/dodam/member/AllViewModel.kt b/feature-student/all/src/main/java/com/b1nd/dodam/member/AllViewModel.kt index 6d471c04..a7d14d40 100644 --- a/feature-student/all/src/main/java/com/b1nd/dodam/member/AllViewModel.kt +++ b/feature-student/all/src/main/java/com/b1nd/dodam/member/AllViewModel.kt @@ -18,7 +18,7 @@ class AllViewModel : ViewModel(), KoinComponent { private val _uiState = MutableStateFlow(AllUiState()) val uiState = _uiState.asStateFlow() - init { + fun getMyInfo() { viewModelScope.launch { memberRepository.getMyInfo().collect { result -> _uiState.update { uiState -> diff --git a/feature-teacher/all/src/commonMain/kotlin/com/b1nd/dodam/all/AllScreen.kt b/feature-teacher/all/src/commonMain/kotlin/com/b1nd/dodam/all/AllScreen.kt index 5788ac6e..9b2446a2 100644 --- a/feature-teacher/all/src/commonMain/kotlin/com/b1nd/dodam/all/AllScreen.kt +++ b/feature-teacher/all/src/commonMain/kotlin/com/b1nd/dodam/all/AllScreen.kt @@ -2,6 +2,7 @@ package com.b1nd.dodam.all import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -25,23 +27,23 @@ import androidx.compose.runtime.getValue 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.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage import com.b1nd.dodam.designsystem.DodamTheme import com.b1nd.dodam.designsystem.animation.rememberBounceIndication import com.b1nd.dodam.designsystem.component.ActionIcon +import com.b1nd.dodam.designsystem.component.AvatarSize import com.b1nd.dodam.designsystem.component.DividerType +import com.b1nd.dodam.designsystem.component.DodamAvatar import com.b1nd.dodam.designsystem.component.DodamDefaultTopAppBar import com.b1nd.dodam.designsystem.component.DodamDivider import com.b1nd.dodam.designsystem.foundation.DodamIcons +import com.b1nd.dodam.ui.component.modifier.`if` import com.b1nd.dodam.ui.effect.shimmerEffect import com.b1nd.dodam.ui.icons.ColoredPencil import com.b1nd.dodam.ui.icons.ColoredTrophy -import com.b1nd.dodam.ui.icons.DefaultProfile import com.b1nd.dodam.ui.icons.Tent import kotlinx.collections.immutable.persistentListOf import org.koin.compose.viewmodel.koinViewModel @@ -100,7 +102,7 @@ internal fun AllScreen( if (uiState.isLoading) { Box( modifier = Modifier - .size(70.dp) + .size(64.dp) .background( brush = shimmerEffect(), shape = DodamTheme.shapes.extraSmall, @@ -117,25 +119,21 @@ internal fun AllScreen( ), ) } else { - if (uiState.memberInfo.profileImage.isNullOrEmpty()) { - Image( - bitmap = DefaultProfile, - contentDescription = "profile", - modifier = Modifier - .size(70.dp) - .clip(DodamTheme.shapes.medium), - contentScale = ContentScale.Crop, - ) - } else { - AsyncImage( - modifier = Modifier - .size(70.dp) - .clip(DodamTheme.shapes.medium), - model = uiState.memberInfo.profileImage, - contentDescription = "profile", - contentScale = ContentScale.Crop, - ) - } + val borderColor = DodamTheme.colors.lineAlternative + DodamAvatar( + avatarSize = AvatarSize.ExtraLarge, + contentDescription = "프로필 이미지", + model = uiState.memberInfo.profileImage, + modifier = Modifier + .`if`(uiState.memberInfo.profileImage.isNullOrEmpty()) { + border( + width = 1.dp, + color = borderColor, + shape = CircleShape, + ) + }, + contentScale = ContentScale.Crop, + ) Spacer(modifier = Modifier.width(16.dp)) Text( text = "환영합니다, " + uiState.memberInfo.name + "님", diff --git a/feature/edit-member-info/.gitignore b/feature/edit-member-info/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/edit-member-info/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/edit-member-info/build.gradle.kts b/feature/edit-member-info/build.gradle.kts new file mode 100644 index 00000000..195e12bf --- /dev/null +++ b/feature/edit-member-info/build.gradle.kts @@ -0,0 +1,35 @@ +import com.b1nd.dodam.dsl.android +import com.b1nd.dodam.dsl.kotlin +import com.b1nd.dodam.dsl.setIOS + +plugins { + alias(libs.plugins.dodam.multiplatform.feature) + alias(libs.plugins.dodam.multiplatform.koin) + alias(libs.plugins.dodam.multiplatform.coil) +} + +kotlin { + + setIOS( + name = "edit_member_info", + bundleId = "com.b1nd.dodam.edit_member_info" + ) + + sourceSets.commonMain.dependencies { + implementation(libs.dodam.design.system.cmm) + implementation(projects.data.member) + implementation(projects.common) + implementation(projects.ui) + implementation(projects.logging) + implementation(projects.data.upload) + implementation("com.mohamedrejeb.calf:calf-file-picker:0.5.5") + implementation("com.mohamedrejeb.calf:calf-permissions:0.5.5") + implementation("net.thauvin.erik.urlencoder:urlencoder-lib:1.6.0") + } +} +android { + namespace = "com.b1nd.dodam.edit_member_info" + defaultConfig { + consumerProguardFiles("consumer-rules.pro") + } +} \ No newline at end of file diff --git a/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/EditMemberInfoScreen.kt b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/EditMemberInfoScreen.kt new file mode 100644 index 00000000..c1c47a17 --- /dev/null +++ b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/EditMemberInfoScreen.kt @@ -0,0 +1,231 @@ +package com.b1nd.dodam.editmemberinfo + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Scaffold +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.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.b1nd.dodam.designsystem.DodamTheme +import com.b1nd.dodam.designsystem.component.AvatarSize +import com.b1nd.dodam.designsystem.component.DodamAvatar +import com.b1nd.dodam.designsystem.component.DodamButton +import com.b1nd.dodam.designsystem.component.DodamTextField +import com.b1nd.dodam.designsystem.component.DodamTopAppBar +import com.b1nd.dodam.designsystem.foundation.DodamIcons +import com.b1nd.dodam.editmemberinfo.model.EditMemberInfoSideEffect +import com.b1nd.dodam.editmemberinfo.viewmodel.EditMemberInfoViewModel +import com.b1nd.dodam.ui.component.modifier.`if` +import com.b1nd.dodam.ui.util.addFocusCleaner +import com.mohamedrejeb.calf.core.LocalPlatformContext +import com.mohamedrejeb.calf.io.getName +import com.mohamedrejeb.calf.io.getPath +import com.mohamedrejeb.calf.io.readByteArray +import com.mohamedrejeb.calf.picker.FilePickerFileType +import com.mohamedrejeb.calf.picker.FilePickerSelectionMode +import com.mohamedrejeb.calf.picker.rememberFilePickerLauncher +import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel + +@Composable +internal fun EditMemberInfoScreen( + viewModel: EditMemberInfoViewModel = koinViewModel(), + profileImage: String, + popBackStack: () -> Unit, + name: String, + email: String, + phone: String, +) { + var name by remember { mutableStateOf(name) } + var email by remember { mutableStateOf(email) } + var phone by remember { mutableStateOf(phone) } + val focusManager = LocalFocusManager.current + + val uiState by viewModel.uiState.collectAsState() + + val scope = rememberCoroutineScope() + val context = LocalPlatformContext.current + + var byteArray by remember { mutableStateOf(ByteArray(0)) } + var platformSpecificFilePath by remember { mutableStateOf("") } + var fileName by remember { mutableStateOf("") } + + val pickerLauncher = rememberFilePickerLauncher( + type = FilePickerFileType.Image, + selectionMode = FilePickerSelectionMode.Multiple, + onResult = { files -> + scope.launch { + files.firstOrNull()?.let { file -> + byteArray = file.readByteArray(context) + platformSpecificFilePath = file.getPath(context) ?: "" + fileName = file.getName(context) ?: "" + viewModel.setProfile(profileImage) + viewModel.fileUpload( + fileByteArray = byteArray, + fileMimeType = fileName.split(".")[1], + fileName = fileName.split(".")[0], + ) + } + } + }, + ) + + LaunchedEffect(true) { + platformSpecificFilePath = profileImage + viewModel.setProfile( + if (profileImage == "default") null else profileImage, + ) + viewModel.sideEffect.collect { + when (it) { + EditMemberInfoSideEffect.SuccessEditMemberInfo -> { + popBackStack() + } + } + } + } + + Scaffold( + modifier = Modifier.addFocusCleaner(focusManager), + topBar = { + DodamTopAppBar( + modifier = Modifier.statusBarsPadding(), + title = "정보수정", + onBackClick = popBackStack, + ) + }, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(DodamTheme.colors.backgroundNeutral) + .padding(it) + .padding(horizontal = 16.dp) + .padding(vertical = 12.dp), + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val borderColor = DodamTheme.colors.lineAlternative + Box { + DodamAvatar( + avatarSize = AvatarSize.XXL, + contentDescription = "프로필 이미지", + model = uiState.image, + modifier = Modifier + .`if`(uiState.image.isNullOrEmpty()) { + border( + width = 1.dp, + color = borderColor, + shape = CircleShape, + ) + }, + contentScale = ContentScale.Crop, + ) + Box( + modifier = Modifier + .size(32.dp) + .align(Alignment.BottomEnd) + .background( + color = DodamTheme.colors.primaryNormal, + shape = CircleShape, + ) + .clickable { + pickerLauncher.launch() + }, + ) { + Image( + imageVector = DodamIcons.Plus.value, + contentDescription = "plus", + colorFilter = ColorFilter.tint(DodamTheme.colors.staticWhite), + modifier = Modifier + .size(16.dp) + .align(Alignment.Center), + ) + } + } + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp), + ) { + DodamTextField( + value = name, + onValueChange = { + name = it + }, + label = "이름", + onClickRemoveRequest = { + name = "" + }, + ) + Spacer(Modifier.height(24.dp)) + DodamTextField( + value = email, + onValueChange = { + email = it + }, + label = "이메일", + onClickRemoveRequest = { + email = "" + }, + modifier = Modifier, + ) + Spacer(Modifier.height(24.dp)) + DodamTextField( + value = phone, + onValueChange = { + phone = it + }, + label = "전화번호", + onClickRemoveRequest = { + phone = "" + }, + modifier = Modifier, + ) + Spacer(Modifier.height(24.dp)) + } + } + DodamButton( + onClick = { + if (!uiState.isLoading) { + viewModel.editMember( + email = email, + name = name, + phone = phone, + profileImage = uiState.image, + ) + } + }, + text = "수정 완료", + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth(), + ) + } + } +} diff --git a/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/di/ViewModelModule.kt b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/di/ViewModelModule.kt new file mode 100644 index 00000000..21f62ac6 --- /dev/null +++ b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/di/ViewModelModule.kt @@ -0,0 +1,9 @@ +package com.b1nd.dodam.editmemberinfo.di + +import com.b1nd.dodam.editmemberinfo.viewmodel.EditMemberInfoViewModel +import org.koin.compose.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val editMemberInfoViewModelModule = module { + viewModel { EditMemberInfoViewModel() } +} diff --git a/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/model/EditMemberInfoSideEffect.kt b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/model/EditMemberInfoSideEffect.kt new file mode 100644 index 00000000..6fe29972 --- /dev/null +++ b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/model/EditMemberInfoSideEffect.kt @@ -0,0 +1,5 @@ +package com.b1nd.dodam.editmemberinfo.model + +sealed interface EditMemberInfoSideEffect { + data object SuccessEditMemberInfo : EditMemberInfoSideEffect +} diff --git a/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/model/ProfileModel.kt b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/model/ProfileModel.kt new file mode 100644 index 00000000..90b53800 --- /dev/null +++ b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/model/ProfileModel.kt @@ -0,0 +1,9 @@ +package com.b1nd.dodam.editmemberinfo.model + +data class ProfileModel( + val name: String = "", + val email: String = "", + val phone: String = "", + val image: String? = null, + val isLoading: Boolean = false, +) diff --git a/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/navigation/EditMemberInfoNavigation.kt b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/navigation/EditMemberInfoNavigation.kt new file mode 100644 index 00000000..4dbc5fb5 --- /dev/null +++ b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/navigation/EditMemberInfoNavigation.kt @@ -0,0 +1,61 @@ +package com.b1nd.dodam.editmemberinfo.navigation + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.b1nd.dodam.editmemberinfo.EditMemberInfoScreen +import net.thauvin.erik.urlencoder.UrlEncoderUtil + +const val EDIT_MEMBER_INFO_ROUTE = "edit_member_info" + +fun NavController.navigationToEditMemberInfo( + profileImage: String?, + name: String, + phone: String, + email: String, + navOptions: NavOptions? = androidx.navigation.navOptions { + launchSingleTop = true + }, +) { + val image = if (profileImage.isNullOrEmpty()) { + "default" + } else { + UrlEncoderUtil.encode(profileImage) + } + navigate("$EDIT_MEMBER_INFO_ROUTE/$image/$name/$email/$phone", navOptions) +} + +@ExperimentalMaterial3Api +fun NavGraphBuilder.editMemberInfoScreen(popBackStack: () -> Unit) { + composable( + route = "$EDIT_MEMBER_INFO_ROUTE/{image}/{name}/{email}/{phone}", + arguments = listOf( + navArgument("image") { type = NavType.StringType }, + navArgument("name") { type = NavType.StringType }, + navArgument("email") { type = NavType.StringType }, + navArgument("phone") { type = NavType.StringType }, + ), + enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) }, + exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Up) }, + popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Down) }, + popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Down) }, + ) { + val encodedImage = it.arguments?.getString("image") ?: "default" + val profileImage = UrlEncoderUtil.decode(encodedImage) + val name = it.arguments?.getString("name") ?: "" + val email = it.arguments?.getString("email") ?: "" + val phone = it.arguments?.getString("phone") ?: "" + EditMemberInfoScreen( + profileImage = profileImage, + popBackStack = popBackStack, + name = name, + email = email, + phone = phone, + ) + } +} diff --git a/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/viewmodel/EditMemberInfoViewModel.kt b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/viewmodel/EditMemberInfoViewModel.kt new file mode 100644 index 00000000..dd571676 --- /dev/null +++ b/feature/edit-member-info/src/commonMain/kotlin/com/b1nd/dodam/editmemberinfo/viewmodel/EditMemberInfoViewModel.kt @@ -0,0 +1,90 @@ +package com.b1nd.dodam.editmemberinfo.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.b1nd.dodam.common.result.Result +import com.b1nd.dodam.data.upload.UploadRepository +import com.b1nd.dodam.editmemberinfo.model.EditMemberInfoSideEffect +import com.b1nd.dodam.editmemberinfo.model.ProfileModel +import com.b1nd.dodam.member.MemberRepository +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class EditMemberInfoViewModel : ViewModel(), KoinComponent { + private val memberRepository: MemberRepository by inject() + private val uploadRepository: UploadRepository by inject() + + private val _uiState = MutableStateFlow(ProfileModel()) + val uiState = _uiState.asStateFlow() + + private val _sideEffect = MutableSharedFlow() + val sideEffect = _sideEffect.asSharedFlow() + + fun setProfile(profileImage: String?) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy( + image = profileImage, + ) + } + } + + fun editMember(email: String, name: String, phone: String, profileImage: String?) { + viewModelScope.launch { + memberRepository.editMemberInfo( + name = name, + email = email, + phone = phone, + profileImage = profileImage, + ).collect { + when (it) { + is Result.Success -> { + _sideEffect.emit(EditMemberInfoSideEffect.SuccessEditMemberInfo) + _uiState.value = _uiState.value.copy(isLoading = false) + } + + is Result.Error -> { + it.error.printStackTrace() + _uiState.value = _uiState.value.copy(isLoading = false) + } + + is Result.Loading -> { + _uiState.value = _uiState.value.copy(isLoading = true) + } + } + } + } + } + + fun fileUpload(fileByteArray: ByteArray, fileMimeType: String, fileName: String) { + viewModelScope.launch { + uploadRepository.upload( + fileName = fileName, + fileMimeType = fileMimeType, + byteArray = fileByteArray, + ).collect { + when (it) { + is Result.Error -> { + it.error.printStackTrace() + } + + is Result.Success -> { + _uiState.update { ui -> + println(it.data) + ui.copy( + image = it.data.profileImage, + ) + } + } + + is Result.Loading -> {} + } + } + } + } +} diff --git a/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/SettingScreen.kt b/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/SettingScreen.kt index 5a61e9e2..a39243e0 100644 --- a/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/SettingScreen.kt +++ b/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/SettingScreen.kt @@ -2,6 +2,7 @@ package com.b1nd.dodam.setting import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold @@ -29,7 +31,6 @@ import androidx.compose.runtime.remember 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.ColorFilter import androidx.compose.ui.layout.ContentScale @@ -37,30 +38,34 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import coil3.compose.AsyncImage import com.b1nd.dodam.designsystem.DodamTheme import com.b1nd.dodam.designsystem.animation.rememberBounceIndication +import com.b1nd.dodam.designsystem.component.AvatarSize import com.b1nd.dodam.designsystem.component.ButtonRole import com.b1nd.dodam.designsystem.component.DividerType +import com.b1nd.dodam.designsystem.component.DodamAvatar import com.b1nd.dodam.designsystem.component.DodamButtonDialog -import com.b1nd.dodam.designsystem.component.DodamDialog import com.b1nd.dodam.designsystem.component.DodamDivider import com.b1nd.dodam.designsystem.component.DodamTopAppBar import com.b1nd.dodam.designsystem.foundation.DodamIcons import com.b1nd.dodam.ui.component.modifier.`if` import com.b1nd.dodam.ui.effect.shimmerEffect -import com.b1nd.dodam.ui.icons.DefaultProfile import org.koin.compose.viewmodel.koinViewModel import org.koin.core.annotation.KoinExperimentalAPI @OptIn(KoinExperimentalAPI::class) @ExperimentalMaterial3Api @Composable -internal fun SettingScreen(viewModel: SettingViewModel = koinViewModel(), versionInfo: String = "3.2.0", popBackStack: () -> Unit, logout: () -> Unit) { +internal fun SettingScreen( + viewModel: SettingViewModel = koinViewModel(), + versionInfo: String = "3.2.0", + popBackStack: () -> Unit, + logout: () -> Unit, + navigationToEditMemberInfo: (profileImage: String?, name: String, email: String, phone: String) -> Unit, +) { val uiState by viewModel.uiState.collectAsState() var showLogoutDialog by remember { mutableStateOf(false) } - var showDialog by remember { mutableStateOf(false) } var showEasterEggDialog by remember { mutableStateOf(false) } var showDeactivationDialog by remember { mutableStateOf(false) } @@ -68,6 +73,10 @@ internal fun SettingScreen(viewModel: SettingViewModel = koinViewModel(), versio var count by remember { mutableIntStateOf(0) } + LaunchedEffect(key1 = true) { + viewModel.getMyInfo() + } + LaunchedEffect(count) { if (count == 10) { showEasterEggDialog = true @@ -110,19 +119,6 @@ internal fun SettingScreen(viewModel: SettingViewModel = koinViewModel(), versio } } - if (showDialog) { - Dialog( - onDismissRequest = { showDialog = false }, - ) { - DodamDialog( - confirmButton = { showDialog = false }, - text = "확인", - title = "아직 준비 중인 기능이에요!", - body = "정보를 수정하시려면 도담도담 웹사이트를 이용해 주세요.", - ) - } - } - if (showDeactivationDialog) { Dialog( onDismissRequest = { showDeactivationDialog = false }, @@ -164,9 +160,17 @@ internal fun SettingScreen(viewModel: SettingViewModel = koinViewModel(), versio .padding(bottom = 8.dp) .fillMaxWidth() .clickable( + enabled = uiState.name.isNotEmpty(), interactionSource = remember { MutableInteractionSource() }, indication = rememberBounceIndication(), - onClick = { showDialog = true }, + onClick = { + navigationToEditMemberInfo( + uiState.profile, + uiState.name, + uiState.email, + uiState.phone, + ) + }, ), ) { if (uiState.isLoading) { @@ -176,10 +180,10 @@ internal fun SettingScreen(viewModel: SettingViewModel = koinViewModel(), versio ) { Box( modifier = Modifier - .size(56.dp) + .size(64.dp) .background( brush = shimmerEffect(), - shape = DodamTheme.shapes.medium, + shape = CircleShape, ), ) @@ -208,25 +212,21 @@ internal fun SettingScreen(viewModel: SettingViewModel = koinViewModel(), versio verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), ) { - if (uiState.profile != null && uiState.profile != "") { - AsyncImage( - modifier = Modifier - .clip(DodamTheme.shapes.medium) - .size(56.dp), - model = uiState.profile, - contentDescription = null, - contentScale = ContentScale.Crop, - ) - } else { - Image( - modifier = Modifier - .clip(DodamTheme.shapes.medium) - .size(56.dp), - bitmap = DefaultProfile, - contentDescription = "프로필 이미지", - contentScale = ContentScale.Crop, - ) - } + val borderColor = DodamTheme.colors.lineAlternative + DodamAvatar( + avatarSize = AvatarSize.ExtraLarge, + contentDescription = "프로필 이미지", + model = if (uiState.profile.isNullOrEmpty()) null else uiState.profile, + modifier = Modifier + .`if`(uiState.profile.isNullOrEmpty()) { + border( + width = 1.dp, + color = borderColor, + shape = CircleShape, + ) + }, + contentScale = ContentScale.Crop, + ) Column { Text( diff --git a/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/SettingViewModel.kt b/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/SettingViewModel.kt index 565c7ce5..c02e635f 100644 --- a/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/SettingViewModel.kt +++ b/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/SettingViewModel.kt @@ -21,7 +21,7 @@ class SettingViewModel : ViewModel(), KoinComponent { private val _uiState = MutableStateFlow(SettingUiState()) val uiState = _uiState.asStateFlow() - init { + fun getMyInfo() { viewModelScope.launch { memberRepository.getMyInfo().collect { result -> when (result) { @@ -31,6 +31,8 @@ class SettingViewModel : ViewModel(), KoinComponent { isLoading = false, name = result.data.name, profile = result.data.profileImage, + email = result.data.email, + phone = result.data.phone, ) } } diff --git a/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/model/SettingUiState.kt b/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/model/SettingUiState.kt index 39f1e222..c21eef83 100644 --- a/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/model/SettingUiState.kt +++ b/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/model/SettingUiState.kt @@ -4,4 +4,6 @@ data class SettingUiState( val isLoading: Boolean = false, val profile: String? = null, val name: String = "", + val email: String = "", + val phone: String = "", ) diff --git a/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/navigation/SettingNavigation.kt b/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/navigation/SettingNavigation.kt index bb964ae3..7ae59a71 100644 --- a/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/navigation/SettingNavigation.kt +++ b/feature/setting/src/commonMain/kotlin/com/b1nd/dodam/setting/navigation/SettingNavigation.kt @@ -18,7 +18,12 @@ fun NavController.navigateToSetting( ) = navigate(SETTING_ROUTE, navOptions) @ExperimentalMaterial3Api -fun NavGraphBuilder.settingScreen(versionInfo: String, popBackStack: () -> Unit, logout: () -> Unit) { +fun NavGraphBuilder.settingScreen( + versionInfo: String, + popBackStack: () -> Unit, + logout: () -> Unit, + navigationToEditMemberInfo: (profileImage: String?, name: String, email: String, phone: String) -> Unit, +) { composable( route = SETTING_ROUTE, enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) }, @@ -30,6 +35,7 @@ fun NavGraphBuilder.settingScreen(versionInfo: String, popBackStack: () -> Unit, versionInfo = versionInfo, popBackStack = popBackStack, logout = logout, + navigationToEditMemberInfo = navigationToEditMemberInfo, ) } } diff --git a/network/core/src/commonMain/kotlin/com/b1nd/dodam/network/core/DodamUrl.kt b/network/core/src/commonMain/kotlin/com/b1nd/dodam/network/core/DodamUrl.kt index 279ee572..70bd97be 100644 --- a/network/core/src/commonMain/kotlin/com/b1nd/dodam/network/core/DodamUrl.kt +++ b/network/core/src/commonMain/kotlin/com/b1nd/dodam/network/core/DodamUrl.kt @@ -15,6 +15,7 @@ object DodamUrl { const val BANNER = "$BASE_URL/banner" const val POINT = "$BASE_URL/point" const val GET_BUNDLE_ID = "https://itunes.apple.com/lookup?bundleId=com.b1nd.dodam.teacher&country=br" + const val UPLOAD = "$BASE_URL/upload" object Meal { const val MONTH = "$MEAL/month" @@ -34,6 +35,7 @@ object DodamUrl { const val MY = "$MEMBER/my" const val DEACTIVATION = "$MEMBER/deactivate" const val STATUS = "$MEMBER/status" + const val EDIT = "$MEMBER/info" } object WakeupSong { diff --git a/network/member/src/commonMain/kotlin/com/b1nd/dodam/member/api/MemberService.kt b/network/member/src/commonMain/kotlin/com/b1nd/dodam/member/api/MemberService.kt index 6417a406..a0643567 100644 --- a/network/member/src/commonMain/kotlin/com/b1nd/dodam/member/api/MemberService.kt +++ b/network/member/src/commonMain/kotlin/com/b1nd/dodam/member/api/MemberService.kt @@ -1,8 +1,10 @@ package com.b1nd.dodam.member.api import com.b1nd.dodam.member.datasource.MemberDataSource +import com.b1nd.dodam.member.model.EditMemberInfoRequest import com.b1nd.dodam.member.model.MemberInfoResponse import com.b1nd.dodam.network.core.DodamUrl +import com.b1nd.dodam.network.core.model.DefaultResponse import com.b1nd.dodam.network.core.model.Response import com.b1nd.dodam.network.core.util.defaultSafeRequest import com.b1nd.dodam.network.core.util.safeRequest @@ -11,6 +13,7 @@ import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.client.request.parameter import io.ktor.client.request.patch +import io.ktor.client.request.setBody internal class MemberService( private val client: HttpClient, @@ -36,4 +39,19 @@ internal class MemberService( }.body() } } + + override suspend fun editMemberInfo(name: String, email: String, phone: String, profileImage: String?) { + return defaultSafeRequest { + client.patch(DodamUrl.Member.EDIT) { + setBody( + EditMemberInfoRequest( + name = name, + email = email, + phone = phone, + profileImage = profileImage, + ), + ) + }.body() + } + } } diff --git a/network/member/src/commonMain/kotlin/com/b1nd/dodam/member/datasource/MemberDataSource.kt b/network/member/src/commonMain/kotlin/com/b1nd/dodam/member/datasource/MemberDataSource.kt index 6349bdf3..013424ad 100644 --- a/network/member/src/commonMain/kotlin/com/b1nd/dodam/member/datasource/MemberDataSource.kt +++ b/network/member/src/commonMain/kotlin/com/b1nd/dodam/member/datasource/MemberDataSource.kt @@ -6,4 +6,5 @@ interface MemberDataSource { suspend fun getMyInfo(): MemberInfoResponse suspend fun deactivation() suspend fun getMemberAll(status: String): List + suspend fun editMemberInfo(name: String, email: String, phone: String, profileImage: String?) } diff --git a/network/member/src/commonMain/kotlin/com/b1nd/dodam/member/model/EditMemberInfoRequest.kt b/network/member/src/commonMain/kotlin/com/b1nd/dodam/member/model/EditMemberInfoRequest.kt new file mode 100644 index 00000000..aca44114 --- /dev/null +++ b/network/member/src/commonMain/kotlin/com/b1nd/dodam/member/model/EditMemberInfoRequest.kt @@ -0,0 +1,11 @@ +package com.b1nd.dodam.member.model + +import kotlinx.serialization.Serializable + +@Serializable +data class EditMemberInfoRequest( + val email: String, + val name: String, + val phone: String, + val profileImage: String?, +) diff --git a/network/schedule/src/commonMain/kotlin/com/b1nd/dodam/network/schedule/di/DatasourceModule.kt b/network/schedule/src/commonMain/kotlin/com/b1nd/dodam/network/schedule/di/DataSourceModule.kt similarity index 100% rename from network/schedule/src/commonMain/kotlin/com/b1nd/dodam/network/schedule/di/DatasourceModule.kt rename to network/schedule/src/commonMain/kotlin/com/b1nd/dodam/network/schedule/di/DataSourceModule.kt diff --git a/network/upload/.gitignore b/network/upload/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/network/upload/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/network/upload/build.gradle.kts b/network/upload/build.gradle.kts new file mode 100644 index 00000000..2b4553b3 --- /dev/null +++ b/network/upload/build.gradle.kts @@ -0,0 +1,33 @@ +import com.b1nd.dodam.dsl.android +import com.b1nd.dodam.dsl.kotlin +import com.b1nd.dodam.dsl.setIOS + +plugins { + alias(libs.plugins.dodam.multiplatform) + alias(libs.plugins.dodam.multiplatform.kotlin.serialization) + alias(libs.plugins.dodam.multiplatform.koin) +} + +kotlin { + setIOS("network.upload") + + sourceSets.commonMain.dependencies { + api(projects.network.core) + implementation(projects.common) + implementation(libs.kotlinx.collections.immutable) + } + + sourceSets.commonTest.dependencies { + implementation(libs.kotlin.test) + implementation(libs.ktor.client.mock) + implementation(libs.kotlinx.coroutines.test) + } +} + +android { + namespace = "com.b1nd.dodam.network.upload" + + defaultConfig { + consumerProguardFiles("consumer-rules.pro") + } +} \ No newline at end of file diff --git a/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/api/UploadService.kt b/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/api/UploadService.kt new file mode 100644 index 00000000..8d2c3695 --- /dev/null +++ b/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/api/UploadService.kt @@ -0,0 +1,43 @@ +package com.b1nd.dodam.network.upload.api + +import com.b1nd.dodam.network.core.DodamUrl +import com.b1nd.dodam.network.core.model.Response +import com.b1nd.dodam.network.core.util.safeRequest +import com.b1nd.dodam.network.upload.datasource.UploadDataSource +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.onUpload +import io.ktor.client.request.forms.MultiPartFormDataContent +import io.ktor.client.request.forms.formData +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders + +class UploadService( + private val client: HttpClient, +) : UploadDataSource { + override suspend fun upload(fileName: String, fileMimeType: String, byteArray: ByteArray): String { + return safeRequest { + client.post(DodamUrl.UPLOAD) { + setBody( + MultiPartFormDataContent( + formData { + append( + "file", + byteArray, + Headers.build { + append(HttpHeaders.ContentType, fileMimeType) + append(HttpHeaders.ContentDisposition, "filename=\"${fileName}\"") + }, + ) + }, + ), + ) + onUpload { bytesSentTotal, contentLength -> + println("Sent $bytesSentTotal bytes from $contentLength") + } + }.body>() + } + } +} diff --git a/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/datasource/UploadDataSource.kt b/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/datasource/UploadDataSource.kt new file mode 100644 index 00000000..798b1406 --- /dev/null +++ b/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/datasource/UploadDataSource.kt @@ -0,0 +1,6 @@ +package com.b1nd.dodam.network.upload.datasource + +interface UploadDataSource { + + suspend fun upload(fileName: String, fileMimeType: String, byteArray: ByteArray): String +} diff --git a/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/di/DataSourceModule.kt b/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/di/DataSourceModule.kt new file mode 100644 index 00000000..73d27ba0 --- /dev/null +++ b/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/di/DataSourceModule.kt @@ -0,0 +1,11 @@ +package com.b1nd.dodam.network.upload.di + +import com.b1nd.dodam.network.upload.api.UploadService +import com.b1nd.dodam.network.upload.datasource.UploadDataSource +import org.koin.dsl.module + +val uploadDatasourceModule = module { + single { + UploadService(get()) + } +} diff --git a/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/model/UploadResponse.kt b/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/model/UploadResponse.kt new file mode 100644 index 00000000..94a652ca --- /dev/null +++ b/network/upload/src/commonMain/kotlin/com/b1nd/dodam/network/upload/model/UploadResponse.kt @@ -0,0 +1,10 @@ +package com.b1nd.dodam.network.upload.model + +import kotlinx.serialization.Serializable + +@Serializable +data class UploadResponse( + val status: Int, + val message: String, + val data: String, +) diff --git a/settings.gradle.kts b/settings.gradle.kts index bd27f354..988e7815 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,11 @@ dependencyResolutionManagement { mavenCentral() maven("https://jitpack.io") maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") + maven("https://s01.oss.sonatype.org/content/repositories/snapshots") + maven("https://oss.sonatype.org/content/repositories/snapshots") { + name = "SonatypeSnapshots" + mavenContent { snapshotsOnly() } + } } } @@ -81,3 +86,6 @@ include(":feature-teacher:approve-outing") include(":feature-teacher:approve-nightstudy") include(":network:bundleid-info") include(":data:bundleid-info") +include(":feature:edit-member-info") +include(":network:upload") +include(":data:upload")