diff --git a/common/resource/src/main/res/drawable/img_friend_accept.xml b/common/resource/src/main/res/drawable/img_friend_accept.xml new file mode 100644 index 0000000..eb015b3 --- /dev/null +++ b/common/resource/src/main/res/drawable/img_friend_accept.xml @@ -0,0 +1,12 @@ + + + + diff --git a/common/resource/src/main/res/drawable/img_friend_empty_plus.xml b/common/resource/src/main/res/drawable/img_friend_empty_plus.xml new file mode 100644 index 0000000..86f5a4e --- /dev/null +++ b/common/resource/src/main/res/drawable/img_friend_empty_plus.xml @@ -0,0 +1,13 @@ + + + diff --git a/common/resource/src/main/res/drawable/img_friend_reject.xml b/common/resource/src/main/res/drawable/img_friend_reject.xml new file mode 100644 index 0000000..dfc2b8d --- /dev/null +++ b/common/resource/src/main/res/drawable/img_friend_reject.xml @@ -0,0 +1,12 @@ + + + + diff --git a/feature/friend/.gitignore b/feature/friend/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/feature/friend/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/friend/build.gradle.kts b/feature/friend/build.gradle.kts new file mode 100644 index 0000000..3e4de1e --- /dev/null +++ b/feature/friend/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("convention.android.feature") +} + +android { + namespace = "com.idiotfrogs.friend" +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.ext) + androidTestImplementation(libs.androidx.test.espresso) +} \ No newline at end of file diff --git a/feature/friend/consumer-rules.pro b/feature/friend/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/feature/friend/proguard-rules.pro b/feature/friend/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/feature/friend/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/friend/src/androidTest/java/com/idiotfrogs/friend/ExampleInstrumentedTest.kt b/feature/friend/src/androidTest/java/com/idiotfrogs/friend/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..5370bc9 --- /dev/null +++ b/feature/friend/src/androidTest/java/com/idiotfrogs/friend/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.idiotfrogs.friend + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.idiotfrogs.friend.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/friend/src/main/AndroidManifest.xml b/feature/friend/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/feature/friend/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/friend/src/main/java/com/idiotfrogs/friend/FriendScreen.kt b/feature/friend/src/main/java/com/idiotfrogs/friend/FriendScreen.kt new file mode 100644 index 0000000..e8c290e --- /dev/null +++ b/feature/friend/src/main/java/com/idiotfrogs/friend/FriendScreen.kt @@ -0,0 +1,184 @@ +package com.idiotfrogs.friend + +import android.content.Intent +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.core.content.FileProvider +import com.idiotfrogs.designsystem.component.MSDetailHeader +import com.idiotfrogs.designsystem.component.MSMenuFab +import com.idiotfrogs.designsystem.component.MSText +import com.idiotfrogs.designsystem.model.MSMenuFabModel +import com.idiotfrogs.designsystem.theme.MSTheme +import com.idiotfrogs.designsystem.util.noRippleClickable +import com.idiotfrogs.friend.component.FriendListItem +import com.idiotfrogs.friend.component.FriendTopNotification +import com.idiotfrogs.resource.R +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun FriendScreen( + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + var isEmpty by remember { mutableStateOf(false) } + var action by remember { mutableStateOf(FriendScreenActionState.IDLE) } + var expanded by remember { mutableStateOf(false) } + val menuList by remember { + mutableStateOf( + listOf( + MSMenuFabModel("참여 링크 공유") { + expanded = false + +// val imageUri = FileProvider.getUriForFile( +// context, +// "${context.packageName}.provider", // provider authority +// imageFile // File 객체 +// ) + + // 공유 인텐트 생성 + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "image/*" +// putExtra(Intent.EXTRA_STREAM, imageUri) + putExtra( + Intent.EXTRA_TEXT, + "내가 만든 타임 티켓에 함께해줘! 아래 링크로 참여 요청을 보내면 “타임 캡슐 이름”에 합류할 수 있어요. [초대 링크]" + ) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + // 시스템 공유 바텀 시트 표시 + context.startActivity( + Intent.createChooser(shareIntent, "공유하기") + ) + }, + MSMenuFabModel("참여 코드 복사") { + expanded = false + action = FriendScreenActionState.COPY + }, + ) + ) + } + + LaunchedEffect(action) { + delay(1000L) + action = FriendScreenActionState.IDLE + } + + Box { + MSMenuFab( + modifier = Modifier + .align(Alignment.TopEnd) + .navigationBarsPadding() + .padding(end = 20.dp), + expanded = expanded, + hasFab = false, + offset = DpOffset(x = 0.dp, y = 40.dp), + menuList = menuList, + onClick = { expanded = !expanded }, + onDismiss = { expanded = false }, + ) + + if (action != FriendScreenActionState.IDLE) { + FriendTopNotification( + modifier = Modifier + .padding(horizontal = 20.dp) + .align(Alignment.TopCenter) + .navigationBarsPadding() + .zIndex(1f), + action = action, + ) + } + + Column( + modifier = modifier + .fillMaxSize() + .background(MSTheme.color.white) + .systemBarsPadding() + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + MSDetailHeader( + title = "맴버 추가", + navigateToBack = {}, + paddingValues = PaddingValues(horizontal = 0.dp, vertical = 16.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_plus), + contentDescription = "Back", + modifier = Modifier.noRippleClickable { expanded = true } + ) + } + if (isEmpty) { + Spacer(Modifier.height(25.dp)) + Icon( + painter = painterResource(R.drawable.img_friend_empty_plus), + contentDescription = "emptyIcon", + tint = MSTheme.color.greyG1, + modifier = Modifier + .align(Alignment.End) + .padding(end = 40.dp) + ) + Spacer(Modifier.height(32.dp)) + MSText( + text = "대기중인 맴버가 없습니다.\n" + "코드 또는 링크로 맴버를 초대해보세요.", + color = MSTheme.color.greyG4, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Center + ) + } else { + repeat(10) { // TODO 테스트용 코드 -> 추 후 실제 list 변경 필요 + Spacer(Modifier.height(8.dp)) + FriendListItem( + nickName = when (it) { + 0 -> "파란 바나나" + 1 -> "검정 복숭아" + 2 -> "별 모양 파인애플" + 3 -> "초코 체리" + 4 -> "자두 수박" + else -> "민트 네모 수박" + }, + onAccept = { action = FriendScreenActionState.ACCEPT }, + onReject = { action = FriendScreenActionState.REJECT } + ) + Spacer(Modifier.height(8.dp)) + } + } + } + } +} + +@Preview +@Composable +fun FriendScreenPreview() { + FriendScreen() +} \ No newline at end of file diff --git a/feature/friend/src/main/java/com/idiotfrogs/friend/FriendScreenActionState.kt b/feature/friend/src/main/java/com/idiotfrogs/friend/FriendScreenActionState.kt new file mode 100644 index 0000000..def6476 --- /dev/null +++ b/feature/friend/src/main/java/com/idiotfrogs/friend/FriendScreenActionState.kt @@ -0,0 +1,5 @@ +package com.idiotfrogs.friend + +enum class FriendScreenActionState { + IDLE, ACCEPT, REJECT, COPY +} \ No newline at end of file diff --git a/feature/friend/src/main/java/com/idiotfrogs/friend/component/FriendListItem.kt b/feature/friend/src/main/java/com/idiotfrogs/friend/component/FriendListItem.kt new file mode 100644 index 0000000..993ce66 --- /dev/null +++ b/feature/friend/src/main/java/com/idiotfrogs/friend/component/FriendListItem.kt @@ -0,0 +1,85 @@ +package com.idiotfrogs.friend.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +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.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.idiotfrogs.designsystem.component.MSText +import com.idiotfrogs.designsystem.theme.MSTheme +import com.idiotfrogs.designsystem.util.noRippleClickable +import com.idiotfrogs.resource.R + +@Composable +fun FriendListItem( + nickName: String, + onAccept: () -> Unit, + onReject: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Image( // TODO 추 후 AsyncImage 변경 필요 + painter = painterResource(R.drawable.img_profile), + contentDescription = "프로필", + modifier = Modifier.size(48.dp) + ) + Spacer(Modifier.width(8.dp)) + MSText( + text = nickName, + color = MSTheme.color.greyG5, + fontSize = 16.dp, + fontWeight = FontWeight.Normal, + ) + + Spacer(Modifier.weight(1f)) + MSText( + modifier = Modifier + .background( + color = MSTheme.color.greyG1, + shape = RoundedCornerShape(8.dp) + ) + .padding(horizontal = 12.dp, vertical = 7.5.dp) + .noRippleClickable { onReject() }, + text = "거절", + color = MSTheme.color.greyG4, + fontSize = 14.dp, + ) + Spacer(Modifier.width(8.dp)) + MSText( + modifier = Modifier + .background( + color = MSTheme.color.primaryLight, + shape = RoundedCornerShape(8.dp) + ) + .padding(horizontal = 12.dp, vertical = 7.5.dp) + .noRippleClickable { onAccept() }, + text = "수락", + color = MSTheme.color.primaryDark, + fontSize = 14.dp, + ) + } +} + +@Preview(showBackground = true) +@Composable +fun FriendListItemPreview() { + FriendListItem( + "nickName", + onAccept = {}, + onReject = {}, + ) +} \ No newline at end of file diff --git a/feature/friend/src/main/java/com/idiotfrogs/friend/component/FriendTopNotification.kt b/feature/friend/src/main/java/com/idiotfrogs/friend/component/FriendTopNotification.kt new file mode 100644 index 0000000..a800ab9 --- /dev/null +++ b/feature/friend/src/main/java/com/idiotfrogs/friend/component/FriendTopNotification.kt @@ -0,0 +1,69 @@ +package com.idiotfrogs.friend.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +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.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.idiotfrogs.designsystem.component.MSText +import com.idiotfrogs.designsystem.theme.MSTheme +import com.idiotfrogs.friend.FriendScreenActionState +import com.idiotfrogs.resource.R + +@Composable +fun FriendTopNotification( + action: FriendScreenActionState, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .shadow( + elevation = 8.dp, + shape = CircleShape, + ambientColor = Color(0x50505029), + spotColor = Color(0x50505029) + ) + .background( + color = MSTheme.color.white, + shape = CircleShape + ) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource( + if (action == FriendScreenActionState.ACCEPT || action == FriendScreenActionState.COPY) R.drawable.img_friend_accept + else R.drawable.img_friend_reject + ), + contentDescription = "알림", + modifier = Modifier.size(24.dp) + ) + Spacer(Modifier.width(8.dp)) + MSText( + text = when (action) { + FriendScreenActionState.ACCEPT -> "참여 요청이 수락되었습니다." + FriendScreenActionState.COPY -> "참여 코드 복사되었습니다." + else -> "참여 요청이 거절되었습니다." + } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun FriendTopNotificationPreview() { + FriendTopNotification(FriendScreenActionState.IDLE) +} \ No newline at end of file diff --git a/feature/friend/src/test/java/com/idiotfrogs/friend/ExampleUnitTest.kt b/feature/friend/src/test/java/com/idiotfrogs/friend/ExampleUnitTest.kt new file mode 100644 index 0000000..d68eba0 --- /dev/null +++ b/feature/friend/src/test/java/com/idiotfrogs/friend/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.idiotfrogs.friend + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d22471..1d85600 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ profileinstaller = "1.4.1" # 구글 로그인 androidxCredentials = "1.3.0" identityGoogleId = "1.1.1" +material = "1.10.0" [libraries] android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } @@ -82,6 +83,7 @@ identity-google-id = { group = "com.google.android.libraries.identity.googleid", # 애플 로그인 firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } firebase-auth = { group = "com.google.firebase", name = "firebase-auth" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 04cb7a2..923700e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,3 +43,4 @@ include(":feature:create") include(":feature:setting") include(":widget") include(":baselineprofile") +include(":feature:friend")