diff --git a/core/src/main/java/com/sopt/core/designsystem/component/button/NoostakBottomButton.kt b/core/src/main/java/com/sopt/core/designsystem/component/button/NoostakBottomButton.kt new file mode 100644 index 00000000..b25c9760 --- /dev/null +++ b/core/src/main/java/com/sopt/core/designsystem/component/button/NoostakBottomButton.kt @@ -0,0 +1,75 @@ +package com.sopt.core.designsystem.component.button + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.sopt.core.R +import com.sopt.core.designsystem.theme.NoostakAndroidTheme +import com.sopt.core.designsystem.theme.NoostakTheme +import com.sopt.core.extension.noRippleClickable +import com.sopt.core.util.NoRippleInteractionSource + +@Composable +fun NoostakBottomButton( + shape: Shape = RoundedCornerShape(8.dp), + style: TextStyle = NoostakTheme.typography.t3Bold, + paddingHorizontal: Dp = 0.dp, + paddingVertical: Dp = dimensionResource(id = R.dimen.bottom_btn_vertical_padding), + text: String, + onButtonClick: () -> Unit, + modifier: Modifier = Modifier, + activateColor: Color = NoostakTheme.colors.blue600, + deactivateColor: Color = NoostakTheme.colors.gray500, + isEnabled: Boolean = true +) { + Button( + modifier = modifier + .fillMaxWidth() + .padding(dimensionResource(id = R.dimen.bottom_padding)) + .run { + if (isEnabled) { + noRippleClickable(onClick = onButtonClick) + } else { + this + } + }, + shape = shape, + colors = ButtonDefaults.buttonColors( + containerColor = if (isEnabled) activateColor else deactivateColor + ), + contentPadding = PaddingValues( + vertical = paddingVertical, + horizontal = paddingHorizontal + ), + onClick = { onButtonClick() }, + enabled = isEnabled, + interactionSource = NoRippleInteractionSource + ) { + Text( + text = text, + color = NoostakTheme.colors.white, + style = style + ) + } +} + +@Preview(showBackground = true) +@Composable +fun NoostakBottomButtonPreview() { + NoostakAndroidTheme { + NoostakBottomButton(text = "다음", onButtonClick = {}) + } +} diff --git a/core/src/main/java/com/sopt/core/designsystem/component/button/NoostakFloatingActionButton.kt b/core/src/main/java/com/sopt/core/designsystem/component/button/NoostakFloatingActionButton.kt index 3eac088a..8d35c4b9 100644 --- a/core/src/main/java/com/sopt/core/designsystem/component/button/NoostakFloatingActionButton.kt +++ b/core/src/main/java/com/sopt/core/designsystem/component/button/NoostakFloatingActionButton.kt @@ -28,7 +28,7 @@ fun NoostakFloatingActionButton( shape = RoundedCornerShape(dimensionResource(id = R.dimen.fab_radius)), containerColor = NoostakTheme.colors.black, contentColor = NoostakTheme.colors.white, - elevation = FloatingActionButtonDefaults.elevation(4.dp), + elevation = FloatingActionButtonDefaults.elevation(dimensionResource(id = R.dimen.fab_shadow)), onClick = { onClick() }, interactionSource = NoRippleInteractionSource ) { diff --git a/core/src/main/java/com/sopt/core/designsystem/component/chip/NoostakCategoryChip.kt b/core/src/main/java/com/sopt/core/designsystem/component/chip/NoostakCategoryChip.kt index 90dc438c..e7212dc6 100644 --- a/core/src/main/java/com/sopt/core/designsystem/component/chip/NoostakCategoryChip.kt +++ b/core/src/main/java/com/sopt/core/designsystem/component/chip/NoostakCategoryChip.kt @@ -3,7 +3,6 @@ package com.sopt.core.designsystem.component.chip import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.unit.dp import com.sopt.core.R import com.sopt.core.designsystem.theme.NoostakTheme diff --git a/core/src/main/java/com/sopt/core/designsystem/component/chip/NoostakUserChip.kt b/core/src/main/java/com/sopt/core/designsystem/component/chip/NoostakUserChip.kt index dfcb3cd5..73b94364 100644 --- a/core/src/main/java/com/sopt/core/designsystem/component/chip/NoostakUserChip.kt +++ b/core/src/main/java/com/sopt/core/designsystem/component/chip/NoostakUserChip.kt @@ -3,7 +3,6 @@ package com.sopt.core.designsystem.component.chip import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.unit.dp import com.sopt.core.R import com.sopt.core.designsystem.theme.NoostakTheme diff --git a/core/src/main/java/com/sopt/core/designsystem/component/snackbar/BaseSnackBar.kt b/core/src/main/java/com/sopt/core/designsystem/component/snackbar/BaseSnackBar.kt deleted file mode 100644 index a01d75ff..00000000 --- a/core/src/main/java/com/sopt/core/designsystem/component/snackbar/BaseSnackBar.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.sopt.core.designsystem.component.snackbar - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp - -@Composable -fun BaseSnackBar(content: @Composable () -> Unit) { - Box( - modifier = Modifier - .clip(CircleShape) - .background(color = Color.LightGray) - .padding(vertical = 7.dp, horizontal = 20.dp) - ) { - content() - } -} diff --git a/core/src/main/java/com/sopt/core/designsystem/component/snackbar/NoostakSnackBar.kt b/core/src/main/java/com/sopt/core/designsystem/component/snackbar/NoostakSnackBar.kt new file mode 100644 index 00000000..03262e27 --- /dev/null +++ b/core/src/main/java/com/sopt/core/designsystem/component/snackbar/NoostakSnackBar.kt @@ -0,0 +1,49 @@ +package com.sopt.core.designsystem.component.snackbar + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import com.sopt.core.R +import com.sopt.core.designsystem.theme.NoostakAndroidTheme +import com.sopt.core.designsystem.theme.NoostakTheme + +const val SNACK_BAR_DURATION = 2000L + +@Composable +fun NoostakSnackBar( + message: String = "", + textStyle: TextStyle = NoostakTheme.typography.c3Regular, + textColor: Color = NoostakTheme.colors.white, + backgroundColor: Color = NoostakTheme.colors.gray800 +) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(dimensionResource(id = R.dimen.snack_bar_radius))) + .background(color = backgroundColor) + .padding( + vertical = dimensionResource(id = R.dimen.snack_bar_vertical_padding), + horizontal = dimensionResource( + id = R.dimen.snack_bar_horizontal_padding + ) + ) + ) { + Text(text = message, style = textStyle, color = textColor) + } +} + +@Preview(showBackground = true) +@Composable +fun NoostakSnackBarPreview() { + NoostakAndroidTheme { + NoostakSnackBar(message = "Noostak SnackBar") + } +} diff --git a/core/src/main/java/com/sopt/core/designsystem/component/textfield/BaseTextField.kt b/core/src/main/java/com/sopt/core/designsystem/component/textfield/BaseTextField.kt index e88592f2..5baaa2b1 100644 --- a/core/src/main/java/com/sopt/core/designsystem/component/textfield/BaseTextField.kt +++ b/core/src/main/java/com/sopt/core/designsystem/component/textfield/BaseTextField.kt @@ -6,6 +6,8 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import com.sopt.core.designsystem.theme.NoostakAndroidTheme @Composable fun BaseTextField( @@ -40,3 +42,17 @@ fun BaseTextField( ) ) } + +@Preview(showBackground = true) +@Composable +fun BaseTextFieldPreview() { + NoostakAndroidTheme { + BaseTextField( + value = "내용", + onValueChange = {}, + label = "라벨", + placeholder = "힌트", + modifier = Modifier + ) + } +} diff --git a/core/src/main/java/com/sopt/core/designsystem/component/topappbar/BaseTopAppBar.kt b/core/src/main/java/com/sopt/core/designsystem/component/topappbar/BaseTopAppBar.kt deleted file mode 100644 index 4132c0d6..00000000 --- a/core/src/main/java/com/sopt/core/designsystem/component/topappbar/BaseTopAppBar.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.sopt.core.designsystem.component.topappbar - -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CenterAlignedTopAppBar -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color.Companion.White -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 com.sopt.core.R - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun BaseTopAppBar( - title: String = "", - modifier: Modifier, - onBackButtonClick: () -> Unit = {} -) { - CenterAlignedTopAppBar( - modifier = modifier, - title = { - Text( - text = title, - textAlign = TextAlign.Center - ) - }, - navigationIcon = { - IconButton( - onClick = { - onBackButtonClick() - } - ) { - Icon( - painter = painterResource(id = R.drawable.ic_back), - contentDescription = stringResource(id = R.string.ic_back), - modifier = Modifier - .padding(start = 8.dp) - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors(White), - windowInsets = WindowInsets( - left = 0, - top = 0, - right = 0, - bottom = 0 - ) - ) -} diff --git a/core/src/main/java/com/sopt/core/designsystem/component/topappbar/NoostakCloseAppBar.kt b/core/src/main/java/com/sopt/core/designsystem/component/topappbar/NoostakCloseAppBar.kt new file mode 100644 index 00000000..e2bdf85f --- /dev/null +++ b/core/src/main/java/com/sopt/core/designsystem/component/topappbar/NoostakCloseAppBar.kt @@ -0,0 +1,52 @@ +package com.sopt.core.designsystem.component.topappbar + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.sopt.core.R +import com.sopt.core.designsystem.theme.NoostakAndroidTheme +import com.sopt.core.designsystem.theme.NoostakTheme + +@Composable +fun NoostakCloseAppBar( + modifier: Modifier, + onBackButtonClick: () -> Unit = {} +) { + Box( + modifier = modifier + .fillMaxWidth() + .background(color = NoostakTheme.colors.white) + .height(dimensionResource(id = R.dimen.appbar_height)) + ) { + IconButton( + onClick = onBackButtonClick, + modifier = Modifier + .align(Alignment.CenterStart) + .size(dimensionResource(id = R.dimen.appbar_height)) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_appbar_close), + contentDescription = stringResource(R.string.icon_noostak_close_appbar_description) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun NoostakCloseAppBarPreview() { + NoostakAndroidTheme { + NoostakCloseAppBar(Modifier) + } +} diff --git a/core/src/main/java/com/sopt/core/designsystem/component/topappbar/NoostakTopAppBar.kt b/core/src/main/java/com/sopt/core/designsystem/component/topappbar/NoostakTopAppBar.kt index cbda4e82..9e06f2a7 100644 --- a/core/src/main/java/com/sopt/core/designsystem/component/topappbar/NoostakTopAppBar.kt +++ b/core/src/main/java/com/sopt/core/designsystem/component/topappbar/NoostakTopAppBar.kt @@ -11,11 +11,12 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color.Companion.White +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.sopt.core.R import com.sopt.core.designsystem.theme.NoostakAndroidTheme import com.sopt.core.designsystem.theme.NoostakTheme @@ -26,8 +27,9 @@ import com.sopt.core.extension.showIf @Composable fun NoostakTopAppBar( title: String = "", + style: TextStyle = NoostakTheme.typography.b2Regular, modifier: Modifier = Modifier, - isIconVisible: Boolean, + isIconVisible: Boolean = true, onBackButtonClick: () -> Unit = {} ) { CenterAlignedTopAppBar( @@ -36,7 +38,7 @@ fun NoostakTopAppBar( Text( text = title, textAlign = TextAlign.Center, - style = NoostakTheme.typography.b2Regular, + style = style, color = NoostakTheme.colors.black ) }, @@ -45,7 +47,7 @@ fun NoostakTopAppBar( painter = painterResource(id = R.drawable.ic_back), contentDescription = stringResource(id = R.string.ic_back), modifier = Modifier - .padding(start = 8.dp) + .padding(start = dimensionResource(id = R.dimen.appbar_start_padding)) .noRippleClickable { onBackButtonClick() } .showIf(isIconVisible), tint = NoostakTheme.colors.black diff --git a/core/src/main/java/com/sopt/core/designsystem/screen/NoostakEmptyScreen.kt b/core/src/main/java/com/sopt/core/designsystem/screen/NoostakEmptyScreen.kt new file mode 100644 index 00000000..57de3913 --- /dev/null +++ b/core/src/main/java/com/sopt/core/designsystem/screen/NoostakEmptyScreen.kt @@ -0,0 +1,29 @@ +package com.sopt.core.designsystem.screen + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle + +@Composable +fun NoostakEmptyScreen(@StringRes emptyText: Int, color: Color, style: TextStyle) { + Box( + modifier = Modifier + .fillMaxSize() + .aspectRatio(1f) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = stringResource(emptyText), + color = color, + style = style + ) + } +} diff --git a/core/src/main/res/drawable/ic_appbar_close.xml b/core/src/main/res/drawable/ic_appbar_close.xml new file mode 100644 index 00000000..65095021 --- /dev/null +++ b/core/src/main/res/drawable/ic_appbar_close.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml index 36fe338b..16581b73 100644 --- a/core/src/main/res/values/dimens.xml +++ b/core/src/main/res/values/dimens.xml @@ -5,5 +5,19 @@ 6dp 15dp 6dp + 30dp + 4dp + + 15dp + + 16dp + + 30dp + 12dp + 20dp + + 48dp + 8dp + \ No newline at end of file diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 6bf9423c..0c1d45e1 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -1,4 +1,9 @@ + 뒤로가기 버튼 + + + Icon Delete On AppBar + \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/entity/GroupEntity.kt b/domain/src/main/java/com/sopt/domain/entity/GroupEntity.kt new file mode 100644 index 00000000..efbd9227 --- /dev/null +++ b/domain/src/main/java/com/sopt/domain/entity/GroupEntity.kt @@ -0,0 +1,8 @@ +package com.sopt.domain.entity + +data class GroupEntity( + val groupId: Long = -1, + val groupName: String = "", + val groupPersonnel: Long = 0, + val newsImage: String? = null +) diff --git a/domain/src/main/java/com/sopt/domain/entity/GroupProfileEntity.kt b/domain/src/main/java/com/sopt/domain/entity/GroupProfileEntity.kt new file mode 100644 index 00000000..3aaa864a --- /dev/null +++ b/domain/src/main/java/com/sopt/domain/entity/GroupProfileEntity.kt @@ -0,0 +1,8 @@ +package com.sopt.domain.entity + +data class GroupProfileEntity( + val groupName: String = "", + val selectedImageUri: String? = null, + val isPermissionGranted: Boolean = false, + val isGroupNameCheck: Boolean = false +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 22a1c6c7..9112c351 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,6 +47,9 @@ encrypted-datastore = "1.7.21-1.0.8-1.1.0-alpha01" coil = "2.7.0" lottie-compose = "6.4.1" +# Landscapist +landscapist = "2.3.6" + # Kakao kakao = "2.20.1" @@ -142,6 +145,12 @@ viewpager-indicator = { group = "com.tbuonomo", name = "dotsindicator", version. androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } # AndroidX Activity 라이브러리 process-phoenix = { group = "com.jakewharton", name = "process-phoenix", version.ref = "phoenix" } # Process Phoenix 라이브러리 +# landscapist +landscapist-bom = { group = "com.github.skydoves", name = "landscapist-bom", version.ref = "landscapist" } +landscapist-glide = { group = "com.github.skydoves", name = "landscapist-glide" } +landscapist-placeholder = { group = "com.github.skydoves", name = "landscapist-placeholder" } +landscapist-animation = { group = "com.github.skydoves", name = "landscapist-animation" } + # Kakao kakao-all = { group = "com.kakao.sdk", name = "v2-all", version.ref = "kakao" } # Kakao SDK 라이브러리 kakao-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao" } # Kakao 로그인 라이브러리 @@ -174,3 +183,4 @@ material = { group = "com.google.android.material", name = "material", version.r [bundles] retrofit = ["retrofit-core", "retrofit-kotlin-serialization", "okhttp-logging"] datastore = ["androidx-datastore-core", "androidx-datastore-preferences", "encrypted-datastore-preference-ksp", "encrypted-datastore-preference-ksp-annotations", "encrypted-datastore-preference-security"] +landscapist-glide = ["landscapist-bom", "landscapist-glide", "landscapist-placeholder", "landscapist-animation"] \ No newline at end of file diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index eb389d42..d90826f2 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { // Image Loading implementation(libs.coil.compose) implementation(libs.lottie.compose) + implementation(libs.bundles.landscapist.glide) // Logging implementation(libs.timber) diff --git a/presentation/src/main/java/com/sopt/presentation/example/ExampleRoute.kt b/presentation/src/main/java/com/sopt/presentation/example/ExampleRoute.kt index 7b961aec..a4c173f0 100644 --- a/presentation/src/main/java/com/sopt/presentation/example/ExampleRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/example/ExampleRoute.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.core.designsystem.component.button.BaseButton -import com.sopt.core.designsystem.component.topappbar.BaseTopAppBar +import com.sopt.core.designsystem.component.topappbar.NoostakTopAppBar import com.sopt.core.designsystem.theme.NoostakAndroidTheme import com.sopt.core.extension.toast import com.sopt.core.state.UiState @@ -109,7 +109,7 @@ fun ExampleScreen( .statusBarsPadding() .navigationBarsPadding() ) { - BaseTopAppBar( + NoostakTopAppBar( title = stringResource(R.string.appbar_example_title), modifier = Modifier.fillMaxWidth(), onBackButtonClick = { onBackButtonClick() } diff --git a/presentation/src/main/java/com/sopt/presentation/group/GroupRoute.kt b/presentation/src/main/java/com/sopt/presentation/group/GroupRoute.kt index a11f22c2..22b181bc 100644 --- a/presentation/src/main/java/com/sopt/presentation/group/GroupRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/group/GroupRoute.kt @@ -1,59 +1,137 @@ package com.sopt.presentation.group -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.FabPosition +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import com.sopt.core.designsystem.component.button.NoostakFloatingActionButton +import com.sopt.core.designsystem.component.topappbar.NoostakTopAppBar +import com.sopt.core.designsystem.screen.NoostakEmptyScreen import com.sopt.core.designsystem.theme.NoostakAndroidTheme -import timber.log.Timber +import com.sopt.core.designsystem.theme.NoostakTheme +import com.sopt.domain.entity.GroupEntity +import com.sopt.presentation.R +import com.sopt.presentation.group.component.GroupFloatingActionDialog +import com.sopt.presentation.group.component.GroupItem +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest @Composable fun GroupRoute( - paddingValues: PaddingValues, + viewModel: GroupViewModel = hiltViewModel(), navigateToGroupDetail: (Long) -> Unit, - groupViewModel: GroupViewModel = hiltViewModel() + navigateToGroupCreate: () -> Unit, + navigateToGroupEnter: () -> Unit ) { - LaunchedEffect(groupViewModel.sideEffects) { - groupViewModel.sideEffects.collect { sideEffect -> - when (sideEffect) { - is GroupSideEffect.NavigateToGroupDetail -> { - navigateToGroupDetail(sideEffect.id) - Timber.d("group id: ${sideEffect.id}") + val lifecycleOwner = LocalLifecycleOwner.current + val groupItems = viewModel.groupItems + + val isEmpty = groupItems.isEmpty() + + val showDialog by viewModel.showDialog.collectAsStateWithLifecycle() + + LaunchedEffect(lifecycleOwner) { + viewModel.sideEffects.flowWithLifecycle(lifecycleOwner.lifecycle) + .collectLatest { sideEffect -> + when (sideEffect) { + is GroupSideEffect.NavigateToGroupDetail -> navigateToGroupDetail(sideEffect.groupId) + is GroupSideEffect.NavigateToGroupCreate -> navigateToGroupCreate() + is GroupSideEffect.NavigateToGroupEnter -> navigateToGroupEnter() } } - } } - GroupScreen( - paddingValues = paddingValues, - onGroupClick = groupViewModel::navigateToGroupDetail - ) + if (showDialog) { + GroupFloatingActionDialog( + onClick = { viewModel.showFloatingActionButtonDialog(false) }, + onDismissRequest = { viewModel.showFloatingActionButtonDialog(false) }, + onCreateGroupClick = viewModel::navigateToGroupCreate, + onEnterGroupClick = viewModel::navigateToGroupEnter + ) + } + + when { + isEmpty -> NoostakEmptyScreen( + emptyText = R.string.text_group_empty, + color = NoostakTheme.colors.gray600, + style = NoostakTheme.typography.b4Regular + ) + + else -> GroupScreen( + groupItems = groupItems, + isFabClicked = viewModel.showDialog, + onItemClick = viewModel::navigateToGroupDetail, + onFabClick = { viewModel.showFloatingActionButtonDialog(true) } + ) + } } +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "StateFlowValueCalledInComposition") @Composable fun GroupScreen( - paddingValues: PaddingValues = PaddingValues(), - onGroupClick: (Long) -> Unit + groupItems: List, + isFabClicked: StateFlow, + onItemClick: (Long) -> Unit, + onFabClick: () -> Unit ) { - Column( + Scaffold( modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text(text = "Appointment Screen") - Button(onClick = { onGroupClick(2) }) { - Text(text = "그룹 상세 페이지로 이동") + .statusBarsPadding() + .navigationBarsPadding(), + topBar = { + NoostakTopAppBar( + title = stringResource(R.string.bottom_nav_group), + modifier = Modifier, + isIconVisible = false + ) + }, + floatingActionButton = { + if (!isFabClicked.value) { + NoostakFloatingActionButton( + title = stringResource(R.string.fab_group_create), + modifier = Modifier.offset(x = 0.dp, y = (-74).dp) + ) { + onFabClick() + } + } + }, + floatingActionButtonPosition = FabPosition.End + ) { innerPadding -> + Box { + LazyColumn( + modifier = Modifier + .padding(innerPadding) + .padding(horizontal = dimensionResource(id = R.dimen.horizontal_padding)) + ) { + items(items = groupItems, key = { item -> item.groupId }) { + GroupItem(it, onItemClick) + HorizontalDivider( + thickness = 1.dp, + color = NoostakTheme.colors.gray100 + ) + } + } } } } @@ -63,7 +141,19 @@ fun GroupScreen( fun GroupScreenPreview() { NoostakAndroidTheme { GroupScreen( - onGroupClick = {} + groupItems = listOf( + GroupEntity(groupId = 1, groupName = "누스탁", groupPersonnel = 15, newsImage = null), + GroupEntity( + groupId = 2, + groupName = "유니보이스", + groupPersonnel = 16, + newsImage = null + ), + GroupEntity(groupId = 3, groupName = "솝트", groupPersonnel = 191, newsImage = null) + ), + isFabClicked = remember { MutableStateFlow(false) }, + onItemClick = {}, + onFabClick = {} ) } } diff --git a/presentation/src/main/java/com/sopt/presentation/group/GroupSideEffect.kt b/presentation/src/main/java/com/sopt/presentation/group/GroupSideEffect.kt index bbc37dfc..1ce1cc0f 100644 --- a/presentation/src/main/java/com/sopt/presentation/group/GroupSideEffect.kt +++ b/presentation/src/main/java/com/sopt/presentation/group/GroupSideEffect.kt @@ -1,5 +1,7 @@ package com.sopt.presentation.group -sealed class GroupSideEffect { - data class NavigateToGroupDetail(val id: Long) : GroupSideEffect() +sealed interface GroupSideEffect { + data class NavigateToGroupDetail(val groupId: Long) : GroupSideEffect + data object NavigateToGroupCreate : GroupSideEffect + data object NavigateToGroupEnter : GroupSideEffect } diff --git a/presentation/src/main/java/com/sopt/presentation/group/GroupViewModel.kt b/presentation/src/main/java/com/sopt/presentation/group/GroupViewModel.kt index d9cde490..63659417 100644 --- a/presentation/src/main/java/com/sopt/presentation/group/GroupViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/group/GroupViewModel.kt @@ -1,20 +1,47 @@ package com.sopt.presentation.group -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import com.sopt.core.util.BaseViewModel +import com.sopt.domain.entity.GroupEntity import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import javax.inject.Inject @HiltViewModel -class GroupViewModel @Inject constructor() : ViewModel() { - private val _sideEffects = MutableSharedFlow() - val sideEffects: MutableSharedFlow get() = _sideEffects +class GroupViewModel @Inject constructor() : BaseViewModel() { + private val _showDialog = MutableStateFlow(false) + val showDialog: StateFlow get() = _showDialog - fun navigateToGroupDetail(id: Long) { - viewModelScope.launch { - _sideEffects.emit(GroupSideEffect.NavigateToGroupDetail(id)) - } + fun showFloatingActionButtonDialog(show: Boolean) { + _showDialog.update { show } } + + fun navigateToGroupDetail(groupId: Long) { + emitSideEffect(GroupSideEffect.NavigateToGroupDetail(groupId)) + } + + fun navigateToGroupCreate() { + emitSideEffect(GroupSideEffect.NavigateToGroupCreate) + } + + fun navigateToGroupEnter() { + emitSideEffect(GroupSideEffect.NavigateToGroupEnter) + } + + val groupItems = + listOf( + GroupEntity(groupId = 1, groupName = "누스탁", groupPersonnel = 15, newsImage = null), + GroupEntity(groupId = 2, groupName = "유니보이스", groupPersonnel = 16, newsImage = null), + GroupEntity(groupId = 3, groupName = "솝트", groupPersonnel = 191, newsImage = null), + GroupEntity(groupId = 4, groupName = "누스탁", groupPersonnel = 15, newsImage = null), + GroupEntity(groupId = 5, groupName = "유니보이스", groupPersonnel = 16, newsImage = null), + GroupEntity(groupId = 6, groupName = "솝트", groupPersonnel = 191, newsImage = null), + GroupEntity(groupId = 7, groupName = "누스탁", groupPersonnel = 15, newsImage = null), + GroupEntity(groupId = 8, groupName = "유니보이스", groupPersonnel = 16, newsImage = null), + GroupEntity(groupId = 9, groupName = "솝트", groupPersonnel = 191, newsImage = null), + GroupEntity(groupId = 10, groupName = "누스탁", groupPersonnel = 15, newsImage = null), + GroupEntity(groupId = 11, groupName = "유니보이스", groupPersonnel = 16, newsImage = null), + GroupEntity(groupId = 12, groupName = "솝트", groupPersonnel = 191, newsImage = null) + ) } diff --git a/presentation/src/main/java/com/sopt/presentation/group/component/GroupFloatingActionButtonItem.kt b/presentation/src/main/java/com/sopt/presentation/group/component/GroupFloatingActionButtonItem.kt new file mode 100644 index 00000000..cad8b3f7 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/group/component/GroupFloatingActionButtonItem.kt @@ -0,0 +1,61 @@ +package com.sopt.presentation.group.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.sopt.core.designsystem.theme.NoostakAndroidTheme +import com.sopt.core.designsystem.theme.NoostakTheme +import com.sopt.core.extension.noRippleClickable +import com.sopt.presentation.R + +@Composable +fun GroupFloatingActionButtonItem( + painter: Painter, + text: String, + onItemClick: () -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .width(122.dp) + .noRippleClickable { onItemClick() } + ) { + Image( + painter = painter, + contentDescription = null, + modifier = Modifier + .padding(end = 8.dp) + .size(24.dp) + .aspectRatio(1f) + ) + Text( + text = text, + color = NoostakTheme.colors.black, + style = NoostakTheme.typography.b4Regular, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun GroupFloatingActionButtonItemPreview() { + NoostakAndroidTheme { + GroupFloatingActionButtonItem( + painter = painterResource(id = R.drawable.ic_launcher_background), + text = "그룹 만들기", + onItemClick = {} + ) + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/group/component/GroupFloatingActionDialog.kt b/presentation/src/main/java/com/sopt/presentation/group/component/GroupFloatingActionDialog.kt new file mode 100644 index 00000000..b743a6a6 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/group/component/GroupFloatingActionDialog.kt @@ -0,0 +1,112 @@ +package com.sopt.presentation.group.component + +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.sopt.core.designsystem.theme.NoostakAndroidTheme +import com.sopt.core.designsystem.theme.NoostakTheme +import com.sopt.core.extension.noRippleClickable +import com.sopt.core.util.NoRippleInteractionSource +import com.sopt.presentation.R + +@Composable +fun GroupFloatingActionDialog( + onClick: () -> Unit, + onDismissRequest: () -> Unit = {}, + onCreateGroupClick: () -> Unit, + onEnterGroupClick: () -> Unit +) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnClickOutside = true + ) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .noRippleClickable { onDismissRequest() } + ) { + Column( + modifier = Modifier + .padding( + end = dimensionResource(id = R.dimen.horizontal_padding), + bottom = 73.dp + ) + .align(Alignment.BottomEnd) + ) { + Box( + modifier = Modifier.background( + color = NoostakTheme.colors.white, + shape = RoundedCornerShape(12.dp) + ) + ) { + Column(modifier = Modifier.padding(horizontal = 17.dp, vertical = 18.dp)) { + GroupFloatingActionButtonItem( + painter = painterResource(id = R.drawable.ic_launcher_background), + text = stringResource(R.string.text_group_create_title) + ) { + onCreateGroupClick() + } + Spacer(modifier = Modifier.height(12.dp)) + GroupFloatingActionButtonItem( + painter = painterResource(id = R.drawable.ic_launcher_background), + text = stringResource(R.string.text_group_fab_enter) + ) { + onEnterGroupClick() + } + } + } + Spacer(modifier = Modifier.height(8.dp)) + FloatingActionButton( + modifier = Modifier.align(Alignment.End), + shape = CircleShape, + containerColor = NoostakTheme.colors.white, + elevation = FloatingActionButtonDefaults.elevation(0.dp), + onClick = { onClick() }, + interactionSource = NoRippleInteractionSource + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_group_fab_close), + contentDescription = null, + tint = NoostakTheme.colors.black + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun GroupFloatingActionDialogPreview() { + NoostakAndroidTheme { + GroupFloatingActionDialog( + onClick = {}, + onCreateGroupClick = {}, + onEnterGroupClick = {} + ) + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/group/component/GroupImage.kt b/presentation/src/main/java/com/sopt/presentation/group/component/GroupImage.kt new file mode 100644 index 00000000..2fcf999b --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/group/component/GroupImage.kt @@ -0,0 +1,44 @@ +package com.sopt.presentation.group.component + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.skydoves.landscapist.ImageOptions +import com.skydoves.landscapist.glide.GlideImage +import com.sopt.core.designsystem.theme.NoostakAndroidTheme +import com.sopt.presentation.R + +@Composable +fun GroupImage( + imageUrl: String?, + modifier: Modifier = Modifier +) { + if (!imageUrl.isNullOrBlank()) { + GlideImage( + imageModel = { imageUrl }, + imageOptions = ImageOptions( + contentScale = ContentScale.Crop, + alignment = Alignment.Center + ), + modifier = modifier.clip(RoundedCornerShape(dimensionResource(id = R.dimen.image_radius))), + previewPlaceholder = painterResource(id = R.drawable.ic_launcher_background) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun GroupImagePreview() { + NoostakAndroidTheme { + GroupImage( + imageUrl = "https://item.elandrs.com/r/image/item/2023-03-29/21e48456-d3bf-4dee-9f3f-8e516f344175.jpg?w=750&h=&q=100", + modifier = Modifier + ) + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/group/component/GroupItem.kt b/presentation/src/main/java/com/sopt/presentation/group/component/GroupItem.kt new file mode 100644 index 00000000..63cf61a9 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/group/component/GroupItem.kt @@ -0,0 +1,65 @@ +package com.sopt.presentation.group.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.sopt.core.designsystem.theme.NoostakAndroidTheme +import com.sopt.core.designsystem.theme.NoostakTheme +import com.sopt.core.extension.noRippleClickable +import com.sopt.domain.entity.GroupEntity + +@Composable +fun GroupItem( + data: GroupEntity, + onItemClick: (Long) -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onItemClick(data.groupId) } + .padding(vertical = 14.dp, horizontal = 6.dp) + ) { + GroupImage( + imageUrl = "https://item.elandrs.com/r/image/item/2023-03-29/21e48456-d3bf-4dee-9f3f-8e516f344175.jpg?w=750&h=&q=100", + modifier = Modifier + .size(44.dp) + .aspectRatio(1f) + ) + Spacer(modifier = Modifier.width(14.dp)) + Text( + text = data.groupName, + color = NoostakTheme.colors.gray900, + style = NoostakTheme.typography.b4SemiBold, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = data.groupPersonnel.toString(), + color = NoostakTheme.colors.gray700, + style = NoostakTheme.typography.b4Regular, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun GroupItemPreview() { + NoostakAndroidTheme { + GroupItem( + data = GroupEntity(1, "누스탁", 1, null), + onItemClick = {} + ) + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/group/navigation/GroupNavigation.kt b/presentation/src/main/java/com/sopt/presentation/group/navigation/GroupNavigation.kt index f2bbaccd..f929a056 100644 --- a/presentation/src/main/java/com/sopt/presentation/group/navigation/GroupNavigation.kt +++ b/presentation/src/main/java/com/sopt/presentation/group/navigation/GroupNavigation.kt @@ -1,12 +1,13 @@ package com.sopt.presentation.group.navigation -import androidx.compose.foundation.layout.PaddingValues import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.sopt.core.navigation.MainTabRoute import com.sopt.presentation.group.GroupRoute +import com.sopt.presentation.groupCreate.navigation.navigateToGroupCreate +import com.sopt.presentation.groupCreate.navigation.navigateToGroupEnter import com.sopt.presentation.groupDetail.navigation.navigateGroupDetail import kotlinx.serialization.Serializable @@ -18,15 +19,15 @@ fun NavController.navigateGroup(navOptions: NavOptions? = null) { } fun NavGraphBuilder.groupNavGraph( - paddingValues: PaddingValues, navHostController: NavController ) { composable { GroupRoute( - paddingValues = paddingValues, navigateToGroupDetail = { groupId -> navHostController.navigateGroupDetail(groupId = groupId) - } + }, + navigateToGroupCreate = { navHostController.navigateToGroupCreate() }, + navigateToGroupEnter = { navHostController.navigateToGroupEnter() } ) } } diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/GroupCreateRoute.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/GroupCreateRoute.kt new file mode 100644 index 00000000..dadd34de --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/GroupCreateRoute.kt @@ -0,0 +1,207 @@ +package com.sopt.presentation.groupCreate + +import android.Manifest +import android.os.Build +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import com.sopt.core.designsystem.component.button.NoostakBottomButton +import com.sopt.core.designsystem.theme.NoostakAndroidTheme +import com.sopt.core.designsystem.theme.NoostakTheme +import com.sopt.domain.entity.GroupProfileEntity +import com.sopt.presentation.R +import com.sopt.presentation.groupCreate.component.GroupProfileImagePicker +import com.sopt.presentation.groupCreate.component.GroupProfileNameTextField +import com.sopt.presentation.groupCreate.permission.launchImagePicker +import com.sopt.presentation.groupCreate.permission.rememberGalleryLauncher +import com.sopt.presentation.groupCreate.permission.rememberPhotoPickerLauncher +import timber.log.Timber + +@Composable +fun GroupCreateRoute( + paddingValues: PaddingValues, + navigateToGroupCreateSuccess: () -> Unit, + viewModel: GroupCreateViewModel = hiltViewModel() +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val groupProfileState by viewModel.groupProfileState.collectAsStateWithLifecycle() + + var isGalleryPermission by remember { mutableStateOf(false) } + + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted -> + try { + if (isGranted) { + viewModel.updateGalleryPermissionState(true) + } else { + isGalleryPermission = true + } + } catch (e: Exception) { + Timber.e(e) + } + } + + LaunchedEffect(Unit) { + val permission = when { + Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU -> Manifest.permission.READ_MEDIA_IMAGES + Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU -> Manifest.permission.READ_EXTERNAL_STORAGE + else -> return@LaunchedEffect + } + + permissionLauncher.launch(permission) + } + + val galleryLauncher = rememberGalleryLauncher { uri -> + viewModel.onImageSelected(uri.toString()) + } + + val photoPickerLauncher = rememberPhotoPickerLauncher { uri -> + viewModel.onImageSelected(uri.toString()) + } + + LaunchedEffect(lifecycleOwner) { + viewModel.sideEffects.flowWithLifecycle(lifecycleOwner.lifecycle) + .collect { sideEffect -> + when (sideEffect) { + is GroupCreateSideEffect.NavigateToGroupCreateSuccess -> navigateToGroupCreateSuccess() + + is GroupCreateSideEffect.ShowPermissionDeniedDialog -> + isGalleryPermission = + true + + is GroupCreateSideEffect.RequestImagePicker -> context.launchImagePicker( + galleryLauncher, + photoPickerLauncher + ) + } + } + } + + if (isGalleryPermission) { + Toast.makeText( + context, + "설정에서 갤러리 권한을 설정하세요", + Toast.LENGTH_SHORT + ).show() + isGalleryPermission = false + } + + GroupCreateScreen( + paddingValues = paddingValues, + groupProfileState = groupProfileState, + onProfileCameraBtnClick = { viewModel.requestGalleryPicker() }, + onNameChange = { newName -> + viewModel.onGroupNameChanged(newName) + }, + onNextBtnClick = { nickname, imageUri -> + viewModel.navigateToGroupCreateSuccess() + } + ) +} + +@Composable +fun GroupCreateScreen( + paddingValues: PaddingValues = PaddingValues(), + groupProfileState: GroupProfileEntity, + onProfileCameraBtnClick: () -> Unit = {}, + onNameChange: (String) -> Unit = {}, + onNextBtnClick: (String, String?) -> Unit +) { + val focusManager = LocalFocusManager.current + + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = dimensionResource(id = R.dimen.horizontal_padding)) + ) { + Column( + modifier = Modifier + .weight(1f) + .pointerInput(Unit) { + detectTapGestures(onTap = { + focusManager.clearFocus() + }) + } + ) { + Text( + text = stringResource(R.string.text_group_create_title), + color = NoostakTheme.colors.black, + style = NoostakTheme.typography.h2Bold, + modifier = Modifier.padding(top = 70.dp) + ) + GroupProfileImagePicker( + selectedImageUri = groupProfileState.selectedImageUri, + onCameraBtnClick = onProfileCameraBtnClick, + modifier = Modifier + .padding(top = 46.dp) + .align(Alignment.CenterHorizontally) + ) + Spacer(modifier = Modifier.height(27.dp)) + GroupProfileNameTextField( + value = groupProfileState.groupName, + placeholder = stringResource(R.string.tf_group_create_placeholder), + onValueChange = { onNameChange(it) } + ) + } + NoostakBottomButton( + text = stringResource(R.string.btn_group_create_next), + activateColor = NoostakTheme.colors.blue600, + deactivateColor = NoostakTheme.colors.gray500, + isEnabled = groupProfileState.isGroupNameCheck, + onButtonClick = { + onNextBtnClick( + groupProfileState.groupName, + groupProfileState.selectedImageUri + ) + } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun GroupCreateScreenPreview() { + NoostakAndroidTheme { + GroupCreateScreen( + groupProfileState = GroupProfileEntity( + groupName = "누스탁", + selectedImageUri = null + ), + onProfileCameraBtnClick = {}, + onNameChange = {}, + onNextBtnClick = { _, _ -> } + ) + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/GroupCreateSideEffect.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/GroupCreateSideEffect.kt new file mode 100644 index 00000000..2d09d434 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/GroupCreateSideEffect.kt @@ -0,0 +1,7 @@ +package com.sopt.presentation.groupCreate + +sealed interface GroupCreateSideEffect { + data object RequestImagePicker : GroupCreateSideEffect + data object ShowPermissionDeniedDialog : GroupCreateSideEffect + data object NavigateToGroupCreateSuccess : GroupCreateSideEffect +} diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/GroupCreateViewModel.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/GroupCreateViewModel.kt new file mode 100644 index 00000000..9c1fbd27 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/GroupCreateViewModel.kt @@ -0,0 +1,52 @@ +package com.sopt.presentation.groupCreate + +import androidx.lifecycle.viewModelScope +import com.sopt.core.util.BaseViewModel +import com.sopt.domain.entity.GroupProfileEntity +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class GroupCreateViewModel @Inject constructor() : BaseViewModel() { + private val _groupProfileState = MutableStateFlow(GroupProfileEntity()) + val groupProfileState: StateFlow = _groupProfileState + + fun updateGalleryPermissionState(isGranted: Boolean) { + viewModelScope.launch { + _groupProfileState.update { it.copy(isPermissionGranted = isGranted) } + } + } + + fun navigateToGroupCreateSuccess() { + emitSideEffect(GroupCreateSideEffect.NavigateToGroupCreateSuccess) + } + + fun requestGalleryPicker() { + if (_groupProfileState.value.isPermissionGranted) { + emitSideEffect(GroupCreateSideEffect.RequestImagePicker) + } else { + emitSideEffect(GroupCreateSideEffect.ShowPermissionDeniedDialog) + } + } + + fun onImageSelected(imageUri: String?) { + viewModelScope.launch { + _groupProfileState.update { it.copy(selectedImageUri = imageUri) } + } + } + + fun onGroupNameChanged(groupName: String) { + _groupProfileState.update { it.copy(groupName = groupName) } + validateGroupName(groupName) + } + + private fun validateGroupName(groupName: String) { + viewModelScope.launch { + _groupProfileState.update { it.copy(isGroupNameCheck = groupName.length in 1..30) } + } + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/component/GroupProfileImagePicker.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/component/GroupProfileImagePicker.kt new file mode 100644 index 00000000..def06e77 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/component/GroupProfileImagePicker.kt @@ -0,0 +1,61 @@ +package com.sopt.presentation.groupCreate.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.skydoves.landscapist.ImageOptions +import com.skydoves.landscapist.glide.GlideImage +import com.sopt.core.designsystem.theme.NoostakAndroidTheme +import com.sopt.core.extension.noRippleClickable +import com.sopt.presentation.R + +@Composable +fun GroupProfileImagePicker( + selectedImageUri: String?, + onCameraBtnClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box(modifier = modifier) { + GlideImage( + imageModel = { selectedImageUri ?: R.drawable.ic_group_create_profile }, + imageOptions = ImageOptions( + contentScale = ContentScale.Crop, + alignment = Alignment.Center + ), + modifier = Modifier + .size(112.dp) + .aspectRatio(1f) + .clip(CircleShape), + previewPlaceholder = painterResource(id = R.drawable.ic_group_create_profile) + ) + Image( + painter = painterResource(id = R.drawable.ic_group_create_camera), + contentDescription = stringResource(R.string.image_group_profile_image_picker_description), + modifier = Modifier + .size(35.dp) + .align(Alignment.BottomEnd) + .noRippleClickable { + onCameraBtnClick() + } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun GroupProfileImagePickerPreview() { + NoostakAndroidTheme { + GroupProfileImagePicker(selectedImageUri = null, onCameraBtnClick = {}) + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/component/GroupProfileNameTextField.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/component/GroupProfileNameTextField.kt new file mode 100644 index 00000000..92d02099 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/component/GroupProfileNameTextField.kt @@ -0,0 +1,97 @@ +package com.sopt.presentation.groupCreate.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.sopt.core.designsystem.theme.NoostakAndroidTheme +import com.sopt.core.designsystem.theme.NoostakTheme +import com.sopt.presentation.R + +@Composable +fun GroupProfileNameTextField( + value: String = "", + onValueChange: (String) -> Unit = { _ -> }, + placeholder: String = "", + placeholderColor: Color = NoostakTheme.colors.gray600, + textStyle: TextStyle = NoostakTheme.typography.b5Regular, + shape: Shape = RoundedCornerShape(6.dp), + focusedBorderColor: Color = NoostakTheme.colors.gray900, + unfocusedBorderColor: Color = NoostakTheme.colors.gray500, + maxLength: Int = 30, + modifier: Modifier = Modifier, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default +) { + Column { + OutlinedTextField( + value = value, + textStyle = textStyle, + placeholder = { Text(text = placeholder, color = placeholderColor, style = textStyle) }, + onValueChange = { newValue -> + if (newValue.replace(" ", "").length <= maxLength) onValueChange(newValue) + }, + shape = shape, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = focusedBorderColor, + unfocusedBorderColor = unfocusedBorderColor + ), + trailingIcon = { + if (value.isNotEmpty()) { + IconButton(onClick = { onValueChange("") }) { + Icon( + painter = painterResource(id = R.drawable.ic_group_create_delete), + contentDescription = stringResource(R.string.icon_group_profile_name_text_field_descrition), + tint = Color.Unspecified + ) + } + } + }, + modifier = modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions + ) + Text( + text = stringResource( + R.string.text_group_profile_name_textfield_count, + value.length, + maxLength + ), + color = NoostakTheme.colors.gray800, + style = NoostakTheme.typography.b5Regular, + modifier = modifier + .align(Alignment.End) + .padding(top = 6.dp), + maxLines = 1 + ) + } +} + +@Preview(showBackground = true) +@Composable +fun GroupProfileNameTextFieldPreview() { + NoostakAndroidTheme { + GroupProfileNameTextField(placeholder = "그룹명을 입력해주세요", value = "누스탁") + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/GroupCreateSuccessRoute.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/GroupCreateSuccessRoute.kt new file mode 100644 index 00000000..e8b7c7ad --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/GroupCreateSuccessRoute.kt @@ -0,0 +1,252 @@ +package com.sopt.presentation.groupCreate.groupCreateSuccess + +import android.annotation.SuppressLint +import android.content.Intent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.startActivity +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.flowWithLifecycle +import com.sopt.core.designsystem.component.button.NoostakBottomButton +import com.sopt.core.designsystem.component.snackbar.NoostakSnackBar +import com.sopt.core.designsystem.component.snackbar.SNACK_BAR_DURATION +import com.sopt.core.designsystem.component.topappbar.NoostakCloseAppBar +import com.sopt.core.designsystem.theme.NoostakAndroidTheme +import com.sopt.core.designsystem.theme.NoostakTheme +import com.sopt.core.extension.noRippleClickable +import com.sopt.presentation.R +import com.sopt.presentation.groupCreate.groupCreateSuccess.regex.generateRandomCode +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@Composable +fun GroupCreateSuccessRoute( + viewModel: GroupCreateSuccessViewModel = hiltViewModel(), + navigateToGroupDetail: (Long) -> Unit +) { + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + + val snackBarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + val snackBarVisible = remember { mutableStateOf(false) } + + val groupCode = generateRandomCode() + + val sendIntent = Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_TEXT, groupCode) + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, null) + + val onShowCopySnackBar: (message: String) -> Unit = { + coroutineScope.launch { + snackBarVisible.value = true + val job = launch { snackBarHostState.showSnackbar(message = it) } + delay(SNACK_BAR_DURATION) + job.cancel() + snackBarVisible.value = false + } + } + + LaunchedEffect(lifecycleOwner) { + viewModel.sideEffects.flowWithLifecycle(lifecycleOwner.lifecycle) + .collectLatest { sideEffect -> + when (sideEffect) { + is GroupCreateSuccessSideEffect.NavigateToGroupDetail -> navigateToGroupDetail( + sideEffect.groupId + ) + + is GroupCreateSuccessSideEffect.ShowSnackBar -> onShowCopySnackBar( + context.getString( + sideEffect.message + ) + ) + } + } + } + + GroupCreateSuccessScreen( + groupCode = groupCode, + snackBarHostState = snackBarHostState, + snackBarVisible = snackBarVisible, + onCloseBtnClick = viewModel::navigateToGroupDetail, + onCopyBtnClick = { + coroutineScope.launch { + viewModel.onCodeCopyBtnClick() + } + }, + onSendBtnClick = { + startActivity(context, shareIntent, null) + } + ) +} + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@Composable +fun GroupCreateSuccessScreen( + groupCode: String, + snackBarHostState: SnackbarHostState, + snackBarVisible: MutableState, + onCloseBtnClick: (Long) -> Unit, + onCopyBtnClick: () -> Unit, + onSendBtnClick: () -> Unit +) { + val clipboardManager = LocalClipboardManager.current + + Scaffold( + modifier = Modifier.statusBarsPadding(), + topBar = { + NoostakCloseAppBar( + modifier = Modifier, + onBackButtonClick = { + onCloseBtnClick( + // 임의 id - api 통신에서 변경해야 함 + 0 + ) + } + ) + }, + snackbarHost = { + AnimatedVisibility( + visible = snackBarVisible.value, + enter = slideInVertically(initialOffsetY = { it }), + exit = slideOutVertically(targetOffsetY = { it }) + ) { + SnackbarHost( + modifier = Modifier.padding(bottom = 78.dp), + hostState = snackBarHostState, + snackbar = { snackBarData -> + NoostakSnackBar( + message = snackBarData.visuals.message, + textStyle = NoostakTheme.typography.c3Regular, + textColor = NoostakTheme.colors.white, + backgroundColor = NoostakTheme.colors.gray900 + ) + } + ) + } + } + ) { innerPadding -> + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = dimensionResource(id = R.dimen.horizontal_padding)) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + Image( + painter = painterResource(id = R.drawable.ic_launcher_background), + contentDescription = stringResource(R.string.image_group_create_success_description), + modifier = Modifier + .padding(top = 56.dp) + .size(108.dp) + .aspectRatio(1f) + .align(Alignment.CenterHorizontally) + ) + Text( + text = stringResource(R.string.text_group_create_success_title), + color = NoostakTheme.colors.gray900, + style = NoostakTheme.typography.t1SemiBold, + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.CenterHorizontally) + ) + Text( + text = stringResource(R.string.text_group_create_success_content), + color = NoostakTheme.colors.gray900, + style = NoostakTheme.typography.b4Regular, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.CenterHorizontally) + ) + Text( + text = groupCode, + color = NoostakTheme.colors.gray800, + style = NoostakTheme.typography.codeMedium, + modifier = Modifier + .padding(top = 16.dp) + .align(Alignment.CenterHorizontally) + ) + } + Text( + text = stringResource(R.string.text_group_create_success_code_copy), + color = NoostakTheme.colors.gray800, + style = NoostakTheme.typography.c3Regular.copy( + textDecoration = TextDecoration.Underline + ), + modifier = Modifier + .padding(12.dp) + .noRippleClickable { + clipboardManager.setText(AnnotatedString(groupCode)) + onCopyBtnClick() + } + .align(Alignment.CenterHorizontally) + ) + NoostakBottomButton( + text = stringResource(R.string.btn_group_create_success_code_send), + activateColor = NoostakTheme.colors.blue600, + deactivateColor = NoostakTheme.colors.gray500, + isEnabled = true, + onButtonClick = { + onSendBtnClick() + } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun GroupCreateSuccessScreenPreview() { + NoostakAndroidTheme { + GroupCreateSuccessScreen( + groupCode = generateRandomCode(), + snackBarHostState = SnackbarHostState(), + snackBarVisible = remember { mutableStateOf(true) }, + onCloseBtnClick = {}, + onCopyBtnClick = {}, + onSendBtnClick = {} + ) + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/GroupCreateSuccessSideEffect.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/GroupCreateSuccessSideEffect.kt new file mode 100644 index 00000000..bd017c96 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/GroupCreateSuccessSideEffect.kt @@ -0,0 +1,6 @@ +package com.sopt.presentation.groupCreate.groupCreateSuccess + +sealed interface GroupCreateSuccessSideEffect { + data class NavigateToGroupDetail(val groupId: Long) : GroupCreateSuccessSideEffect + data class ShowSnackBar(val message: Int) : GroupCreateSuccessSideEffect +} diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/GroupCreateSuccessViewModel.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/GroupCreateSuccessViewModel.kt new file mode 100644 index 00000000..3c2749f3 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/GroupCreateSuccessViewModel.kt @@ -0,0 +1,18 @@ +package com.sopt.presentation.groupCreate.groupCreateSuccess + +import com.sopt.core.util.BaseViewModel +import com.sopt.presentation.R +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class GroupCreateSuccessViewModel @Inject constructor() : + BaseViewModel() { + fun navigateToGroupDetail(groupId: Long) { + emitSideEffect(GroupCreateSuccessSideEffect.NavigateToGroupDetail(groupId)) + } + + fun onCodeCopyBtnClick() { + emitSideEffect(GroupCreateSuccessSideEffect.ShowSnackBar(R.string.sb_group_create_success_code_copy)) + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/navigation/GroupCreateSuccessNavigation.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/navigation/GroupCreateSuccessNavigation.kt new file mode 100644 index 00000000..d982dfb2 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/navigation/GroupCreateSuccessNavigation.kt @@ -0,0 +1,36 @@ +package com.sopt.presentation.groupCreate.groupCreateSuccess.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.sopt.core.navigation.Route +import com.sopt.presentation.groupCreate.groupCreateSuccess.GroupCreateSuccessRoute +import com.sopt.presentation.groupCreate.navigation.GroupCreate +import com.sopt.presentation.groupDetail.navigation.navigateGroupDetail +import kotlinx.serialization.Serializable + +fun NavController.navigateToGroupCreateSuccess(navOptions: NavOptions? = null) { + navigate( + route = GroupCreateSuccess, + navOptions = navOptions ?: NavOptions.Builder() + .setPopUpTo(GroupCreate, inclusive = true) + .build() + ) +} + +fun NavGraphBuilder.groupCreateSuccessNavGraph( + navHostController: NavController +) { + composable { + GroupCreateSuccessRoute( + navigateToGroupDetail = { groupId -> + navHostController.popBackStack() + navHostController.navigateGroupDetail(groupId = groupId) + } + ) + } +} + +@Serializable +data object GroupCreateSuccess : Route diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/regex/generateRandomCode.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/regex/generateRandomCode.kt new file mode 100644 index 00000000..4431cdee --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/groupCreateSuccess/regex/generateRandomCode.kt @@ -0,0 +1,6 @@ +package com.sopt.presentation.groupCreate.groupCreateSuccess.regex + +fun generateRandomCode(): String { + val charset = ('0'..'9') + ('A'..'Z') + return List(6) { charset.random() }.joinToString("") +} diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/navigation/GroupCreateNavigation.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/navigation/GroupCreateNavigation.kt new file mode 100644 index 00000000..28b967f1 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/navigation/GroupCreateNavigation.kt @@ -0,0 +1,49 @@ +package com.sopt.presentation.groupCreate.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.sopt.core.navigation.Route +import com.sopt.presentation.groupCreate.GroupCreateRoute +import com.sopt.presentation.groupCreate.groupCreateSuccess.navigation.navigateToGroupCreateSuccess +import kotlinx.serialization.Serializable + +fun NavController.navigateToGroupCreate(navOptions: NavOptions? = null) { + navigate( + route = GroupCreate, + navOptions = navOptions + ) +} + +fun NavController.navigateToGroupEnter(navOptions: NavOptions? = null) { + navigate( + route = GroupEnter, + navOptions = navOptions + ) +} + +fun NavGraphBuilder.groupCreateNavGraph( + paddingValues: PaddingValues, + navHostController: NavController +) { + composable { + GroupCreateRoute( + paddingValues = paddingValues, + navigateToGroupCreateSuccess = { + navHostController.navigateToGroupCreateSuccess() + } + ) + } + + composable { + // 이동 코드 추가하기 + } +} + +@Serializable +data object GroupCreate : Route + +@Serializable +data object GroupEnter : Route diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/permission/launchImagePicker.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/permission/launchImagePicker.kt new file mode 100644 index 00000000..1cc35dab --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/permission/launchImagePicker.kt @@ -0,0 +1,20 @@ +package com.sopt.presentation.groupCreate.permission + +import android.content.Context +import android.os.Build +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts + +fun Context.launchImagePicker( + galleryLauncher: ActivityResultLauncher, + photoPickerLauncher: ActivityResultLauncher +) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + // API 33 미만: 갤러리 실행 + galleryLauncher.launch("image/*") + } else { + // API 33 이상: 포토피커 실행 (이미지만 선택) + photoPickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/groupCreate/permission/rememberGalleryLauncher.kt b/presentation/src/main/java/com/sopt/presentation/groupCreate/permission/rememberGalleryLauncher.kt new file mode 100644 index 00000000..fc4a53ea --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/groupCreate/permission/rememberGalleryLauncher.kt @@ -0,0 +1,23 @@ +package com.sopt.presentation.groupCreate.permission + +import android.net.Uri +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable + +@Composable +fun rememberGalleryLauncher(onImageSelected: (Uri) -> Unit): ActivityResultLauncher { + return rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> + uri?.let(onImageSelected) + } +} + +@Composable +fun rememberPhotoPickerLauncher(onImageSelected: (Uri) -> Unit): ManagedActivityResultLauncher { + return rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? -> + uri?.let(onImageSelected) + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/main/MainScreen.kt b/presentation/src/main/java/com/sopt/presentation/main/MainScreen.kt index 456df371..6b8d6dfd 100644 --- a/presentation/src/main/java/com/sopt/presentation/main/MainScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/main/MainScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.compose.NavHost import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.sopt.core.designsystem.component.snackbar.BaseSnackBar +import com.sopt.core.designsystem.component.snackbar.NoostakSnackBar import com.sopt.core.util.NoRippleInteractionSource import com.sopt.presentation.auth.login.navigation.loginNavGraph import com.sopt.presentation.auth.signup.checkInvite.navigation.checkInviteNavGraph @@ -45,6 +45,8 @@ import com.sopt.presentation.auth.signup.navigation.signUpNavGraph import com.sopt.presentation.calendar.navigation.calendarNavGraph import com.sopt.presentation.example.navigation.exampleNavGraph import com.sopt.presentation.group.navigation.groupNavGraph +import com.sopt.presentation.groupCreate.groupCreateSuccess.navigation.groupCreateSuccessNavGraph +import com.sopt.presentation.groupCreate.navigation.groupCreateNavGraph import com.sopt.presentation.groupDetail.navigation.groupDetailNavGraph import com.sopt.presentation.mypage.navigation.myPageNavGraph import kotlinx.coroutines.launch @@ -96,11 +98,7 @@ fun MainScreen( hostState = snackBarHostState, modifier = Modifier.padding(bottom = 10.dp) ) { snackBarData -> - BaseSnackBar { - Text( - text = snackBarData.visuals.message - ) - } + NoostakSnackBar(message = snackBarData.visuals.message) } }, bottomBar = { @@ -137,10 +135,12 @@ fun MainScreen( paddingValues = paddingValues, navHostController = navigator.navController ) - groupNavGraph( + groupNavGraph(navHostController = navigator.navController) + groupCreateNavGraph( paddingValues = paddingValues, navHostController = navigator.navController ) + groupCreateSuccessNavGraph(navHostController = navigator.navController) groupDetailNavGraph(navHostController = navigator.navController) myPageNavGraph( paddingValues = paddingValues, diff --git a/presentation/src/main/res/drawable/ic_group_create_camera.xml b/presentation/src/main/res/drawable/ic_group_create_camera.xml new file mode 100644 index 00000000..d0a337b3 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_group_create_camera.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_group_create_delete.xml b/presentation/src/main/res/drawable/ic_group_create_delete.xml new file mode 100644 index 00000000..592d9709 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_group_create_delete.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_group_create_profile.xml b/presentation/src/main/res/drawable/ic_group_create_profile.xml new file mode 100644 index 00000000..00b13e70 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_group_create_profile.xml @@ -0,0 +1,12 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_group_fab_close.xml b/presentation/src/main/res/drawable/ic_group_fab_close.xml new file mode 100644 index 00000000..0b7f6560 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_group_fab_close.xml @@ -0,0 +1,18 @@ + + + + diff --git a/presentation/src/main/res/values/dimens.xml b/presentation/src/main/res/values/dimens.xml index d3872931..d0567b6d 100644 --- a/presentation/src/main/res/values/dimens.xml +++ b/presentation/src/main/res/values/dimens.xml @@ -1,4 +1,6 @@ 16dp + + 10dp \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 3da10858..534e2636 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -56,4 +56,31 @@ 방장 멤버 + + 그룹추가 + 아직 생성된 그룹이 없어요.\n그룹을 추가해보세요! + 그룹 입장하기 + Image Group Create Success + + + 그룹 만들기 + 다음 + 그룹명을 입력해주세요 + + + 그룹 만들기가 완료되었습니다 + 그룹코드를 복사해\n그룹원을 초대해주세요! + 그룹 코드 복사하기 + 그룹 코드 보내기 + 그룹 코드가 복사되었습니다 + + + %1$s/%2$s + + + Add Profile Image By Camera Button + + + Delete Group Profile Name + \ No newline at end of file