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