From 9a8d4918a551b367b0b11b6ebbce5ad3eb356707 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Wed, 21 Feb 2024 01:02:22 +0900 Subject: [PATCH 001/129] =?UTF-8?q?Boolti-156=20fix:=20QR=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- domain/src/main/java/com/nexters/boolti/domain/model/Ticket.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/domain/src/main/java/com/nexters/boolti/domain/model/Ticket.kt b/domain/src/main/java/com/nexters/boolti/domain/model/Ticket.kt index 4d33287a..3f5ce19c 100644 --- a/domain/src/main/java/com/nexters/boolti/domain/model/Ticket.kt +++ b/domain/src/main/java/com/nexters/boolti/domain/model/Ticket.kt @@ -48,9 +48,8 @@ data class Ticket( get() = run { val now = LocalDateTime.now() when { + usedAt != null && now > usedAt -> TicketState.Used now.toLocalDate() > showDate.toLocalDate() -> TicketState.Finished - usedAt == null -> TicketState.Ready - now > usedAt -> TicketState.Used else -> TicketState.Ready } } From 985d59bcd84c7b9cf74456cecc09b8547594069e Mon Sep 17 00:00:00 2001 From: algosketch Date: Thu, 22 Feb 2024 01:43:22 +0900 Subject: [PATCH 002/129] =?UTF-8?q?feat=20:=20message=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EB=B0=8F=20=EC=95=8C=EB=A6=BC=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 4 ++++ gradle/libs.versions.toml | 3 ++- presentation/build.gradle.kts | 2 ++ .../boolti/presentation/screen/MainActivity.kt | 12 +++++++++++- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3547adb..04512855 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -58,5 +58,9 @@ android:scheme="kakao${KAKAO_APP_KEY}" /> + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5636d4f..de18cf43 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -100,6 +100,7 @@ retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.require = "false" } firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.require = "false" } +firebase-messaging-ktx = { module = "com.google.firebase:firebase-messaging-ktx", version.require = "false" } firebase-config-ktx = { module = "com.google.firebase:firebase-config-ktx", version.ref = "firebase-config" } zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing" } kakao-login = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao" } @@ -124,7 +125,7 @@ lifecycle = ["androidx-lifecycle-runtime-ktx", "androidx-lifecycle-viewmodel-ktx android-test = ["junit", "androidx-junit", "androidx-espresso-core", "kotlinx-coroutines-test"] network = ["retrofit", "logging-interceptor"] db = ["androidx-datastore", "androidx-datastore-preferences-core", "androidx-room-runtime", "androidx-room-ktx"] -firebase = ["firebase-analytics-ktx", "firebase-crashlytics-ktx"] +firebase = ["firebase-analytics-ktx", "firebase-crashlytics-ktx", "firebase-messaging-ktx"] kotest = ["kotest-assertions-core", "kotest-property", "kotest-runner-junit5"] coil = ["coil", "coil-compose"] compose = ["androidx-activity-compose", "androidx-navigation-compose", "androidx-material3-android", "androidx-compose-ui-ui", "androidx-compose-ui-ui-graphics", "androidx-compose-ui-tooling-preview", "androidx-compose-ui-ui-util", "androidx-lifecycle-runtime-compose"] \ No newline at end of file diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index ff190d69..20dec306 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -59,6 +59,8 @@ dependencies { implementation(libs.bundles.compose) implementation(platform(libs.andoridx.compose.compose.bom)) implementation(libs.bundles.coroutines) + implementation(libs.bundles.firebase) + implementation(platform(libs.firebase.bom)) implementation(libs.hilt.android) implementation(libs.androidx.hilt.navigation.compose) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt index 8569df68..aedafb56 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt @@ -1,5 +1,7 @@ package com.nexters.boolti.presentation.screen +import android.Manifest +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -8,6 +10,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import com.nexters.boolti.presentation.QrScanActivity +import com.nexters.boolti.presentation.extension.requestPermission import com.nexters.boolti.presentation.extension.startActivity import com.nexters.boolti.presentation.theme.BooltiTheme import dagger.hilt.android.AndroidEntryPoint @@ -18,7 +21,10 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { BooltiTheme { - Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { Main( onClickQrScan = { showId, showName -> startActivity { @@ -30,5 +36,9 @@ class MainActivity : ComponentActivity() { } } } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermission(Manifest.permission.POST_NOTIFICATIONS, 101) + } } } From c8d7c191dd1a354020bb05320adf5d7eec5e3146 Mon Sep 17 00:00:00 2001 From: algosketch Date: Thu, 22 Feb 2024 04:39:33 +0900 Subject: [PATCH 003/129] =?UTF-8?q?refactor=20:=20=EB=AF=B8=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/show/ShowScreen.kt | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt index 51a76114..2c864827 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt @@ -81,7 +81,6 @@ fun ShowScreen( val changeableAppBarHeightPx = with(LocalDensity.current) { (appbarHeight - searchBarHeight).roundToPx().toFloat() } var appbarOffsetHeightPx by rememberSaveable { mutableFloatStateOf(0f) } - var changeableAppBarHeight by remember { mutableFloatStateOf(0f) } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { @@ -138,18 +137,12 @@ fun ShowScreen( nickname = nickname.ifBlank { stringResource(id = R.string.nickname_default) }, text = uiState.keyword, onKeywordChanged = viewModel::updateKeyword, - onChangeableSizeChanged = { size -> - changeableAppBarHeight = size.height.toFloat() - }, search = viewModel::search, ) } } } -/** - * @param onChangeableSizeChanged 변할 수 있는 최대 사이즈를 전달 app bar height - search bar - */ @Composable fun ShowAppBar( text: String, @@ -157,20 +150,13 @@ fun ShowAppBar( navigateToReservations: () -> Unit, nickname: String, onKeywordChanged: (keyword: String) -> Unit, - onChangeableSizeChanged: (size: IntSize) -> Unit, search: () -> Unit, modifier: Modifier = Modifier, ) { - var appBarHeight by remember { mutableFloatStateOf(0f) } - val searchBarHeight = with(LocalDensity.current) { 80.dp.toPx() } Column( modifier = modifier .fillMaxWidth() .padding(horizontal = marginHorizontal) - .onSizeChanged(onSizeChanged = { size -> - appBarHeight = size.height.toFloat() - onChangeableSizeChanged(IntSize(0, size.height - searchBarHeight.toInt())) - }) ) { Spacer(modifier = Modifier.height(20.dp)) if (hasPendingTicket) Banner( From e5e0ce839bfe989b803c72ac64959329e978fd34 Mon Sep 17 00:00:00 2001 From: algosketch Date: Thu, 22 Feb 2024 05:25:38 +0900 Subject: [PATCH 004/129] =?UTF-8?q?fix=20:=20=EC=8A=A4=ED=81=AC=EB=A1=A4?= =?UTF-8?q?=20=EC=9E=94=EB=9F=89=EB=A7=8C=ED=81=BC=20offset=20=EB=A1=A4?= =?UTF-8?q?=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/show/ShowScreen.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt index 2c864827..7eb5070f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions @@ -59,6 +60,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.ShowFeed +import com.nexters.boolti.presentation.extension.toPx import com.nexters.boolti.presentation.theme.Grey15 import com.nexters.boolti.presentation.theme.Grey60 import com.nexters.boolti.presentation.theme.Grey70 @@ -76,10 +78,11 @@ fun ShowScreen( val user by viewModel.user.collectAsStateWithLifecycle() val nickname = user?.nickname ?: "" val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + val lazyGridState = rememberLazyGridState() val appbarHeight = if (uiState.hasPendingTicket) 196.dp + 52.dp else 196.dp val searchBarHeight = 80.dp - val changeableAppBarHeightPx = - with(LocalDensity.current) { (appbarHeight - searchBarHeight).roundToPx().toFloat() } + val changeableAppBarHeightPx = (appbarHeight - searchBarHeight).toPx() var appbarOffsetHeightPx by rememberSaveable { mutableFloatStateOf(0f) } val nestedScrollConnection = remember { object : NestedScrollConnection { @@ -88,6 +91,15 @@ fun ShowScreen( return Offset.Zero } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + appbarOffsetHeightPx -= available.y + return super.onPostScroll(consumed, available, source) + } } } @@ -110,6 +122,7 @@ fun ShowScreen( LazyVerticalGrid( modifier = Modifier .padding(horizontal = marginHorizontal), + state = lazyGridState, columns = GridCells.Adaptive(minSize = 150.dp), horizontalArrangement = Arrangement.spacedBy(15.dp), verticalArrangement = Arrangement.spacedBy(28.dp), @@ -129,7 +142,7 @@ fun ShowScreen( modifier = Modifier.offset { IntOffset( x = 0, - y = appbarOffsetHeightPx.coerceIn(-changeableAppBarHeightPx, 0f).toInt() + y = appbarOffsetHeightPx.coerceAtLeast(-changeableAppBarHeightPx).toInt(), ) }, navigateToReservations = navigateToReservations, From ee95f2568c2d9e43ceabb6a5e25c4647f6ba57c3 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Fri, 23 Feb 2024 00:34:45 +0900 Subject: [PATCH 005/129] =?UTF-8?q?Boolti-155=20style:=20QR=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 19 +++-- .../presentation/screen/home/HomeScreen.kt | 2 +- .../presentation/screen/qr/QrFullScreen.kt | 70 ++++++++++++------- .../presentation/screen/qr/QrFullViewModel.kt | 1 + .../screen/ticket/TicketContent.kt | 4 +- .../screen/ticket/TicketScreen.kt | 4 +- .../ticket/detail/TicketDetailScreen.kt | 10 ++- .../src/main/res/drawable/ic_logo_boolti.xml | 60 +++++++--------- 8 files changed, 99 insertions(+), 71 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 91fb77cd..fcbc5436 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -63,8 +63,10 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: onClickTicket = { navController.navigate("tickets/$it") }, - onClickQr = { - navController.navigate("qr/${it.filter { c -> c.isLetterOrDigit() }}") + onClickQr = { code, ticketName -> + navController.navigate( + "qr/${code.filter { c -> c.isLetterOrDigit() }}?ticketName=$ticketName" + ) }, onClickQrScan = { navController.navigate("hostedShows") @@ -189,7 +191,11 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: ) { TicketDetailScreen(modifier = modifier, onBackClicked = { navController.popBackStack() }, - onClickQr = { navController.navigate("qr/${it.filter { c -> c.isLetterOrDigit() }}") }, + onClickQr = { code, ticketName -> + navController.navigate( + "qr/${code.filter { c -> c.isLetterOrDigit() }}?ticketName=$ticketName" + ) + }, navigateToShowDetail = { navController.navigate("show/$it") } ) } @@ -212,8 +218,11 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: } composable( - route = "qr/{data}", - arguments = listOf(navArgument("data") { type = NavType.StringType }), + route = "qr/{data}?ticketName={ticketName}", + arguments = listOf( + navArgument("data") { type = NavType.StringType }, + navArgument("ticketName") { type = NavType.StringType }, + ), ) { QrFullScreen(modifier = modifier) { navController.popBackStack() diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt index cc41e6c9..77865400 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt @@ -43,7 +43,7 @@ fun HomeScreen( viewModel: HomeViewModel = hiltViewModel(), onClickShowItem: (showId: String) -> Unit, onClickTicket: (ticketId: String) -> Unit, - onClickQr: (data: String) -> Unit, + onClickQr: (data: String, ticketName: String) -> Unit, onClickQrScan: () -> Unit, navigateToReservations: () -> Unit, requireLogin: () -> Unit, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt index 8111991d..c64ede2e 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt @@ -2,13 +2,19 @@ package com.nexters.boolti.presentation.screen.qr import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text 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.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource @@ -17,6 +23,8 @@ import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.hilt.navigation.compose.hiltViewModel import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.theme.Grey10 +import com.nexters.boolti.presentation.theme.Grey85 import com.nexters.boolti.presentation.theme.Grey90 import com.nexters.boolti.presentation.util.rememberQrBitmapPainter @@ -31,7 +39,7 @@ fun QrFullScreen( .background(Color.White) .fillMaxSize() ) { - val (closeButton, logo, qr) = createRefs() + val (closeButton, qr) = createRefs() IconButton( onClick = onClose, @@ -48,31 +56,45 @@ fun QrFullScreen( ) } - Image( - modifier = Modifier - .constrainAs(logo) { - centerHorizontallyTo(parent) - bottom.linkTo(qr.top, margin = 16.dp) - } - .width(84.dp), - painter = painterResource(R.drawable.ic_logo_boolti), - contentScale = ContentScale.FillWidth, - contentDescription = null, - ) - Image( + Column( modifier = Modifier .constrainAs(qr) { - centerVerticallyTo(parent) - centerHorizontallyTo(parent) + centerTo(parent) } - .background(Color.White) - .padding(8.dp), - painter = rememberQrBitmapPainter( - viewModel.data, - size = 260.dp, - ), - contentScale = ContentScale.Inside, - contentDescription = stringResource(R.string.description_qr), - ) + .background( + color = Grey10, + shape = RoundedCornerShape(8.dp), + ) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(top = 16.dp), + text = viewModel.ticketName, + style = MaterialTheme.typography.titleMedium, + color = Grey85.copy(alpha = .85f), + ) + Image( + modifier = Modifier + .padding(vertical = 12.dp) + .clip(RoundedCornerShape(4.dp)) + .background(Color.White) + .padding(14.dp), + painter = rememberQrBitmapPainter( + viewModel.data, + size = 260.dp, + ), + contentScale = ContentScale.Inside, + contentDescription = stringResource(R.string.description_qr), + ) + Image( + modifier = Modifier + .width(84.dp) + .padding(bottom = 12.dp), + painter = painterResource(R.drawable.ic_logo_boolti), + contentScale = ContentScale.FillWidth, + contentDescription = null, + ) + } } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullViewModel.kt index 597a92c3..b84f0df3 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullViewModel.kt @@ -9,6 +9,7 @@ import javax.inject.Inject class QrFullViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel() { + val ticketName: String = savedStateHandle["ticketName"] ?: "" val data: String = requireNotNull(savedStateHandle["data"]) { "QrFullViewModel 에 data 가 전달되지 않았습니다" } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt index 06661312..3fa8cd1d 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt @@ -65,7 +65,7 @@ import java.time.LocalDateTime @Composable fun TicketContent( ticket: Ticket, - onClickQr: (data: String) -> Unit, + onClickQr: (data: String, ticketName: String) -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -155,7 +155,7 @@ fun TicketContent( placeName = ticket.placeName, entryCode = ticket.entryCode, ticketState = ticket.ticketState, - onClickQr = onClickQr, + onClickQr = { onClickQr(it, ticket.ticketName) }, ) } // 티켓 좌상단 꼭지점 그라데이션 diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketScreen.kt index ea8d313e..b5afaefc 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketScreen.kt @@ -32,7 +32,7 @@ import kotlin.math.absoluteValue @Composable fun TicketScreen( onClickTicket: (String) -> Unit, - onClickQr: (entryCode: String) -> Unit, + onClickQr: (entryCode: String, ticketName: String) -> Unit, modifier: Modifier = Modifier, viewModel: TicketViewModel = hiltViewModel(), ) { @@ -55,7 +55,7 @@ fun TicketScreen( private fun TicketNotEmptyScreen( modifier: Modifier, uiState: TicketUiState, - onClickQr: (entryCode: String) -> Unit, + onClickQr: (entryCode: String, ticketName: String) -> Unit, onClickTicket: (ticketId: String) -> Unit, ) { Column( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt index e4a5943b..16580443 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt @@ -110,7 +110,7 @@ fun TicketDetailScreen( modifier: Modifier = Modifier, viewModel: TicketDetailViewModel = hiltViewModel(), onBackClicked: () -> Unit, - onClickQr: (entryCode: String) -> Unit, + onClickQr: (entryCode: String, ticketName: String) -> Unit, navigateToShowDetail: (showId: String) -> Unit, ) { val scrollState = rememberScrollState() @@ -246,7 +246,7 @@ fun TicketDetailScreen( placeName = ticket.placeName, entryCode = ticket.entryCode, ticketState = ticket.ticketState, - onClickQr = onClickQr, + onClickQr = { onClickQr(it, ticket.ticketName) }, ) } // 티켓 좌상단 꼭지점 그라데이션 @@ -695,7 +695,11 @@ private fun SectionTitle(title: String) { fun TicketDetailPreview() { BooltiTheme { Surface { - TicketDetailScreen(modifier = Modifier, onBackClicked = {}, onClickQr = {}, navigateToShowDetail = {}) + TicketDetailScreen( + modifier = Modifier, + onBackClicked = {}, + onClickQr = { _, _ -> }, + navigateToShowDetail = {}) } } } diff --git a/presentation/src/main/res/drawable/ic_logo_boolti.xml b/presentation/src/main/res/drawable/ic_logo_boolti.xml index fb70f7cc..c9ef8e49 100644 --- a/presentation/src/main/res/drawable/ic_logo_boolti.xml +++ b/presentation/src/main/res/drawable/ic_logo_boolti.xml @@ -1,36 +1,28 @@ - - - - - - - - - - - - + android:width="71dp" + android:height="26dp" + android:viewportWidth="71" + android:viewportHeight="26"> + + + + + + From 5676500c1658c738afc74f961f9502eb31f98bf3 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Fri, 23 Feb 2024 01:46:50 +0900 Subject: [PATCH 006/129] =?UTF-8?q?Boolti-162=20style:=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 12 ++++ .../presentation/screen/home/HomeScreen.kt | 2 + .../boolti/presentation/screen/my/MyScreen.kt | 50 +------------- .../screen/signout/SignoutEvent.kt | 5 ++ .../screen/signout/SignoutNotice.kt | 54 +++++++++++++++ .../screen/signout/SignoutReason.kt | 49 ++++++++++++++ .../screen/signout/SignoutScreen.kt | 66 +++++++++++++++++++ .../screen/signout/SignoutViewModel.kt | 38 +++++++++++ presentation/src/main/res/values/strings.xml | 12 +++- 9 files changed, 239 insertions(+), 49 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutEvent.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNotice.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutReason.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutViewModel.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 91fb77cd..8cf086ce 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -29,6 +29,7 @@ import com.nexters.boolti.presentation.screen.show.ShowDetailContentScreen import com.nexters.boolti.presentation.screen.show.ShowDetailScreen import com.nexters.boolti.presentation.screen.show.ShowDetailViewModel import com.nexters.boolti.presentation.screen.show.ShowImagesScreen +import com.nexters.boolti.presentation.screen.signout.SignoutScreen import com.nexters.boolti.presentation.screen.ticket.detail.TicketDetailScreen import com.nexters.boolti.presentation.screen.ticketing.TicketingScreen import com.nexters.boolti.presentation.theme.BooltiTheme @@ -69,6 +70,9 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: onClickQrScan = { navController.navigate("hostedShows") }, + onClickSignout = { + navController.navigate("signout") + }, navigateToReservations = { navController.navigate("reservations") } @@ -86,6 +90,14 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: navController.popBackStack() } } + composable( + route = "signout", + ) { + SignoutScreen( + navigateToHome = { navController.navigateToHome() }, + navigateBack = { navController.popBackStack() }, + ) + } composable( route = "reservations", diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt index cc41e6c9..5df06220 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt @@ -45,6 +45,7 @@ fun HomeScreen( onClickTicket: (ticketId: String) -> Unit, onClickQr: (data: String) -> Unit, onClickQrScan: () -> Unit, + onClickSignout: () -> Unit, navigateToReservations: () -> Unit, requireLogin: () -> Unit, ) { @@ -104,6 +105,7 @@ fun HomeScreen( requireLogin = requireLogin, navigateToReservations = navigateToReservations, onClickQrScan = onClickQrScan, + onClickSignout = onClickSignout, ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt index 814567a8..580c66cc 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt @@ -1,6 +1,5 @@ package com.nexters.boolti.presentation.screen.my -import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -26,7 +25,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -48,12 +46,11 @@ fun MyScreen( requireLogin: () -> Unit, navigateToReservations: () -> Unit, onClickQrScan: () -> Unit, + onClickSignout: () -> Unit, modifier: Modifier = Modifier, viewModel: MyViewModel = hiltViewModel(), ) { val user by viewModel.user.collectAsStateWithLifecycle() - var showSignoutDialog by remember { mutableStateOf(false) } - val context = LocalContext.current var openLogoutDialog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { @@ -87,17 +84,7 @@ fun MyScreen( } Spacer(modifier = Modifier.weight(1.0f)) - if (user != null) SignoutButton(onClick = { showSignoutDialog = true }) - } - - if (showSignoutDialog) { - SignoutDialog( - onDismiss = { showSignoutDialog = false }, - onClickButton = { - Toast.makeText(context, "탈퇴 요청이 접수되었습니다.", Toast.LENGTH_LONG).show() - viewModel.logout() - }, - ) + if (user != null) SignoutButton(onClick = onClickSignout) } if (openLogoutDialog) { @@ -196,39 +183,8 @@ fun SignoutButton( modifier = modifier .padding(bottom = 40.dp) .clickable(onClick = onClick), - text = stringResource(id = R.string.signout_button), + text = stringResource(id = R.string.signout), style = MaterialTheme.typography.bodySmall.copy(color = Grey50), textDecoration = TextDecoration.Underline, ) } - -@Composable -private fun SignoutDialog( - onDismiss: () -> Unit, - onClickButton: () -> Unit, -) { - BTDialog( - positiveButtonLabel = stringResource(R.string.signout), - onClickPositiveButton = { - onClickButton() - onDismiss() - }, - onDismiss = onDismiss, - ) { - Text( - text = stringResource(R.string.signout_dialog_title), - style = MaterialTheme.typography.titleLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Text( - modifier = Modifier - .padding(top = 4.dp) - .align(Alignment.CenterHorizontally), - text = stringResource(R.string.signout_dialog_message), - style = MaterialTheme.typography.bodySmall, - textAlign = TextAlign.Center, - color = Grey50, - ) - } -} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutEvent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutEvent.kt new file mode 100644 index 00000000..6216d0d2 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutEvent.kt @@ -0,0 +1,5 @@ +package com.nexters.boolti.presentation.screen.signout + +sealed interface SignoutEvent { + data object SignoutSuccess : SignoutEvent +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNotice.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNotice.kt new file mode 100644 index 00000000..4aef8d8f --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNotice.kt @@ -0,0 +1,54 @@ +package com.nexters.boolti.presentation.screen.signout + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.theme.Grey50 +import com.nexters.boolti.presentation.theme.marginHorizontal +import com.nexters.boolti.presentation.theme.point4 + +@Composable +fun SignoutNotice( + modifier: Modifier, +) { + Column( + modifier = modifier, + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 20.dp, horizontal = marginHorizontal), + text = stringResource(R.string.signout_notice_title), + style = point4, + color = MaterialTheme.colorScheme.onPrimary, + ) + stringArrayResource(R.array.signout_notice).forEach { notice -> + Row( + modifier = Modifier + .wrapContentWidth() + .padding(horizontal = marginHorizontal), + ) { + Text( + text = stringResource(R.string.bullet), + style = MaterialTheme.typography.bodySmall, + color = Grey50, + ) + Text( + text = notice, + style = MaterialTheme.typography.bodySmall, + color = Grey50, + ) + } + } + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutReason.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutReason.kt new file mode 100644 index 00000000..4a2f5697 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutReason.kt @@ -0,0 +1,49 @@ +package com.nexters.boolti.presentation.screen.signout + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BTTextField +import com.nexters.boolti.presentation.theme.marginHorizontal +import com.nexters.boolti.presentation.theme.point4 + +@Composable +fun SignoutReason( + modifier: Modifier, + viewModel: SignoutViewModel = hiltViewModel(), +) { + val reason by viewModel.reason.collectAsStateWithLifecycle() + + Column( + modifier = modifier, + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 20.dp, horizontal = marginHorizontal), + text = stringResource(R.string.signout_reason_title), + style = point4, + color = MaterialTheme.colorScheme.onPrimary, + ) + BTTextField( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 160.dp, max = 300.dp) + .padding(horizontal = marginHorizontal), + text = reason, + placeholder = stringResource(R.string.signout_reason_placeholder), + onValueChanged = viewModel::setReason, + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt new file mode 100644 index 00000000..3861a6f2 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt @@ -0,0 +1,66 @@ +package com.nexters.boolti.presentation.screen.signout + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BtAppBar +import com.nexters.boolti.presentation.component.MainButton +import com.nexters.boolti.presentation.theme.marginHorizontal + +@Composable +fun SignoutScreen( + navigateToHome: () -> Unit, + navigateBack: () -> Unit, + viewModel: SignoutViewModel = hiltViewModel(), +) { + var firstPage by remember { mutableStateOf(true) } + val reason by viewModel.reason.collectAsStateWithLifecycle() + + BackHandler { + if (firstPage) navigateBack() else firstPage = true + } + + LaunchedEffect(viewModel.event) { + viewModel.event.collect { + when (it) { + SignoutEvent.SignoutSuccess -> navigateToHome() + } + } + } + + Scaffold( + topBar = { BtAppBar(title = stringResource(R.string.signout), onBackPressed = navigateBack) }, + bottomBar = { + MainButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = marginHorizontal) + .padding(bottom = 42.dp), + label = if (firstPage) stringResource(R.string.next) else stringResource(R.string.signout_button), + enabled = firstPage || reason.isNotBlank(), + onClick = { + if (firstPage) firstPage = false else viewModel.signout() + }, + ) + } + ) { innerPadding -> + if (firstPage) { + SignoutNotice(modifier = Modifier.padding(innerPadding)) + } else { + SignoutReason(modifier = Modifier.padding(innerPadding)) + } + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutViewModel.kt new file mode 100644 index 00000000..cf7fa576 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutViewModel.kt @@ -0,0 +1,38 @@ +package com.nexters.boolti.presentation.screen.signout + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SignoutViewModel @Inject constructor( + +) : ViewModel() { + private val _reason = MutableStateFlow("") + val reason = _reason.asStateFlow() + + private val _event = Channel() + val event = _event.receiveAsFlow() + + fun signout() { + // TODO 회원 탈퇴 API + // TODO 로그아웃 + event(SignoutEvent.SignoutSuccess) + } + + fun setReason(reason: String) { + _reason.value = reason + } + + private fun event(event: SignoutEvent) { + viewModelScope.launch { + _event.send(event) + } + } +} diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index c85fe8c0..96192754 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -61,10 +61,18 @@ 불티 유저 - 회원 탈퇴 - 탈퇴 + 회원 탈퇴 + 탈퇴하기 탈퇴하시겠어요? 탈퇴일로부터 30일 이내로 로그인 시 계정 삭제를 취소할 수 있습니다. 30일이 지나면 계정 및 정보가 영구 삭제됩니다. + 탈퇴 전, 꼭 읽어보세요! + + 주최한 공연 정보는 사라지지 않아요 + 예매한 티켓은 전부 사라지며 복구할 수 없어요 + 탈퇴 일로부터 30일 이내 재 로그인 시 계정 삭제를 취소할 수 있어요 + + 탈퇴 이유를 입력해주세요 + 예) 계정 탈퇴 후 재가입할게요 %,d원 총 %,d원 From 7ff3fda76d79ac0fa6e16fd8e53513475aa4040f Mon Sep 17 00:00:00 2001 From: mangbaam Date: Fri, 23 Feb 2024 02:15:06 +0900 Subject: [PATCH 007/129] =?UTF-8?q?Boolti-162=20feat:=20=ED=83=88=ED=87=B4?= =?UTF-8?q?=20API=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/data/datasource/AuthDataSource.kt | 6 +++++- .../boolti/data/datasource/UserDataSource.kt | 4 +++- .../boolti/data/network/api/UserService.kt | 10 +++++++++- .../boolti/data/repository/AuthRepositoryImpl.kt | 7 +++++++ .../boolti/domain/repository/AuthRepository.kt | 2 ++ .../boolti/domain/request/SignoutRequest.kt | 8 ++++++++ .../screen/signout/SignoutViewModel.kt | 16 ++++++++++++---- 7 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 domain/src/main/java/com/nexters/boolti/domain/request/SignoutRequest.kt diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt index fe65f6eb..6ff528c1 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt @@ -49,6 +49,10 @@ class AuthDataSource @Inject constructor( suspend fun logout(): Result = runCatching { loginService.logout() + localLogout() + } + + suspend fun localLogout() { dataStore.updateData { it.copy( userId = null, @@ -58,7 +62,7 @@ class AuthDataSource @Inject constructor( phoneNumber = null, photo = null, accessToken = "", - refreshToken = "" + refreshToken = "", ) } } diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/UserDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/UserDataSource.kt index 112db81a..cc94a3eb 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/UserDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/UserDataSource.kt @@ -2,10 +2,12 @@ package com.nexters.boolti.data.datasource import com.nexters.boolti.data.network.api.UserService import com.nexters.boolti.data.network.response.UserResponse +import com.nexters.boolti.domain.request.SignoutRequest import javax.inject.Inject class UserDataSource @Inject constructor( private val userService: UserService, ) { suspend fun getUser(): UserResponse = userService.getUser() -} \ No newline at end of file + suspend fun signout(request: SignoutRequest) = userService.signout(request) +} diff --git a/data/src/main/java/com/nexters/boolti/data/network/api/UserService.kt b/data/src/main/java/com/nexters/boolti/data/network/api/UserService.kt index 98e734f1..51d17ffd 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/api/UserService.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/api/UserService.kt @@ -1,9 +1,17 @@ package com.nexters.boolti.data.network.api import com.nexters.boolti.data.network.response.UserResponse +import com.nexters.boolti.domain.request.SignoutRequest +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.HTTP interface UserService { @GET("/app/api/v1/user") suspend fun getUser(): UserResponse -} \ No newline at end of file + + @HTTP(method = "DELETE", path = "/app/api/v1/user", hasBody = true) + suspend fun signout( + @Body request: SignoutRequest, + ) +} diff --git a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt index 1d37e778..69378e80 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt @@ -8,6 +8,7 @@ import com.nexters.boolti.domain.model.User import com.nexters.boolti.domain.repository.AuthRepository import com.nexters.boolti.domain.request.LoginRequest import com.nexters.boolti.domain.request.SignUpRequest +import com.nexters.boolti.domain.request.SignoutRequest import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map @@ -45,6 +46,12 @@ class AuthRepositoryImpl @Inject constructor( .mapCatching { } } + override suspend fun signout(request: SignoutRequest): Result = runCatching { + userDateSource.signout(request) + }.onSuccess { + authDataSource.localLogout() + } + override fun getUserAndCache(): Flow = flow { val response = userDateSource.getUser() authDataSource.updateUser(response) diff --git a/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt b/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt index 2f6f1e9c..81a362be 100644 --- a/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt +++ b/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt @@ -3,6 +3,7 @@ package com.nexters.boolti.domain.repository import com.nexters.boolti.domain.model.User import com.nexters.boolti.domain.request.LoginRequest import com.nexters.boolti.domain.request.SignUpRequest +import com.nexters.boolti.domain.request.SignoutRequest import kotlinx.coroutines.flow.Flow interface AuthRepository { @@ -17,6 +18,7 @@ interface AuthRepository { suspend fun kakaoLogin(request: LoginRequest): Result suspend fun logout(): Result suspend fun signUp(signUpRequest: SignUpRequest): Result + suspend fun signout(request: SignoutRequest): Result fun getUserAndCache(): Flow val loggedIn: Flow diff --git a/domain/src/main/java/com/nexters/boolti/domain/request/SignoutRequest.kt b/domain/src/main/java/com/nexters/boolti/domain/request/SignoutRequest.kt new file mode 100644 index 00000000..9027447a --- /dev/null +++ b/domain/src/main/java/com/nexters/boolti/domain/request/SignoutRequest.kt @@ -0,0 +1,8 @@ +package com.nexters.boolti.domain.request + +import kotlinx.serialization.Serializable + +@Serializable +data class SignoutRequest( + val reason: String, +) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutViewModel.kt index cf7fa576..04ebb3f0 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutViewModel.kt @@ -2,6 +2,8 @@ package com.nexters.boolti.presentation.screen.signout import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nexters.boolti.domain.repository.AuthRepository +import com.nexters.boolti.domain.request.SignoutRequest import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -12,7 +14,7 @@ import javax.inject.Inject @HiltViewModel class SignoutViewModel @Inject constructor( - + private val repository: AuthRepository, ) : ViewModel() { private val _reason = MutableStateFlow("") val reason = _reason.asStateFlow() @@ -21,9 +23,15 @@ class SignoutViewModel @Inject constructor( val event = _event.receiveAsFlow() fun signout() { - // TODO 회원 탈퇴 API - // TODO 로그아웃 - event(SignoutEvent.SignoutSuccess) + viewModelScope.launch { + repository.signout( + request = SignoutRequest(reason.value) + ).onSuccess { + event(SignoutEvent.SignoutSuccess) + }.onFailure { + it.printStackTrace() + } + } } fun setReason(reason: String) { From e154c37acabc92358655fe7cfff62c2adf50ca0f Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 23 Feb 2024 03:51:32 +0900 Subject: [PATCH 008/129] =?UTF-8?q?feat=20:=20zoomable=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 2 ++ presentation/build.gradle.kts | 1 + .../boolti/presentation/screen/show/ShowImagesScreen.kt | 5 ++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5636d4f..85a683a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,6 +41,7 @@ google-services = "4.4.0" firebase-crashlytics = "2.9.9" firebase-config = "21.6.0" kotest = "5.8.0" +zoomable = "1.6.0" zxing = "4.3.0" kakao = "2.19.0" timber = "5.0.1" @@ -101,6 +102,7 @@ firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "fir firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.require = "false" } firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.require = "false" } firebase-config-ktx = { module = "com.google.firebase:firebase-config-ktx", version.ref = "firebase-config" } +zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" } zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing" } kakao-login = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao" } retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "serializationConverter" } diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index ff190d69..15c128ec 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(libs.hilt.android) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.material3.android) + implementation(libs.zoomable) kapt(libs.hilt.compiler) implementation(libs.lottie) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt index e8260185..38218df5 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt @@ -27,6 +27,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BtAppBar +import net.engawapg.lib.zoomable.rememberZoomState +import net.engawapg.lib.zoomable.zoomable @OptIn(ExperimentalFoundationApi::class) @Composable @@ -67,7 +69,8 @@ fun ShowImagesScreen( ) { AsyncImage( modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() + .zoomable(rememberZoomState()), model = uiState.showDetail.images[it].originImage, contentDescription = null, contentScale = ContentScale.FillWidth, From aae4eac7efa657b7c15de740d3eb80e4e5ab9c1f Mon Sep 17 00:00:00 2001 From: mangbaam Date: Fri, 23 Feb 2024 23:02:49 +0900 Subject: [PATCH 009/129] =?UTF-8?q?Boolti-162=20style:=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=20=EB=AA=A9=EB=A1=9D=20(=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EC=95=88=EB=82=B4,=20=ED=99=98=EB=B6=88=20=EC=A0=95=EC=B1=85)?= =?UTF-8?q?=20=EA=B0=84=EA=B2=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/presentation/screen/signout/SignoutNotice.kt | 1 + .../boolti/presentation/screen/ticketing/TicketingScreen.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNotice.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNotice.kt index 4aef8d8f..9b3bbf02 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNotice.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNotice.kt @@ -44,6 +44,7 @@ fun SignoutNotice( color = Grey50, ) Text( + modifier = Modifier.padding(start = 2.dp), text = notice, style = MaterialTheme.typography.bodySmall, color = Grey50, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index cf761671..d4c3bf6a 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -289,6 +289,7 @@ private fun RefundPolicySection(refundPolicy: List) { color = Grey50, ) Text( + modifier = Modifier.padding(start = 2.dp), text = it, style = MaterialTheme.typography.bodySmall, color = Grey50, From fef13712ad6bc2ff9587bec9f956f97b8fa36273 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sat, 24 Feb 2024 19:22:21 +0900 Subject: [PATCH 010/129] =?UTF-8?q?Boolti-165=20style:=20=EC=9E=85?= =?UTF-8?q?=EC=9E=A5=EC=BD=94=EB=93=9C=20=EB=8B=A4=EC=9D=B4=EC=96=BC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EB=AC=B8=EA=B5=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- presentation/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index c85fe8c0..69353ee1 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -136,7 +136,7 @@ 입장 코드로 입장 확인 입장 코드는 주최자 계정의 마이 > QR 스캔 > 해당 공연 스캐너에서 확인 가능해요. 입장 코드를 입력해 주세요 - 올바른 입장 코드를 입력해 주세요 + 입장 코드가 올바르지 않아요 입장코드 보기 From e4047e9d9aec8df23baf4583e7d8f7b05e676dc9 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sat, 24 Feb 2024 19:24:14 +0900 Subject: [PATCH 011/129] =?UTF-8?q?Boolti-165=20style:=20=ED=99=98?= =?UTF-8?q?=EB=B6=88=20=EC=9E=85=EB=A0=A5=EC=B0=BD=20=EA=B0=84=EA=B2=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/presentation/screen/refund/RefundScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt index 3557dc7e..980aeece 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt @@ -394,7 +394,7 @@ fun RefundInfoPage( BTTextField( modifier = Modifier .fillMaxWidth() - .padding(top = 12.dp) + .padding(top = 16.dp) .onFocusChanged { focusState -> showAccountError = uiState.accountNumber.isNotEmpty() && !uiState.isValidAccountNumber && From 6eaf3f8b4209519be1b2a7d1753229f276b326f8 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sat, 24 Feb 2024 19:30:39 +0900 Subject: [PATCH 012/129] =?UTF-8?q?Boolti-165=20style:=20=ED=8B=B0?= =?UTF-8?q?=EC=BC=93=20=EB=AA=A9=EB=A1=9D=20=ED=83=AD=20=EB=A1=9C=EB=94=A9?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/ticket/TicketScreen.kt | 19 +++++++++++++++---- .../screen/ticket/TicketUiState.kt | 2 +- .../screen/ticket/TicketViewModel.kt | 19 ++++++++++++------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketScreen.kt index ea8d313e..26a3e9f4 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketScreen.kt @@ -2,6 +2,7 @@ package com.nexters.boolti.presentation.screen.ticket import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -27,6 +28,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nexters.boolti.presentation.component.BtCircularProgressIndicator import kotlin.math.absoluteValue @Composable @@ -43,10 +45,19 @@ fun TicketScreen( viewModel.load() } - if (uiState.tickets.isNotEmpty()) { - TicketNotEmptyScreen(modifier, uiState, onClickQr, onClickTicket = onClickTicket) - } else { - TicketEmptyScreen(modifier) + when { + uiState.loading -> Box(modifier = Modifier.fillMaxSize()) { + BtCircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + + uiState.tickets.isNotEmpty() -> TicketNotEmptyScreen( + modifier, + uiState, + onClickQr, + onClickTicket = onClickTicket + ) + + else -> TicketEmptyScreen(modifier) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketUiState.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketUiState.kt index 551c4297..a9003ec8 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketUiState.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketUiState.kt @@ -3,6 +3,6 @@ package com.nexters.boolti.presentation.screen.ticket import com.nexters.boolti.domain.model.Ticket data class TicketUiState( - // TODO 로딩 중 어떻게 표시할 지 결정되면 Loading 필드 추가. + val loading: Boolean = false, val tickets: List = emptyList(), ) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketViewModel.kt index cf8212c2..2e00cff8 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketViewModel.kt @@ -7,6 +7,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -16,18 +17,22 @@ import javax.inject.Inject class TicketViewModel @Inject constructor( private val ticketRepository: TicketRepository, ) : ViewModel() { - private val _uiState = MutableStateFlow(TicketUiState()) + private val _uiState = MutableStateFlow(TicketUiState(loading = true)) val uiState = _uiState.asStateFlow() fun load() { viewModelScope.launch { - ticketRepository.getTicket().catch { e -> - e.printStackTrace() - }.singleOrNull()?.let { tickets -> - _uiState.update { - it.copy(tickets = tickets) + _uiState.update { it.copy(loading = true) } + ticketRepository.getTicket() + .onCompletion { + _uiState.update { it.copy(loading = false) } + }.catch { e -> + e.printStackTrace() + }.singleOrNull()?.let { tickets -> + _uiState.update { + it.copy(tickets = tickets) + } } - } } } } From 515dcccda61adeef7638fa08fc3082e744c57a16 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sat, 24 Feb 2024 19:51:21 +0900 Subject: [PATCH 013/129] =?UTF-8?q?Boolti-165=20style:=20=EC=9E=85?= =?UTF-8?q?=EC=9E=A5=EC=BD=94=EB=93=9C=20=EB=8B=A4=EC=9D=B4=EC=96=BC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EB=AC=B8=EA=B5=AC=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- presentation/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 69353ee1..c85fe8c0 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -136,7 +136,7 @@ 입장 코드로 입장 확인 입장 코드는 주최자 계정의 마이 > QR 스캔 > 해당 공연 스캐너에서 확인 가능해요. 입장 코드를 입력해 주세요 - 입장 코드가 올바르지 않아요 + 올바른 입장 코드를 입력해 주세요 입장코드 보기 From 4c29f9a09599ef125c27eb3f2ff6c44c21303644 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 25 Feb 2024 02:06:36 +0900 Subject: [PATCH 014/129] =?UTF-8?q?Boolti-166=20feat:=20QR=20=EC=8A=A4?= =?UTF-8?q?=EC=BA=94=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/QrScanActivity.kt | 47 +++++-------------- .../boolti/presentation/QrScanViewModel.kt | 7 +-- .../presentation/component/CircleBgIcon.kt | 26 ++++++++++ .../component/ToastSnackbarHost.kt | 23 +++++++-- .../presentation/screen/qr/QrScanScreen.kt | 34 +++++++++++--- .../boolti/presentation/theme/Color.kt | 1 + .../src/main/res/drawable/ic_check.xml | 10 ++++ .../src/main/res/drawable/ic_error.xml | 14 ++++++ .../src/main/res/drawable/ic_warning.xml | 13 +++++ 9 files changed, 123 insertions(+), 52 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/component/CircleBgIcon.kt create mode 100644 presentation/src/main/res/drawable/ic_check.xml create mode 100644 presentation/src/main/res/drawable/ic_error.xml create mode 100644 presentation/src/main/res/drawable/ic_warning.xml diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/QrScanActivity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/QrScanActivity.kt index 2462e936..d892d373 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/QrScanActivity.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/QrScanActivity.kt @@ -3,45 +3,13 @@ package com.nexters.boolti.presentation import android.Manifest import android.os.Bundle import android.view.KeyEvent -import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -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.draw.clip -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.BarcodeCallback import com.journeyapps.barcodescanner.BarcodeResult @@ -51,6 +19,8 @@ import com.nexters.boolti.presentation.extension.requestPermission import com.nexters.boolti.presentation.screen.qr.QrScanScreen import com.nexters.boolti.presentation.theme.BooltiTheme import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @AndroidEntryPoint class QrScanActivity : ComponentActivity() { @@ -70,6 +40,13 @@ class QrScanActivity : ComponentActivity() { private val callback = BarcodeCallback { result: BarcodeResult -> result.text ?: return@BarcodeCallback viewModel.scan(result.text) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + barcodeView.pause() + delay(1000) + barcodeView.resume() + } + } } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/QrScanViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/QrScanViewModel.kt index ff127553..e075119d 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/QrScanViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/QrScanViewModel.kt @@ -48,11 +48,8 @@ class QrScanViewModel @Inject constructor( * @param entryCode 스캔한 QR 의 데이터 */ fun scan(entryCode: String) { - if (entryCode != lastCode) { - lastCode = entryCode - Timber.tag("mangbaam_QrScanActivity").d("스캔 결과: $entryCode") - requestEntrance(entryCode) - } + Timber.tag("mangbaam_QrScanActivity").d("스캔 결과: $entryCode") + requestEntrance(entryCode) } /** diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/CircleBgIcon.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/CircleBgIcon.kt new file mode 100644 index 00000000..3c68a133 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/CircleBgIcon.kt @@ -0,0 +1,26 @@ +package com.nexters.boolti.presentation.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +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.graphics.painter.Painter + +@Composable +fun CircleBgIcon( + modifier: Modifier = Modifier, + painter: Painter, + bgColor: Color, +) { + Box( + modifier = modifier + .clip(CircleShape) + .background(bgColor) + ) { + Icon(painter = painter, contentDescription = null) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/ToastSnackbarHost.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/ToastSnackbarHost.kt index 5203d552..f5b0e013 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/ToastSnackbarHost.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/ToastSnackbarHost.kt @@ -1,5 +1,7 @@ package com.nexters.boolti.presentation.component +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card @@ -9,6 +11,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -16,6 +19,7 @@ import androidx.compose.ui.unit.dp fun ToastSnackbarHost( hostState: SnackbarHostState, modifier: Modifier = Modifier, + leadingIcon: (@Composable () -> Unit)? = null, ) { SnackbarHost( hostState = hostState, @@ -28,11 +32,20 @@ fun ToastSnackbarHost( contentColor = MaterialTheme.colorScheme.onSurface, ), ) { - Text( - modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp), - text = data.visuals.message, - style = MaterialTheme.typography.bodySmall, - ) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (leadingIcon != null) { + leadingIcon() + Spacer(modifier = Modifier.padding(end = 12.dp)) + } + Text( + modifier = Modifier.padding(vertical = 12.dp), + text = data.visuals.message, + style = MaterialTheme.typography.bodySmall, + ) + } } } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt index bb4b9506..c1adb427 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel @@ -42,8 +41,12 @@ import com.nexters.boolti.presentation.QrScanEvent import com.nexters.boolti.presentation.QrScanViewModel import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog +import com.nexters.boolti.presentation.component.CircleBgIcon import com.nexters.boolti.presentation.component.ToastSnackbarHost +import com.nexters.boolti.presentation.theme.Error import com.nexters.boolti.presentation.theme.Grey50 +import com.nexters.boolti.presentation.theme.Success +import com.nexters.boolti.presentation.theme.Warning import kotlinx.coroutines.launch @Composable @@ -54,6 +57,7 @@ fun QrScanScreen( ) { var showEntryCodeDialog by remember { mutableStateOf(false) } val snackbarHostState = remember { SnackbarHostState() } + var snackbarIconId by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() val successMessage = stringResource(R.string.message_ticket_validated) @@ -70,17 +74,18 @@ fun QrScanScreen( LaunchedEffect(viewModel.event) { scope.launch { viewModel.event.collect { event -> - val errMessage = when (event) { + val (iconId, errMessage) = when (event) { is QrScanEvent.ScanError -> { when (event.errorType) { - QrErrorType.ShowNotToday -> notTodayErrMessage - QrErrorType.UsedTicket -> usedTicketErrMessage - QrErrorType.TicketNotFound -> notMatchedErrMessage + QrErrorType.ShowNotToday -> Pair(R.drawable.ic_warning, notTodayErrMessage) + QrErrorType.UsedTicket -> Pair(R.drawable.ic_error, usedTicketErrMessage) + QrErrorType.TicketNotFound -> Pair(R.drawable.ic_error, notMatchedErrMessage) } } - is QrScanEvent.ScanSuccess -> successMessage + is QrScanEvent.ScanSuccess -> Pair(R.drawable.ic_error, successMessage) } + snackbarIconId = iconId snackbarHostState.showSnackbar(errMessage) } } @@ -94,7 +99,22 @@ fun QrScanScreen( QrScanBottombar { showEntryCodeDialog = true } }, snackbarHost = { - ToastSnackbarHost(hostState = snackbarHostState, modifier = Modifier.padding(bottom = 100.dp)) + ToastSnackbarHost( + hostState = snackbarHostState, + modifier = Modifier.padding(bottom = 100.dp), + leadingIcon = { + snackbarIconId?.let { + CircleBgIcon( + painter = painterResource(it), + bgColor = when (it) { + R.drawable.ic_check -> Success + R.drawable.ic_error -> Error + else -> Warning + } + ) + } + }, + ) }, ) { innerPadding -> AndroidView( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/theme/Color.kt b/presentation/src/main/java/com/nexters/boolti/presentation/theme/Color.kt index 327d787f..3584c857 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/theme/Color.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/theme/Color.kt @@ -13,6 +13,7 @@ val Orange01 = Color(0xFFFF6827) val Error = Color(0xFFFF4D4F) val Success = Color(0xFF52C41A) +val Warning = Color(0xFFFAAD14) val Grey05 = Color(0xFFF6F7FF) val Grey10 = Color(0xFFE7EAF2) diff --git a/presentation/src/main/res/drawable/ic_check.xml b/presentation/src/main/res/drawable/ic_check.xml new file mode 100644 index 00000000..7ff24ca5 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_check.xml @@ -0,0 +1,10 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_error.xml b/presentation/src/main/res/drawable/ic_error.xml new file mode 100644 index 00000000..9d606708 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_error.xml @@ -0,0 +1,14 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_warning.xml b/presentation/src/main/res/drawable/ic_warning.xml new file mode 100644 index 00000000..27d50942 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_warning.xml @@ -0,0 +1,13 @@ + + + + From b6bd28ac7ca42581449e3753bcfbabd3d3ce2d78 Mon Sep 17 00:00:00 2001 From: HamBP Date: Sun, 25 Feb 2024 03:15:13 +0900 Subject: [PATCH 015/129] =?UTF-8?q?feat=20:=20fcm=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EC=A0=84=EA=B9=8C=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/build.gradle.kts | 2 ++ .../boolti/data/datasource/TokenDataSource.kt | 20 +++++++++++++++++-- .../data/repository/AuthRepositoryImpl.kt | 12 ++++++++++- .../domain/repository/AuthRepository.kt | 1 + .../presentation/screen/HomeViewModel.kt | 11 ++++++++++ .../service/BtFirebaseMessagingService.kt | 12 +++++++++++ 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 25ec2843..38bbf1dd 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -64,6 +64,8 @@ dependencies { implementation(libs.bundles.network) implementation(libs.firebase.config.ktx) + implementation(libs.bundles.firebase) + implementation(platform(libs.firebase.bom)) testImplementation(libs.junit) testImplementation(libs.bundles.kotest) diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt index d5a1ec88..90ef0280 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt @@ -2,13 +2,15 @@ package com.nexters.boolti.data.datasource import android.content.Context import androidx.datastore.core.DataStore +import com.google.android.gms.tasks.OnCompleteListener +import com.google.firebase.messaging.FirebaseMessaging import com.nexters.boolti.data.db.AppSettings import com.nexters.boolti.data.db.dataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.single -import kotlinx.coroutines.runBlocking import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class TokenDataSource @Inject constructor( private val context: Context, @@ -37,4 +39,18 @@ class TokenDataSource @Inject constructor( ) } } + + suspend fun getFcmToken(): String? = suspendCoroutine { continuation -> + val firebaseMessaging = FirebaseMessaging.getInstance() + firebaseMessaging.token.addOnCompleteListener(OnCompleteListener { task -> + if (!task.isSuccessful) { + continuation.resume(null) + return@OnCompleteListener + } + + val token = task.result + + continuation.resume(token) + }) + } } diff --git a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt index 1d37e778..952bbe73 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt @@ -29,18 +29,23 @@ class AuthRepositoryImpl @Inject constructor( return authDataSource.login(request) .onSuccess { response -> tokenDataSource.saveTokens(response.accessToken ?: "", response.refreshToken ?: "") + // TODO fcm 토큰 서버에 전송하기 } .mapCatching { !it.signUpRequired } } - override suspend fun logout(): Result = authDataSource.logout() + override suspend fun logout(): Result { + // TODO 서버에서 fcm 토큰 삭제하기 + return authDataSource.logout() + } override suspend fun signUp(signUpRequest: SignUpRequest): Result { return signUpDataSource.signUp(signUpRequest) .onSuccess { response -> tokenDataSource.saveTokens(response.accessToken, response.refreshToken) + // TODO fcm 토큰 서버에 전송하기 } .mapCatching { } } @@ -50,4 +55,9 @@ class AuthRepositoryImpl @Inject constructor( authDataSource.updateUser(response) emit(response.toDomain()) } + + override suspend fun sendFcmToken(): Result = runCatching { + val token = tokenDataSource.getFcmToken() + // TODO : 서버에 토큰 전송하기 + } } diff --git a/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt b/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt index 2f6f1e9c..79d8ecf5 100644 --- a/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt +++ b/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt @@ -18,6 +18,7 @@ interface AuthRepository { suspend fun logout(): Result suspend fun signUp(signUpRequest: SignUpRequest): Result fun getUserAndCache(): Flow + suspend fun sendFcmToken(): Result val loggedIn: Flow val cachedUser: Flow diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt index e3cabf53..e465f09f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -22,6 +23,7 @@ class HomeViewModel @Inject constructor( init { initUserInfo() + sendFcmToken() } private fun initUserInfo() { @@ -29,4 +31,13 @@ class HomeViewModel @Inject constructor( .catch { } .launchIn(viewModelScope) } + + private fun sendFcmToken() { + viewModelScope.launch { + authRepository.sendFcmToken() + .onFailure { + // TODO 실패했을 때 처리 어떡하지? retry를 해야 하나? + } + } + } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt new file mode 100644 index 00000000..5b461f25 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt @@ -0,0 +1,12 @@ +package com.nexters.boolti.presentation.service + +import com.google.firebase.messaging.FirebaseMessagingService +import timber.log.Timber + +class BtFirebaseMessagingService : FirebaseMessagingService() { + + override fun onNewToken(token: String) { + Timber.d("fcm token : $token") + // TODO : 로그인 되어 있다면 서버에 토큰 보내기 + } +} \ No newline at end of file From 07e200854eda75c49df31560104012564b8c1d62 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 25 Feb 2024 19:14:46 +0900 Subject: [PATCH 016/129] =?UTF-8?q?Boolti-167=20feat:=20=EC=9D=BC=EB=B0=98?= =?UTF-8?q?=ED=8B=B0=EC=BC=93=20=EC=98=88=EB=A7=A4=20=EC=8B=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/TicketingConfirmDialog.kt | 184 ++++++++++++++++++ .../screen/ticketing/TicketingScreen.kt | 32 ++- .../screen/ticketing/TicketingState.kt | 10 +- .../screen/ticketing/TicketingViewModel.kt | 10 +- presentation/src/main/res/values/strings.xml | 6 + 5 files changed, 228 insertions(+), 14 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt new file mode 100644 index 00000000..b4acd77e --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt @@ -0,0 +1,184 @@ +package com.nexters.boolti.presentation.screen.ticketing + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +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.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import com.nexters.boolti.domain.model.PaymentType +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BTDialog +import com.nexters.boolti.presentation.theme.Grey15 +import com.nexters.boolti.presentation.theme.Grey30 +import com.nexters.boolti.presentation.theme.marginHorizontal + +@Composable +fun TicketingConfirmDialog( + reservationName: String, + reservationContact: String, + depositor: String, + depositorContact: String, + ticketName: String, + ticketCount: Int, + totalPrice: Int, + paymentType: PaymentType, + onClick: () -> Unit, + onDismiss: () -> Unit, +) { + BTDialog( + positiveButtonLabel = stringResource(R.string.ticketing_payment_button_label_short), + onClickPositiveButton = onClick, + onDismiss = onDismiss, + ) { + Text(text = stringResource(R.string.ticketing_confirm_dialog_title)) + ConstraintLayout( + modifier = Modifier + .padding(top = 24.dp) + .fillMaxWidth() + .background(MaterialTheme.colorScheme.secondaryContainer) + .clip(RoundedCornerShape(4.dp)) + .padding(horizontal = marginHorizontal, vertical = 16.dp) + ) { + val ( + reservationLabelRef, + depositorRef, + ticketRef, + paymentTypeRef, + ) = createRefs() + + val barrier = createEndBarrier( + reservationLabelRef, + depositorRef, + ticketRef, + paymentTypeRef, + margin = 20.dp, + ) + + // 예매자 + Label( + modifier = Modifier.constrainAs(reservationLabelRef) { + top.linkTo(parent.top) + start.linkTo(parent.start) + }, + label = stringResource(R.string.ticket_holder) + ) + val (reservationNameRef, reservationContactRef) = createRefs() + InfoText( + modifier = Modifier.constrainAs(reservationNameRef) { + start.linkTo(barrier) + baseline.linkTo(reservationLabelRef.baseline) + }, + value = reservationName, + ) + InfoText( + modifier = Modifier.constrainAs(reservationContactRef) { + start.linkTo(reservationNameRef.start) + top.linkTo(reservationNameRef.bottom, 4.dp) + }, + value = reservationContact, + ) + + // 입금자 + Label( + modifier = Modifier.constrainAs(depositorRef) { + top.linkTo(reservationContactRef.bottom, 16.dp) + start.linkTo(parent.start) + }, + label = stringResource(R.string.depositor), + ) + val (depositorNameRef, depositorContactRef) = createRefs() + InfoText( + modifier = Modifier.constrainAs(depositorNameRef) { + start.linkTo(barrier) + baseline.linkTo(depositorRef.baseline) + }, + value = depositor + ) + InfoText( + modifier = Modifier.constrainAs(depositorContactRef) { + start.linkTo(depositorNameRef.start) + top.linkTo(depositorNameRef.bottom, 4.dp) + }, + value = depositorContact + ) + + // 티켓 + Label( + modifier = Modifier.constrainAs(ticketRef) { + top.linkTo(depositorContactRef.bottom, 16.dp) + start.linkTo(parent.start) + }, + label = stringResource(R.string.ticket), + ) + val (ticketNameRef, ticketInfoRef) = createRefs() + InfoText( + modifier = Modifier.constrainAs(ticketNameRef) { + start.linkTo(barrier) + baseline.linkTo(ticketRef.baseline) + }, + value = ticketName, + ) + InfoText( + modifier = Modifier.constrainAs(ticketInfoRef) { + start.linkTo(ticketNameRef.start) + top.linkTo(ticketNameRef.bottom, 4.dp) + }, + value = "${ticketCount}매 / ${totalPrice}원" + ) + + // 결제 수단 + Label( + modifier = Modifier.constrainAs(paymentTypeRef) { + top.linkTo(ticketInfoRef.bottom, 16.dp) + start.linkTo(parent.start) + }, + label = stringResource(R.string.ticket_type_label), + ) + val paymentTypeDataRef = createRef() + InfoText( + modifier = Modifier.constrainAs(paymentTypeDataRef) { + start.linkTo(barrier) + baseline.linkTo(paymentTypeRef.baseline) + }, + value = when (paymentType) { + PaymentType.ACCOUNT_TRANSFER -> stringResource(R.string.payment_account_transfer) + PaymentType.CARD -> stringResource(R.string.payment_card) + PaymentType.UNDEFINED -> "" + } + ) + } + } +} + +@Composable +private fun Label( + modifier: Modifier = Modifier, + label: String, +) { + Text( + modifier = modifier, + text = label, + style = MaterialTheme.typography.bodySmall, + color = Grey30, + ) +} + +@Composable +private fun InfoText( + modifier: Modifier = Modifier, + value: String, +) { + Text( + modifier = modifier, + text = value, + style = MaterialTheme.typography.bodySmall, + color = Grey15, + ) +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index d4c3bf6a..180cdc2d 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -100,11 +100,15 @@ fun TicketingScreen( val snackbarHostState = remember { SnackbarHostState() } val uiState by viewModel.uiState.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() + var showConfirmDialog by remember { mutableStateOf(false) } LaunchedEffect(viewModel.event) { viewModel.event.collect { when (it) { - is TicketingEvent.TicketingSuccess -> onReserved(it.reservationId, it.showId) + is TicketingEvent.TicketingSuccess -> { + showConfirmDialog = false + onReserved(it.reservationId, it.showId) + } } } } @@ -148,14 +152,14 @@ fun TicketingScreen( ) TicketHolderSection( name = uiState.reservationName, - phoneNumber = uiState.reservationPhoneNumber, + phoneNumber = uiState.reservationContact, isSameContactInfo = uiState.isSameContactInfo, onNameChanged = viewModel::setReservationName, onPhoneNumberChanged = viewModel::setReservationPhoneNumber, ) // 예매자 정보 if (!uiState.isInviteTicket) DeposorSection( name = uiState.depositorName, - phoneNumber = uiState.depositorPhoneNumber, + phoneNumber = uiState.depositorContact, isSameContactInfo = uiState.isSameContactInfo, onClickSameContact = viewModel::toggleIsSameContactInfo, onNameChanged = viewModel::setDepositorName, @@ -201,10 +205,30 @@ fun TicketingScreen( .padding(start = 20.dp, end = 20.dp, top = 8.dp, bottom = 24.dp), enabled = uiState.reservationButtonEnabled, label = stringResource(R.string.ticketing_payment_button_label, uiState.totalPrice), - onClick = viewModel::reservation, + onClick = { + if (uiState.isInviteTicket) { + viewModel.reservation() + } else { + showConfirmDialog = true + } + }, ) } } + if (showConfirmDialog) { + TicketingConfirmDialog( + reservationName = uiState.reservationName, + reservationContact = uiState.reservationContact, + depositor = if (uiState.isSameContactInfo) uiState.reservationName else uiState.depositorName, + depositorContact = if (uiState.isSameContactInfo) uiState.reservationContact else uiState.depositorContact, + ticketName = uiState.ticketName, + ticketCount = uiState.ticketCount, + totalPrice = uiState.totalPrice, + paymentType = uiState.paymentType, + onClick = viewModel::reservation, + onDismiss = { showConfirmDialog = false }, + ) + } } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt index a5d3ab57..75e79dd6 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt @@ -17,21 +17,21 @@ data class TicketingState( val inviteCodeStatus: InviteCodeStatus = InviteCodeStatus.Default, val paymentType: PaymentType = PaymentType.ACCOUNT_TRANSFER, val reservationName: String = "", - val reservationPhoneNumber: String = "", + val reservationContact: String = "", val depositorName: String = "", - val depositorPhoneNumber: String = "", + val depositorContact: String = "", val inviteCode: String = "", val refundPolicy: List = emptyList(), ) { val reservationButtonEnabled: Boolean get() = if (isInviteTicket) { reservationName.isNotBlank() && - reservationPhoneNumber.isNotBlank() && + reservationContact.isNotBlank() && inviteCodeStatus is InviteCodeStatus.Valid } else { reservationName.isNotBlank() && - reservationPhoneNumber.isNotBlank() && + reservationContact.isNotBlank() && (isSameContactInfo || depositorName.isNotBlank()) && - (isSameContactInfo || depositorPhoneNumber.isNotBlank()) + (isSameContactInfo || depositorContact.isNotBlank()) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt index 2edb718d..8ff018c2 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt @@ -54,20 +54,20 @@ class TicketingViewModel @Inject constructor( showId = showId, salesTicketTypeId = salesTicketTypeId, reservationName = state.reservationName, - reservationPhoneNumber = state.reservationPhoneNumber, + reservationPhoneNumber = state.reservationContact, ) false -> TicketingRequest.Normal( ticketCount = uiState.value.ticketCount, depositorName = if (uiState.value.isSameContactInfo) state.reservationName else state.depositorName, - depositorPhoneNumber = if (uiState.value.isSameContactInfo) state.reservationPhoneNumber else state.depositorPhoneNumber, + depositorPhoneNumber = if (uiState.value.isSameContactInfo) state.reservationContact else state.depositorContact, paymentAmount = uiState.value.totalPrice, paymentType = uiState.value.paymentType, userId = userId, showId = showId, salesTicketTypeId = salesTicketTypeId, reservationName = state.reservationName, - reservationPhoneNumber = state.reservationPhoneNumber, + reservationPhoneNumber = state.reservationContact, ) } @@ -156,7 +156,7 @@ class TicketingViewModel @Inject constructor( } fun setReservationPhoneNumber(number: String) { - _uiState.update { it.copy(reservationPhoneNumber = number) } + _uiState.update { it.copy(reservationContact = number) } } fun setDepositorName(name: String) { @@ -164,7 +164,7 @@ class TicketingViewModel @Inject constructor( } fun setDepositorPhoneNumber(number: String) { - _uiState.update { it.copy(depositorPhoneNumber = number) } + _uiState.update { it.copy(depositorContact = number) } } fun setInviteCode(code: String) { diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 96192754..56fe7248 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -50,6 +50,10 @@ 복사 다음 + 예매자 + 입금자 + 티켓 + 불티나게 팔리는 티켓, 불티 지금 티켓을 예매하고 공연을 즐겨보세요! @@ -135,6 +139,8 @@ 다음 페이지에서 계좌 번호를 안내해 드릴게요 지금은 계좌 이체로만 결제할 수 있어요 %,d원 결제하기 + 결제하기 + 결제 정보를 확인해주세요 공연장 주소가 복사되었어요 %s (%s) From 20af295d2c1bc9ee7330e75fa898bc4256af2861 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 25 Feb 2024 19:39:45 +0900 Subject: [PATCH 017/129] =?UTF-8?q?Boolti-167=20feat:=20=EC=97=B0=EB=9D=BD?= =?UTF-8?q?=EC=B2=98=20=ED=8F=AC=EB=A7=A4=ED=8C=85=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=ED=99=94=20=EB=B0=8F=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/presentation/extension/String.kt | 9 +++++++++ .../screen/ticketing/TicketingConfirmDialog.kt | 7 ++++--- .../boolti/presentation/util/VisualTransformation.kt | 11 ++--------- presentation/src/main/res/values/strings.xml | 1 + 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/extension/String.kt b/presentation/src/main/java/com/nexters/boolti/presentation/extension/String.kt index 16407af5..378bb6d1 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/extension/String.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/extension/String.kt @@ -1,7 +1,16 @@ package com.nexters.boolti.presentation.extension +import java.lang.StringBuilder + fun String.filterToPhoneNumber(): String = filter { it.isDigit() }.run { substring(0..minOf(10, lastIndex)) } fun String.sliceAtMost(maxLength: Int): String = slice(0 until minOf(maxLength, length)) + +fun String.toContactFormat(sep: Char = '-'): String = StringBuilder().apply { + filterToPhoneNumber().forEachIndexed { i, n -> + if (i in listOf(3, 7)) append(sep) + append(n) + } +}.toString() diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt index b4acd77e..681c047d 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt @@ -15,6 +15,7 @@ import androidx.constraintlayout.compose.ConstraintLayout import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog +import com.nexters.boolti.presentation.extension.toContactFormat import com.nexters.boolti.presentation.theme.Grey15 import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.marginHorizontal @@ -82,7 +83,7 @@ fun TicketingConfirmDialog( start.linkTo(reservationNameRef.start) top.linkTo(reservationNameRef.bottom, 4.dp) }, - value = reservationContact, + value = reservationContact.toContactFormat(), ) // 입금자 @@ -106,7 +107,7 @@ fun TicketingConfirmDialog( start.linkTo(depositorNameRef.start) top.linkTo(depositorNameRef.bottom, 4.dp) }, - value = depositorContact + value = depositorContact.toContactFormat(), ) // 티켓 @@ -130,7 +131,7 @@ fun TicketingConfirmDialog( start.linkTo(ticketNameRef.start) top.linkTo(ticketNameRef.bottom, 4.dp) }, - value = "${ticketCount}매 / ${totalPrice}원" + value = stringResource(R.string.reservations_ticket_count_price_format_short, ticketCount, totalPrice), ) // 결제 수단 diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/VisualTransformation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/VisualTransformation.kt index e7baa61e..f6a2c033 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/VisualTransformation.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/VisualTransformation.kt @@ -5,20 +5,13 @@ import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation import com.nexters.boolti.presentation.extension.filterToPhoneNumber +import com.nexters.boolti.presentation.extension.toContactFormat class PhoneNumberVisualTransformation( private val sep: Char = '-', ) : VisualTransformation { override fun filter(text: AnnotatedString): TransformedText { - val filtered = text.text.filterToPhoneNumber() - val annotatedString = AnnotatedString.Builder().run { - filtered.forEachIndexed { i, n -> - if (i in listOf(3, 7)) append(sep) - append(n) - } - toAnnotatedString() - } - + val annotatedString = AnnotatedString(text.text.toContactFormat(sep)) return TransformedText(annotatedString, phoneNumberOffsetMapping) } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 56fe7248..56632fa2 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -190,6 +190,7 @@ 상세 보기 알 수 없음 "%s / %d매 / %,d원" + %d매 / %,d원" 예매 내역 상세 From efe1c9271889ff7d7a8841b8e3c1fbf5be896797 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 25 Feb 2024 19:46:37 +0900 Subject: [PATCH 018/129] =?UTF-8?q?Boolti-167=20feat:=20=EC=B4=88=EC=B2=AD?= =?UTF-8?q?=ED=8B=B0=EC=BC=93=EC=97=90=EB=8F=84=20=EC=98=88=EB=A7=A4=20?= =?UTF-8?q?=EC=A0=84=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/TicketingConfirmDialog.kt | 63 +++++++++++-------- .../screen/ticketing/TicketingScreen.kt | 8 +-- presentation/src/main/res/values/strings.xml | 1 + 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt index 681c047d..e8046cec 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt @@ -22,6 +22,7 @@ import com.nexters.boolti.presentation.theme.marginHorizontal @Composable fun TicketingConfirmDialog( + isInviteTicket: Boolean, reservationName: String, reservationContact: String, depositor: String, @@ -87,33 +88,39 @@ fun TicketingConfirmDialog( ) // 입금자 - Label( - modifier = Modifier.constrainAs(depositorRef) { - top.linkTo(reservationContactRef.bottom, 16.dp) - start.linkTo(parent.start) - }, - label = stringResource(R.string.depositor), - ) val (depositorNameRef, depositorContactRef) = createRefs() - InfoText( - modifier = Modifier.constrainAs(depositorNameRef) { - start.linkTo(barrier) - baseline.linkTo(depositorRef.baseline) - }, - value = depositor - ) - InfoText( - modifier = Modifier.constrainAs(depositorContactRef) { - start.linkTo(depositorNameRef.start) - top.linkTo(depositorNameRef.bottom, 4.dp) - }, - value = depositorContact.toContactFormat(), - ) + if (!isInviteTicket) { + Label( + modifier = Modifier.constrainAs(depositorRef) { + top.linkTo(reservationContactRef.bottom, 16.dp) + start.linkTo(parent.start) + }, + label = stringResource(R.string.depositor), + ) + InfoText( + modifier = Modifier.constrainAs(depositorNameRef) { + start.linkTo(barrier) + baseline.linkTo(depositorRef.baseline) + }, + value = depositor + ) + InfoText( + modifier = Modifier.constrainAs(depositorContactRef) { + start.linkTo(depositorNameRef.start) + top.linkTo(depositorNameRef.bottom, 4.dp) + }, + value = depositorContact.toContactFormat(), + ) + } // 티켓 Label( modifier = Modifier.constrainAs(ticketRef) { - top.linkTo(depositorContactRef.bottom, 16.dp) + if (isInviteTicket) { + top.linkTo(reservationContactRef.bottom, 16.dp) + } else { + top.linkTo(depositorContactRef.bottom, 16.dp) + } start.linkTo(parent.start) }, label = stringResource(R.string.ticket), @@ -148,10 +155,14 @@ fun TicketingConfirmDialog( start.linkTo(barrier) baseline.linkTo(paymentTypeRef.baseline) }, - value = when (paymentType) { - PaymentType.ACCOUNT_TRANSFER -> stringResource(R.string.payment_account_transfer) - PaymentType.CARD -> stringResource(R.string.payment_card) - PaymentType.UNDEFINED -> "" + value = if (isInviteTicket) { + stringResource(R.string.invite_ticket) + } else { + when (paymentType) { + PaymentType.ACCOUNT_TRANSFER -> stringResource(R.string.payment_account_transfer) + PaymentType.CARD -> stringResource(R.string.payment_card) + PaymentType.UNDEFINED -> "" + } } ) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index 180cdc2d..d4fb414c 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -206,17 +206,14 @@ fun TicketingScreen( enabled = uiState.reservationButtonEnabled, label = stringResource(R.string.ticketing_payment_button_label, uiState.totalPrice), onClick = { - if (uiState.isInviteTicket) { - viewModel.reservation() - } else { - showConfirmDialog = true - } + showConfirmDialog = true }, ) } } if (showConfirmDialog) { TicketingConfirmDialog( + isInviteTicket = uiState.isInviteTicket, reservationName = uiState.reservationName, reservationContact = uiState.reservationContact, depositor = if (uiState.isSameContactInfo) uiState.reservationName else uiState.depositorName, @@ -274,7 +271,6 @@ private fun Header( @Composable private fun RefundPolicySection(refundPolicy: List) { var expanded by remember { mutableStateOf(false) } -// val refundPolicy = stringArrayResource(R.array.refund_policy) val rotation by animateFloatAsState( targetValue = if (expanded) 0F else 180F, animationSpec = tween(), diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 56632fa2..04d10131 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -53,6 +53,7 @@ 예매자 입금자 티켓 + 초청 티켓 불티나게 팔리는 티켓, 불티 From d3e2a7e5dce682e57a2c47e18d02d910988196fd Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 25 Feb 2024 23:52:06 +0900 Subject: [PATCH 019/129] =?UTF-8?q?Boolti-172=20style:=20=ED=8B=B0?= =?UTF-8?q?=EC=BC=93=20=EB=AA=A9=EB=A1=9D=20=ED=8F=AC=EC=8A=A4=ED=84=B0=20?= =?UTF-8?q?scale=20type=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/presentation/screen/ticket/TicketContent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt index 3fa8cd1d..f41356be 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt @@ -138,7 +138,7 @@ fun TicketContent( .padding(top = 20.dp, start = 20.dp, end = 20.dp) .clip(RoundedCornerShape(8.dp)), model = ticket.poster, - contentScale = ContentScale.FillWidth, + contentScale = ContentScale.Crop, contentDescription = stringResource(R.string.description_poster), ) DottedDivider( From 04c2be7a9b635ee9b9ec1f1da8119bc3e29a2236 Mon Sep 17 00:00:00 2001 From: algosketch Date: Mon, 26 Feb 2024 01:13:39 +0900 Subject: [PATCH 020/129] =?UTF-8?q?feat=20:=20=EC=84=9C=EB=B2=84=EC=97=90?= =?UTF-8?q?=20fcm=20=ED=86=A0=ED=81=B0=20=EC=A0=84=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 8 ++++ .../data/datasource/DeviceTokenDataSource.kt | 37 +++++++++++++++++++ .../boolti/data/datasource/TokenDataSource.kt | 18 --------- .../nexters/boolti/data/di/NetworkModule.kt | 6 +++ .../data/network/api/DeviceTokenService.kt | 13 +++++++ .../network/request/DeviceTokenRequest.kt | 9 +++++ .../network/response/DeviceTokenResponse.kt | 8 ++++ .../data/repository/AuthRepositoryImpl.kt | 36 +++++++----------- .../presentation/screen/HomeViewModel.kt | 8 ++-- .../service/BtFirebaseMessagingService.kt | 23 +++++++++++- 10 files changed, 120 insertions(+), 46 deletions(-) create mode 100644 data/src/main/java/com/nexters/boolti/data/datasource/DeviceTokenDataSource.kt create mode 100644 data/src/main/java/com/nexters/boolti/data/network/api/DeviceTokenService.kt create mode 100644 data/src/main/java/com/nexters/boolti/data/network/request/DeviceTokenRequest.kt create mode 100644 data/src/main/java/com/nexters/boolti/data/network/response/DeviceTokenResponse.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 04512855..9d49694f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -62,5 +62,13 @@ + + + + + + \ No newline at end of file diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/DeviceTokenDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/DeviceTokenDataSource.kt new file mode 100644 index 00000000..baed798c --- /dev/null +++ b/data/src/main/java/com/nexters/boolti/data/datasource/DeviceTokenDataSource.kt @@ -0,0 +1,37 @@ +package com.nexters.boolti.data.datasource + +import com.google.android.gms.tasks.OnCompleteListener +import com.google.firebase.messaging.FirebaseMessaging +import com.nexters.boolti.data.network.api.DeviceTokenService +import com.nexters.boolti.data.network.request.DeviceTokenRequest +import java.io.IOException +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class DeviceTokenDataSource @Inject constructor( + private val deviceTokenService: DeviceTokenService, +) { + suspend fun sendFcmToken(): Result = runCatching { + val response = deviceTokenService.postFcmToken( + DeviceTokenRequest(deviceToken = getFcmToken(), deviceType = "ANDROID") + ) + + if (!response.isSuccessful) throw IOException("fcm 토큰을 서버에 전송하는 데 실패했어요.") + } + + private suspend fun getFcmToken(): String = suspendCoroutine { continuation -> + val firebaseMessaging = FirebaseMessaging.getInstance() + firebaseMessaging.token.addOnCompleteListener(OnCompleteListener { task -> + if (!task.isSuccessful) { + continuation.resumeWithException(IllegalStateException("fcm 토큰을 가져오는 데 실패했어요.")) + return@OnCompleteListener + } + + val token = task.result + + continuation.resume(token) + }) + } +} \ No newline at end of file diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt index 90ef0280..b25cd10c 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt @@ -2,15 +2,11 @@ package com.nexters.boolti.data.datasource import android.content.Context import androidx.datastore.core.DataStore -import com.google.android.gms.tasks.OnCompleteListener -import com.google.firebase.messaging.FirebaseMessaging import com.nexters.boolti.data.db.AppSettings import com.nexters.boolti.data.db.dataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import javax.inject.Inject -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine class TokenDataSource @Inject constructor( private val context: Context, @@ -39,18 +35,4 @@ class TokenDataSource @Inject constructor( ) } } - - suspend fun getFcmToken(): String? = suspendCoroutine { continuation -> - val firebaseMessaging = FirebaseMessaging.getInstance() - firebaseMessaging.token.addOnCompleteListener(OnCompleteListener { task -> - if (!task.isSuccessful) { - continuation.resume(null) - return@OnCompleteListener - } - - val token = task.result - - continuation.resume(token) - }) - } } diff --git a/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt b/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt index 8b3d4b39..75adc147 100644 --- a/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt +++ b/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt @@ -7,6 +7,7 @@ import com.nexters.boolti.data.network.api.LoginService import com.nexters.boolti.data.network.AuthAuthenticator import com.nexters.boolti.data.datasource.AuthDataSource import com.nexters.boolti.data.network.AuthInterceptor +import com.nexters.boolti.data.network.api.DeviceTokenService import com.nexters.boolti.data.network.api.HostService import com.nexters.boolti.data.network.api.ReservationService import com.nexters.boolti.data.network.api.ShowService @@ -77,6 +78,11 @@ object NetworkModule { @Provides fun provideUserService(@Named("auth") retrofit: Retrofit): UserService = retrofit.create() + @Singleton + @Provides + fun provideDeviceTokenService(@Named("auth") retrofit: Retrofit): DeviceTokenService = + retrofit.create() + @Singleton @Provides fun provideSignUpService(retrofit: Retrofit): SignUpService = retrofit.create() diff --git a/data/src/main/java/com/nexters/boolti/data/network/api/DeviceTokenService.kt b/data/src/main/java/com/nexters/boolti/data/network/api/DeviceTokenService.kt new file mode 100644 index 00000000..12d932f9 --- /dev/null +++ b/data/src/main/java/com/nexters/boolti/data/network/api/DeviceTokenService.kt @@ -0,0 +1,13 @@ +package com.nexters.boolti.data.network.api + + +import com.nexters.boolti.data.network.request.DeviceTokenRequest +import com.nexters.boolti.data.network.response.DeviceTokenResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + +interface DeviceTokenService { + @POST("/app/papi/v1/device-token") + suspend fun postFcmToken(@Body request: DeviceTokenRequest): Response +} \ No newline at end of file diff --git a/data/src/main/java/com/nexters/boolti/data/network/request/DeviceTokenRequest.kt b/data/src/main/java/com/nexters/boolti/data/network/request/DeviceTokenRequest.kt new file mode 100644 index 00000000..faff6094 --- /dev/null +++ b/data/src/main/java/com/nexters/boolti/data/network/request/DeviceTokenRequest.kt @@ -0,0 +1,9 @@ +package com.nexters.boolti.data.network.request + +import kotlinx.serialization.Serializable + +@Serializable +data class DeviceTokenRequest( + val deviceToken: String, + val deviceType: String, +) diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/DeviceTokenResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/DeviceTokenResponse.kt new file mode 100644 index 00000000..412d6299 --- /dev/null +++ b/data/src/main/java/com/nexters/boolti/data/network/response/DeviceTokenResponse.kt @@ -0,0 +1,8 @@ +package com.nexters.boolti.data.network.response + +import kotlinx.serialization.Serializable + +@Serializable +data class DeviceTokenResponse( + val tokenId: String +) \ No newline at end of file diff --git a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt index 952bbe73..f4c1dd6a 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt @@ -1,6 +1,7 @@ package com.nexters.boolti.data.repository import com.nexters.boolti.data.datasource.AuthDataSource +import com.nexters.boolti.data.datasource.DeviceTokenDataSource import com.nexters.boolti.data.datasource.SignUpDataSource import com.nexters.boolti.data.datasource.TokenDataSource import com.nexters.boolti.data.datasource.UserDataSource @@ -18,6 +19,7 @@ class AuthRepositoryImpl @Inject constructor( private val tokenDataSource: TokenDataSource, private val signUpDataSource: SignUpDataSource, private val userDateSource: UserDataSource, + private val deviceTokenDataSource: DeviceTokenDataSource, ) : AuthRepository { override val loggedIn: Flow get() = authDataSource.loggedIn @@ -26,28 +28,21 @@ class AuthRepositoryImpl @Inject constructor( get() = authDataSource.user.map { it?.toDomain() } override suspend fun kakaoLogin(request: LoginRequest): Result { - return authDataSource.login(request) - .onSuccess { response -> - tokenDataSource.saveTokens(response.accessToken ?: "", response.refreshToken ?: "") - // TODO fcm 토큰 서버에 전송하기 - } - .mapCatching { - !it.signUpRequired - } + return authDataSource.login(request).onSuccess { response -> + tokenDataSource.saveTokens(response.accessToken ?: "", response.refreshToken ?: "") + deviceTokenDataSource.sendFcmToken() + }.mapCatching { + !it.signUpRequired + } } - override suspend fun logout(): Result { - // TODO 서버에서 fcm 토큰 삭제하기 - return authDataSource.logout() - } + override suspend fun logout(): Result = authDataSource.logout() override suspend fun signUp(signUpRequest: SignUpRequest): Result { - return signUpDataSource.signUp(signUpRequest) - .onSuccess { response -> - tokenDataSource.saveTokens(response.accessToken, response.refreshToken) - // TODO fcm 토큰 서버에 전송하기 - } - .mapCatching { } + return signUpDataSource.signUp(signUpRequest).onSuccess { response -> + tokenDataSource.saveTokens(response.accessToken, response.refreshToken) + deviceTokenDataSource.sendFcmToken() + }.mapCatching { } } override fun getUserAndCache(): Flow = flow { @@ -56,8 +51,5 @@ class AuthRepositoryImpl @Inject constructor( emit(response.toDomain()) } - override suspend fun sendFcmToken(): Result = runCatching { - val token = tokenDataSource.getFcmToken() - // TODO : 서버에 토큰 전송하기 - } + override suspend fun sendFcmToken(): Result = deviceTokenDataSource.sendFcmToken() } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt index e465f09f..240c8bad 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt @@ -6,6 +6,7 @@ import com.nexters.boolti.domain.repository.AuthRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -34,10 +35,9 @@ class HomeViewModel @Inject constructor( private fun sendFcmToken() { viewModelScope.launch { - authRepository.sendFcmToken() - .onFailure { - // TODO 실패했을 때 처리 어떡하지? retry를 해야 하나? - } + loggedIn.collectLatest { + if (it == true) authRepository.sendFcmToken() + } } } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt index 5b461f25..d2db25ed 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt @@ -1,12 +1,31 @@ package com.nexters.boolti.presentation.service import com.google.firebase.messaging.FirebaseMessagingService +import com.nexters.boolti.domain.repository.AuthRepository +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import timber.log.Timber +import javax.inject.Inject +@AndroidEntryPoint class BtFirebaseMessagingService : FirebaseMessagingService() { + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + @Inject + lateinit var authRepository: AuthRepository override fun onNewToken(token: String) { - Timber.d("fcm token : $token") - // TODO : 로그인 되어 있다면 서버에 토큰 보내기 + scope.launch { + authRepository.sendFcmToken() + } + } + + override fun onDestroy() { + super.onDestroy() + scope.cancel() } } \ No newline at end of file From 9da3306b17bc080da5a6f44946d8362346ee2ef6 Mon Sep 17 00:00:00 2001 From: algosketch Date: Mon, 26 Feb 2024 05:27:51 +0900 Subject: [PATCH 021/129] =?UTF-8?q?feat=20:=20channel=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 2 +- .../boolti/presentation/screen/MainActivity.kt | 18 +++++++++++++++++- presentation/src/main/res/values/strings.xml | 5 +++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9d49694f..6607f1c9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -61,7 +61,7 @@ + android:value="@string/default_notification_channel_id" /> = Build.VERSION_CODES.TIRAMISU) { requestPermission(Manifest.permission.POST_NOTIFICATIONS, 101) } + + createDefaultFcmChannel() + } + + private fun createDefaultFcmChannel() { + val channelId = getString(R.string.default_notification_channel_id) + val name = getString(R.string.fcm_default_channel_name) + val description = getString(R.string.fcm_default_channel_description) + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(channelId, name, importance) + channel.description = description + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) } -} +} \ 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 c85fe8c0..44c6ec08 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -212,4 +212,9 @@ 업데이트 알림 지금 업데이트하고\n더 편리해진 불티를 만나보세요 업데이트 하러가기 + + + fcm + 불티 알림 + 티켓 발권 알림을 받습니다. \ No newline at end of file From 993f5e6a1ff13df1ff1e2708a93cd9fe35e4a2ac Mon Sep 17 00:00:00 2001 From: algosketch Date: Mon, 26 Feb 2024 06:10:07 +0900 Subject: [PATCH 022/129] =?UTF-8?q?feat=20:=20=ED=8F=AC=EA=B7=B8=EB=9D=BC?= =?UTF-8?q?=EC=9A=B4=EB=93=9C=EC=9D=BC=20=EB=95=8C=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- presentation/src/main/AndroidManifest.xml | 1 + .../service/BtFirebaseMessagingService.kt | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index a5918e68..1818d8cd 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt index d2db25ed..aeb56b26 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt @@ -1,14 +1,20 @@ package com.nexters.boolti.presentation.service +import android.Manifest +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage import com.nexters.boolti.domain.repository.AuthRepository +import com.nexters.boolti.presentation.R import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint @@ -24,6 +30,25 @@ class BtFirebaseMessagingService : FirebaseMessagingService() { } } + override fun onMessageReceived(remoteMessage: RemoteMessage) { + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) return + + remoteMessage.notification?.let { notification -> + val defaultChannelId = getString(R.string.default_notification_channel_id) + val builder = + NotificationCompat.Builder(this, notification.channelId ?: defaultChannelId) + .setContentTitle(notification.title) + .setContentText(notification.body) + .setSmallIcon(R.drawable.ic_logo) + + NotificationManagerCompat.from(this).notify(0, builder.build()) + } + } + override fun onDestroy() { super.onDestroy() scope.cancel() From 964f921eb5179a827e72a0d0bb14d87c09adc4ba Mon Sep 17 00:00:00 2001 From: algosketch Date: Mon, 26 Feb 2024 06:32:13 +0900 Subject: [PATCH 023/129] =?UTF-8?q?feat=20:=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=A3=BC=EC=A0=9C=20=EA=B5=AC=EB=8F=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/MainActivity.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt index 4131fa7d..29acdf4f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt @@ -11,12 +11,16 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier +import com.google.firebase.Firebase +import com.google.firebase.messaging.messaging +import com.nexters.boolti.presentation.BuildConfig import com.nexters.boolti.presentation.QrScanActivity import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.extension.requestPermission import com.nexters.boolti.presentation.extension.startActivity import com.nexters.boolti.presentation.theme.BooltiTheme import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -45,6 +49,7 @@ class MainActivity : ComponentActivity() { } createDefaultFcmChannel() + subscribeDefaultTopic() } private fun createDefaultFcmChannel() { @@ -57,4 +62,15 @@ class MainActivity : ComponentActivity() { val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } + + private fun subscribeDefaultTopic() { + val defaultTopic = if (BuildConfig.DEBUG) "dev" else "prod" + + Firebase.messaging.subscribeToTopic(defaultTopic) + .addOnCompleteListener { task -> + if (!task.isSuccessful) { + Timber.d("구독 실패") + } + } + } } \ No newline at end of file From 9fccb504a4228790d3d2906a2479e84c2faca234 Mon Sep 17 00:00:00 2001 From: algosketch Date: Tue, 27 Feb 2024 03:36:27 +0900 Subject: [PATCH 024/129] =?UTF-8?q?feat=20:=20LocalSnackbarController=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 37 ++++++++++++++++++- .../presentation/util/SnackbarController.kt | 19 ++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/util/SnackbarController.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 2eb85b63..6005be5c 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -1,10 +1,17 @@ package com.nexters.boolti.presentation.screen import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.navigation.NavBackStackEntry @@ -15,6 +22,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.navigateToHome import com.nexters.boolti.presentation.screen.home.HomeScreen import com.nexters.boolti.presentation.screen.login.LoginScreen @@ -33,13 +41,40 @@ import com.nexters.boolti.presentation.screen.signout.SignoutScreen import com.nexters.boolti.presentation.screen.ticket.detail.TicketDetailScreen import com.nexters.boolti.presentation.screen.ticketing.TicketingScreen import com.nexters.boolti.presentation.theme.BooltiTheme +import com.nexters.boolti.presentation.util.SnackbarController + +val LocalSnackbarController = staticCompositionLocalOf { + SnackbarController(SnackbarHostState()) +} @Composable fun Main(onClickQrScan: (showId: String, showName: String) -> Unit) { val modifier = Modifier.fillMaxSize() + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + BooltiTheme { Surface(modifier) { - MainNavigation(modifier, onClickQrScan) + Scaffold( + snackbarHost = { + ToastSnackbarHost( + modifier = Modifier.padding(bottom = 80.dp), + hostState = snackbarHostState, + ) + }, + ) { innerPadding -> + CompositionLocalProvider( + LocalSnackbarController provides SnackbarController( + snackbarHostState, + scope + ) + ) { + MainNavigation( + modifier = modifier.padding(innerPadding), + onClickQrScan = onClickQrScan, + ) + } + } } } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/SnackbarController.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/SnackbarController.kt new file mode 100644 index 00000000..0dfbeb4e --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/SnackbarController.kt @@ -0,0 +1,19 @@ +package com.nexters.boolti.presentation.util + +import androidx.compose.material3.SnackbarHostState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch + +class SnackbarController( + private val snackbarHostState: SnackbarHostState, + private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob()), +) { + fun showMessage( + message: String, + ) { + coroutineScope.launch { + snackbarHostState.showSnackbar(message) + } + } +} \ No newline at end of file From d281219d150fcfd62fa4c6e0d1c4d772e6359000 Mon Sep 17 00:00:00 2001 From: algosketch Date: Tue, 27 Feb 2024 03:50:14 +0900 Subject: [PATCH 025/129] =?UTF-8?q?enhance=20:=20=EA=B8=B0=EB=B3=B8=20snac?= =?UTF-8?q?kbar=20->=20Controller=20=EC=9D=B4=EC=9A=A9.=20toast=20->=20sna?= =?UTF-8?q?ckbar=20=EB=B3=80=EA=B2=BD.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/refund/RefundScreen.kt | 6 ++--- .../reservations/ReservationDetailScreen.kt | 22 +++++-------------- .../screen/show/ShowDetailScreen.kt | 22 +++---------------- 3 files changed, 12 insertions(+), 38 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt index 980aeece..f5ad7357 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt @@ -74,6 +74,7 @@ import com.nexters.boolti.presentation.component.BTTextField import com.nexters.boolti.presentation.component.BtAppBar import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.extension.filterToPhoneNumber +import com.nexters.boolti.presentation.screen.LocalSnackbarController import com.nexters.boolti.presentation.theme.Error import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey15 @@ -98,17 +99,16 @@ fun RefundScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() val events = viewModel.events val scope = rememberCoroutineScope() - val context = LocalContext.current val pagerState = rememberPagerState { 2 } var openDialog by remember { mutableStateOf(false) } + val snackbarController = LocalSnackbarController.current val refundMessage = stringResource(id = R.string.refund_completed) LaunchedEffect(Unit) { events.collect { event -> when (event) { RefundEvent.SuccessfullyRefunded -> { - // TODO 스낵바로 변경 - Toast.makeText(context, refundMessage, Toast.LENGTH_LONG).show() + snackbarController.showMessage(refundMessage) onBackPressed() } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index e757f6d3..8ff42afc 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -58,6 +58,7 @@ import com.nexters.boolti.presentation.component.CopyButton import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.constants.datetimeFormat import com.nexters.boolti.presentation.extension.toDescriptionAndColorPair +import com.nexters.boolti.presentation.screen.LocalSnackbarController import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey15 import com.nexters.boolti.presentation.theme.Grey20 @@ -78,8 +79,6 @@ fun ReservationDetailScreen( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val refundPolicy by viewModel.refundPolicy.collectAsStateWithLifecycle() - val snackbarHostState = remember { SnackbarHostState() } - val scope = rememberCoroutineScope() LaunchedEffect(Unit) { viewModel.fetchReservation() @@ -87,12 +86,6 @@ fun ReservationDetailScreen( Scaffold( modifier = modifier, - snackbarHost = { - ToastSnackbarHost( - modifier = Modifier.padding(bottom = 80.dp), - hostState = snackbarHostState, - ) - }, topBar = { ReservationDetailAppBar(onBackPressed = onBackPressed) }) { innerPadding -> val state = uiState @@ -113,11 +106,7 @@ fun ReservationDetailScreen( ) Header(reservation = state.reservation) if (!state.reservation.isInviteTicket) { - DepositInfo( - reservation = state.reservation, - showMessage = { message -> - scope.launch { snackbarHostState.showSnackbar(message) } - }) + DepositInfo(reservation = state.reservation) } PaymentInfo(reservation = state.reservation) TicketInfo(reservation = state.reservation) @@ -211,10 +200,11 @@ private fun Header( @Composable private fun DepositInfo( - reservation: ReservationDetail, - showMessage: (message: String) -> Unit, modifier: Modifier = Modifier, + reservation: ReservationDetail, ) { + val snackbarController = LocalSnackbarController.current + Section( modifier = modifier, title = stringResource(id = R.string.reservation_account_info), @@ -242,7 +232,7 @@ private fun DepositInfo( onClick = { clipboardManager.setText(AnnotatedString(reservation.accountNumber)) if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - showMessage(copiedMessage) + snackbarController.showMessage(copiedMessage) } }, ) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index cd59ec8a..0b295cae 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -61,6 +61,7 @@ import com.nexters.boolti.presentation.component.CopyButton import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.requireActivity +import com.nexters.boolti.presentation.screen.LocalSnackbarController import com.nexters.boolti.presentation.screen.ticketing.ChooseTicketBottomSheet import com.nexters.boolti.presentation.theme.Grey05 import com.nexters.boolti.presentation.theme.Grey20 @@ -95,7 +96,6 @@ fun ShowDetailScreen( val isLoggedIn by viewModel.loggedIn.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() - val snackbarHostState = remember { SnackbarHostState() } var showBottomSheet by remember { mutableStateOf(false) } val window = LocalContext.current.requireActivity().window @@ -103,12 +103,6 @@ fun ShowDetailScreen( Scaffold( modifier = modifier, - snackbarHost = { - ToastSnackbarHost( - modifier = Modifier.padding(bottom = 80.dp), - hostState = snackbarHostState, - ) - }, topBar = { ShowDetailAppBar( onBack = onBack, @@ -137,7 +131,6 @@ fun ShowDetailScreen( modifier = Modifier .padding(horizontal = marginHorizontal) .padding(bottom = 114.dp), - snackbarHost = snackbarHostState, showDetail = uiState.showDetail, host = stringResource( id = R.string.ticketing_host_format, @@ -168,11 +161,6 @@ fun ShowDetailScreen( ShowDetailCtaButton( showState = uiState.showDetail.state, purchased = uiState.showDetail.isReserved, - showMessage = { message -> - scope.launch { - snackbarHostState.showSnackbar(message = message) - } - }, onClick = { scope.launch { if (isLoggedIn == true) { @@ -315,13 +303,12 @@ private fun ShowDetailAppBar( @Composable private fun ContentScaffold( - snackbarHost: SnackbarHostState, showDetail: ShowDetail, host: String, onClickContent: () -> Unit, modifier: Modifier = Modifier, ) { - val scope = rememberCoroutineScope() + val snackbarController = LocalSnackbarController.current Column( modifier = modifier, @@ -361,9 +348,7 @@ private fun ContentScaffold( onClick = { clipboardManager.setText(AnnotatedString(showDetail.streetAddress)) if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - scope.launch { - snackbarHost.showSnackbar(copiedMessage) - } + snackbarController.showMessage(copiedMessage) } } ) @@ -498,7 +483,6 @@ private fun SectionContent( @Composable fun ShowDetailCtaButton( onClick: () -> Unit, - showMessage: (message: String) -> Unit, purchased: Boolean, showState: ShowState, modifier: Modifier = Modifier, From 1e9e8617a289f165cc4bcd4a09a97997a33bed77 Mon Sep 17 00:00:00 2001 From: algosketch Date: Tue, 27 Feb 2024 04:52:16 +0900 Subject: [PATCH 026/129] =?UTF-8?q?fix=20:=20=ED=8F=B0=ED=8A=B8=20center?= =?UTF-8?q?=20=EC=A0=95=EB=A0=AC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/nexters/boolti/presentation/theme/Type.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/theme/Type.kt b/presentation/src/main/java/com/nexters/boolti/presentation/theme/Type.kt index 2b57ce7c..1f257282 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/theme/Type.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/theme/Type.kt @@ -127,10 +127,9 @@ private fun createTextStyle( fontSize = fontSize, lineHeight = lineHeight, lineHeightStyle = LineHeightStyle( - alignment = LineHeightStyle.Alignment.Proportional, + alignment = LineHeightStyle.Alignment.Center, trim = LineHeightStyle.Trim.None ), - baselineShift = BaselineShift(0.12f), ) } From 3e8d28da516e903a20323464aa4a65097d11202e Mon Sep 17 00:00:00 2001 From: algosketch Date: Tue, 27 Feb 2024 18:02:37 +0900 Subject: [PATCH 027/129] =?UTF-8?q?update=20:=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 85a683a8..04cf6dfe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] minSdk = "26" targetSdk = "34" -versionCode = "5" -versionName = "1.1.1" +versionCode = "6" +versionName = "1.1.2" packageName = "com.nexters.boolti" compileSdk = "34" targetJvm = "17" From 07ca330f3471183328a1cc2a93c2324c7b174a0d Mon Sep 17 00:00:00 2001 From: algosketch Date: Wed, 28 Feb 2024 23:42:29 +0900 Subject: [PATCH 028/129] =?UTF-8?q?fix=20:=20=ED=8F=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=84=B8=EB=B6=80=EC=82=AC=ED=95=AD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/component/ShowFeed.kt | 9 ++----- .../screen/qr/HostedShowScreen.kt | 5 ++-- .../screen/show/ShowDetailScreen.kt | 9 ++----- .../presentation/screen/show/ShowScreen.kt | 15 ++--------- .../nexters/boolti/presentation/theme/Type.kt | 24 ++++++++++++++---- presentation/src/main/res/font/sb_aggro_b.otf | Bin 0 -> 521328 bytes 6 files changed, 27 insertions(+), 35 deletions(-) create mode 100644 presentation/src/main/res/font/sb_aggro_b.otf diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt index b1d0da01..b755a5fc 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt @@ -19,10 +19,8 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import com.nexters.boolti.domain.model.Show import com.nexters.boolti.domain.model.ShowState @@ -30,7 +28,7 @@ import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.theme.Grey05 import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey80 -import com.nexters.boolti.presentation.theme.aggroFamily +import com.nexters.boolti.presentation.theme.point1 import java.time.format.DateTimeFormatter @Composable @@ -119,11 +117,8 @@ fun ShowFeed( modifier = Modifier.padding(top = 2.dp), maxLines = 2, overflow = TextOverflow.Ellipsis, - fontSize = 16.sp, - lineHeight = 26.sp, - fontFamily = aggroFamily, - fontWeight = FontWeight.Normal, color = MaterialTheme.colorScheme.onBackground, + style = point1, ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt index 96e2a37d..64f22283 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt @@ -39,7 +39,7 @@ import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey60 -import com.nexters.boolti.presentation.theme.aggroFamily +import com.nexters.boolti.presentation.theme.point1 import java.time.LocalDate import java.time.LocalDateTime @@ -128,9 +128,8 @@ private fun HostedShowItem( Text( modifier = Modifier.weight(1f), text = show.name, - style = MaterialTheme.typography.bodyLarge, + style = point1, color = tint, - fontFamily = aggroFamily, ) Icon( painter = painterResource(id = R.drawable.ic_scan), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index cd59ec8a..cf0875c2 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nexters.boolti.domain.model.ShowDetail @@ -62,14 +61,13 @@ import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.requireActivity import com.nexters.boolti.presentation.screen.ticketing.ChooseTicketBottomSheet -import com.nexters.boolti.presentation.theme.Grey05 import com.nexters.boolti.presentation.theme.Grey20 import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey50 import com.nexters.boolti.presentation.theme.Grey80 import com.nexters.boolti.presentation.theme.Grey85 -import com.nexters.boolti.presentation.theme.aggroFamily import com.nexters.boolti.presentation.theme.marginHorizontal +import com.nexters.boolti.presentation.theme.point3 import kotlinx.coroutines.launch import timber.log.Timber import java.time.format.DateTimeFormatter @@ -448,10 +446,7 @@ private fun Poster( Text( modifier = Modifier.padding(top = 24.dp, bottom = 30.dp), text = title, - fontFamily = aggroFamily, - color = Grey05, - fontSize = 24.sp, - lineHeight = 34.sp, + style = point3, ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt index 7eb5070f..fd177ad5 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt @@ -44,18 +44,12 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nexters.boolti.presentation.R @@ -65,8 +59,8 @@ import com.nexters.boolti.presentation.theme.Grey15 import com.nexters.boolti.presentation.theme.Grey60 import com.nexters.boolti.presentation.theme.Grey70 import com.nexters.boolti.presentation.theme.Grey85 -import com.nexters.boolti.presentation.theme.aggroFamily import com.nexters.boolti.presentation.theme.marginHorizontal +import com.nexters.boolti.presentation.theme.point4 @Composable fun ShowScreen( @@ -181,12 +175,7 @@ fun ShowAppBar( modifier = Modifier .fillMaxWidth(), text = stringResource(id = R.string.home_sub_title, nickname), - style = TextStyle( - lineHeight = 34.sp, - fontWeight = FontWeight.Normal, - fontSize = 24.sp, - fontFamily = aggroFamily, - ), + style = point4, ) SearchBar( modifier = Modifier diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/theme/Type.kt b/presentation/src/main/java/com/nexters/boolti/presentation/theme/Type.kt index 1f257282..f81a5bfc 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/theme/Type.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/theme/Type.kt @@ -5,9 +5,9 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import com.nexters.boolti.presentation.R @@ -21,7 +21,8 @@ val pretendardFamily = FontFamily( ) val aggroFamily = FontFamily( - Font(R.font.sb_aggro_m, FontWeight.Normal), + Font(R.font.sb_aggro_m, FontWeight.Medium), + Font(R.font.sb_aggro_b, FontWeight.Bold), ) private val headline3 = createTextStyle( @@ -96,23 +97,34 @@ private val caption = createTextStyle( val point1 = createTextStyle( fontFamily = aggroFamily, - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Bold, fontSize = 16.sp, lineHeight = 26.sp, + letterSpacing = (-0.03).em, ) val point2 = createTextStyle( fontFamily = aggroFamily, - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Bold, fontSize = 20.sp, lineHeight = 30.sp, + letterSpacing = (-0.03).em, +) + +val point3 = createTextStyle( + fontFamily = aggroFamily, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + lineHeight = 34.sp, + letterSpacing = (-0.03).em, ) val point4 = createTextStyle( fontFamily = aggroFamily, - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Medium, fontSize = 24.sp, lineHeight = 34.sp, + letterSpacing = (-0.03).em, ) private fun createTextStyle( @@ -120,6 +132,7 @@ private fun createTextStyle( lineHeight: TextUnit, fontFamily: FontFamily = pretendardFamily, fontWeight: FontWeight = FontWeight.Normal, + letterSpacing: TextUnit = TextUnit.Unspecified, ): TextStyle { return TextStyle( fontFamily = fontFamily, @@ -130,6 +143,7 @@ private fun createTextStyle( alignment = LineHeightStyle.Alignment.Center, trim = LineHeightStyle.Trim.None ), + letterSpacing = letterSpacing, ) } diff --git a/presentation/src/main/res/font/sb_aggro_b.otf b/presentation/src/main/res/font/sb_aggro_b.otf new file mode 100644 index 0000000000000000000000000000000000000000..f909eae5fb00424f05b6ba7d6d8b7d0217fe6805 GIT binary patch literal 521328 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𝔷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

%xE_@TU|gH3O9e`F1Aq_dwc0*AkmShSi5X} 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 Date: Thu, 29 Feb 2024 03:11:54 +0900 Subject: [PATCH 029/129] =?UTF-8?q?fix=20:=20=EC=83=81=EC=9A=B4=EC=9D=B4?= =?UTF-8?q?=EA=B0=80=20=EB=82=B4=EB=A0=A4=EC=A3=BC=EB=8A=94=20=EC=83=88?= =?UTF-8?q?=EB=A1=9C=EC=9A=B4=20=ED=8B=B0=EC=BC=93=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/data/network/response/ReservationDetailResponse.kt | 2 ++ .../java/com/nexters/boolti/domain/model/ReservationDetail.kt | 1 + .../presentation/screen/reservations/ReservationDetailScreen.kt | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt index e9d98d63..e11ab0cd 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt @@ -26,6 +26,7 @@ data class ReservationDetailResponse( val reservationPhoneNumber: String, val depositorName: String = "", val depositorPhoneNumber: String = "", + val csReservationId: String, ) { fun toDomain(): ReservationDetail { return ReservationDetail( @@ -47,6 +48,7 @@ data class ReservationDetailResponse( ticketHolderPhoneNumber = reservationPhoneNumber, depositorName = depositorName, depositorPhoneNumber = depositorPhoneNumber, + csReservationId = csReservationId, ) } } diff --git a/domain/src/main/java/com/nexters/boolti/domain/model/ReservationDetail.kt b/domain/src/main/java/com/nexters/boolti/domain/model/ReservationDetail.kt index 2504da9a..1ef6b4b1 100644 --- a/domain/src/main/java/com/nexters/boolti/domain/model/ReservationDetail.kt +++ b/domain/src/main/java/com/nexters/boolti/domain/model/ReservationDetail.kt @@ -21,4 +21,5 @@ data class ReservationDetail( val ticketHolderPhoneNumber: String, val depositorName: String, val depositorPhoneNumber: String, + val csReservationId: String, ) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index e757f6d3..2374b929 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -108,7 +108,7 @@ fun ReservationDetailScreen( modifier = Modifier .padding(horizontal = marginHorizontal) .padding(top = 12.dp), - text = "No. ${state.reservation.id}", + text = "No. ${state.reservation.csReservationId}", style = MaterialTheme.typography.bodySmall.copy(color = Grey50), ) Header(reservation = state.reservation) From f2ffbaf33fa5e8aabeb850b0e22f13957d7513b7 Mon Sep 17 00:00:00 2001 From: algosketch Date: Thu, 29 Feb 2024 03:23:44 +0900 Subject: [PATCH 030/129] =?UTF-8?q?fix=20:=20=ED=99=98=EB=B6=88=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=A0=95=EB=A0=AC=20=EB=B0=8F=20=EB=A7=88=EC=A7=84?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/refund/RefundScreen.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt index 980aeece..c4809b76 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt @@ -184,7 +184,7 @@ fun RefundScreen( value = uiState.name ) InfoRow( - modifier = Modifier.padding(top = 8.dp), + modifier = Modifier.padding(top = 16.dp), type = stringResource(id = R.string.contact_label), value = StringBuilder(uiState.contact).apply { if (uiState.contact.length > 7) { @@ -194,12 +194,12 @@ fun RefundScreen( }.toString() ) InfoRow( - modifier = Modifier.padding(top = 8.dp), + modifier = Modifier.padding(top = 16.dp), type = stringResource(id = R.string.bank_name), value = uiState.bankInfo?.bankName ?: "" ) InfoRow( - modifier = Modifier.padding(top = 8.dp), + modifier = Modifier.padding(top = 16.dp), type = stringResource(id = R.string.account_number), value = uiState.accountNumber ) @@ -671,17 +671,16 @@ fun InfoRow( ) { Row( modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, ) { Text( + modifier = Modifier.width(70.dp), text = type, style = MaterialTheme.typography.bodySmall.copy(color = Grey30), ) Text( - modifier = Modifier.weight(1.0f), + modifier = Modifier.padding(start = 12.dp), text = value, style = MaterialTheme.typography.bodySmall.copy(color = Grey15), - textAlign = TextAlign.End, ) } } \ No newline at end of file From 86e0666a5f8cacf252bb0604ba303aa5138c58c9 Mon Sep 17 00:00:00 2001 From: algosketch Date: Thu, 29 Feb 2024 03:42:46 +0900 Subject: [PATCH 031/129] =?UTF-8?q?fix=20:=20=EC=9D=B4=EB=A6=84,=20?= =?UTF-8?q?=EC=97=B0=EB=9D=BD=EC=B2=98=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/refund/RefundScreen.kt | 33 ++----------------- .../screen/refund/RefundUiState.kt | 10 ++---- 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt index c4809b76..0d0a72a7 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt @@ -274,9 +274,7 @@ fun RefundInfoPage( val sheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true ) - var showNameError by remember { mutableStateOf(false) } var showAccountError by remember { mutableStateOf(false) } - var showContactError by remember { mutableStateOf(false) } Column( modifier = modifier.verticalScroll(rememberScrollState()), @@ -298,13 +296,7 @@ fun RefundInfoPage( ) BTTextField( modifier = Modifier - .weight(1.0f) - .onFocusChanged { focusState -> - showNameError = uiState.name.isNotEmpty() && - !uiState.isValidName && - !focusState.isFocused - }, - isError = showNameError, + .weight(1.0f), text = uiState.name, singleLine = true, keyboardOptions = KeyboardOptions( @@ -315,13 +307,6 @@ fun RefundInfoPage( onValueChanged = onNameChanged ) } - if (showNameError) { - Text( - modifier = Modifier.padding(start = 56.dp, top = 12.dp), - text = stringResource(id = R.string.validation_name), - style = MaterialTheme.typography.bodySmall.copy(color = Error), - ) - } Row( modifier = Modifier.padding(top = 16.dp), @@ -334,32 +319,18 @@ fun RefundInfoPage( ) BTTextField( modifier = Modifier - .weight(1.0f) - .onFocusChanged { focusState -> - showContactError = - uiState.contact.isNotEmpty() && - !uiState.isValidContact && - !focusState.isFocused - }, + .weight(1.0f), text = uiState.contact.filterToPhoneNumber(), singleLine = true, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Phone, imeAction = ImeAction.Next ), - isError = showContactError, placeholder = stringResource(id = R.string.ticketing_contact_placeholder), onValueChanged = onContactNumberChanged, visualTransformation = PhoneNumberVisualTransformation('-'), ) } - if (showContactError) { - Text( - modifier = Modifier.padding(start = 56.dp, top = 12.dp), - text = stringResource(id = R.string.validation_contact), - style = MaterialTheme.typography.bodySmall.copy(color = Error), - ) - } } } Section( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundUiState.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundUiState.kt index 20df78d4..cc4af476 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundUiState.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundUiState.kt @@ -10,15 +10,9 @@ data class RefundUiState( val accountNumber: String = "", val reservation: ReservationDetail? = null, ) { - val isValidName: Boolean get() { - val regex = "^[가-힣]{2,10}$".toRegex() - return regex.matches(name) - } + val isValidName: Boolean get() = name.isNotBlank() - val isValidContact: Boolean get() { - val regex = "^0[0-9]{8,10}$".toRegex() - return regex.matches(contact) - } + val isValidContact: Boolean get() = contact.isNotBlank() val isValidAccountNumber: Boolean get() { val regex = "^[0-9]{11,14}$".toRegex() From 02b2994549032ad15137d4355d2e88cad452b0c4 Mon Sep 17 00:00:00 2001 From: algosketch Date: Thu, 29 Feb 2024 04:16:36 +0900 Subject: [PATCH 032/129] =?UTF-8?q?fix=20:=20=EB=B1=83=EC=A7=80=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/component/ShowFeed.kt | 74 +++++++++++-------- presentation/src/main/res/values/strings.xml | 3 +- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt index b1d0da01..e189ba9e 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -28,7 +29,9 @@ import com.nexters.boolti.domain.model.Show import com.nexters.boolti.domain.model.ShowState import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.theme.Grey05 +import com.nexters.boolti.presentation.theme.Grey20 import com.nexters.boolti.presentation.theme.Grey30 +import com.nexters.boolti.presentation.theme.Grey40 import com.nexters.boolti.presentation.theme.Grey80 import com.nexters.boolti.presentation.theme.aggroFamily import java.time.format.DateTimeFormatter @@ -74,35 +77,10 @@ fun ShowFeed( ) } - when (showState) { - is ShowState.WaitingTicketing -> { - Badge( - label = stringResource( - id = R.string.ticketing_button_upcoming_ticket, - showState.dDay - ), - modifier = Modifier.padding(all = 10.dp), - color = Grey05, - containerColor = MaterialTheme.colorScheme.primary, - ) - } - - is ShowState.FinishedShow -> { - Badge( - label = stringResource(id = R.string.finished_show), - modifier = Modifier.padding(all = 10.dp) - ) - } - - is ShowState.ClosedTicketing -> { - Badge( - label = stringResource(id = R.string.ticketing_button_closed_ticket), - modifier = Modifier.padding(all = 10.dp) - ) - } - - else -> {} - } + ShowBadge( + modifier = Modifier.padding(all = 10.dp), + showState = showState + ) } val daysOfWeek = stringArrayResource(id = R.array.days_of_week) @@ -127,3 +105,41 @@ fun ShowFeed( ) } } + +@Composable +private fun ShowBadge( + showState: ShowState, + modifier: Modifier = Modifier, +) { + var dDay: Int? = null + val (color, containerColor, labelId) = when (showState) { + is ShowState.WaitingTicketing -> { + dDay = showState.dDay + Triple( + MaterialTheme.colorScheme.primary, + Grey80, + R.string.ticketing_button_upcoming_ticket, + ) + } + + ShowState.TicketingInProgress -> Triple( + Grey05, + MaterialTheme.colorScheme.primary, + R.string.ticketing_in_progress, + ) + + ShowState.ClosedTicketing -> Triple(Grey80, Grey20, R.string.ticketing_button_closed_ticket) + ShowState.FinishedShow -> Triple(Grey40, Grey80, R.string.finished_show) + } + val label = if (dDay == null) stringResource(labelId) else stringResource(labelId, dDay) + + + Text( + text = label, + modifier = modifier + .clip(RoundedCornerShape(100.dp)) + .background(containerColor.copy(0.9f)) + .padding(horizontal = 12.dp, vertical = 3.dp), + style = MaterialTheme.typography.labelMedium.copy(color = color), + ) +} \ 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 04d10131..fdea1b05 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -27,6 +27,7 @@ 공연 종료 공연명으로 검색해 주세요 입금 확인 중인 티켓이 있어요! + 예매 중 @@ -111,7 +112,7 @@ 이미 예매한 공연 1인 1매만 예매할 수 있어요 예매 시작 D-%d - 예매 마감 + 예매 종료 공연 종료 1인 %d매 결제하기 From 389eb51ecafab5f03b7a8e17cafd923377fec498 Mon Sep 17 00:00:00 2001 From: algosketch Date: Thu, 29 Feb 2024 04:27:02 +0900 Subject: [PATCH 033/129] =?UTF-8?q?fix=20:=20=EA=B3=B5=EC=97=B0=20dim=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/component/ShowFeed.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt index e189ba9e..b686e496 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -15,6 +14,7 @@ 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.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.ContentScale @@ -28,11 +28,13 @@ import coil.compose.AsyncImage import com.nexters.boolti.domain.model.Show import com.nexters.boolti.domain.model.ShowState import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.extension.toPx import com.nexters.boolti.presentation.theme.Grey05 import com.nexters.boolti.presentation.theme.Grey20 import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey40 import com.nexters.boolti.presentation.theme.Grey80 +import com.nexters.boolti.presentation.theme.Grey95 import com.nexters.boolti.presentation.theme.aggroFamily import java.time.format.DateTimeFormatter @@ -65,7 +67,7 @@ fun ShowFeed( contentScale = ContentScale.Crop, ) - if (showState !is ShowState.TicketingInProgress) { + if (showState is ShowState.WaitingTicketing || showState is ShowState.FinishedShow) { Box( modifier = Modifier .fillMaxWidth() @@ -75,6 +77,19 @@ fun ShowFeed( alpha = 0.5f, ) ) + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(210f / 297f) + .background( + brush = Brush.verticalGradient( + listOf(Color.Transparent, Grey95), + startY = 48.dp.toPx(), + ), + alpha = 0.5f, + ) + ) } ShowBadge( From e951c2ab89f56d9efe598c4793d38f32fbba54a6 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Thu, 29 Feb 2024 16:55:24 +0900 Subject: [PATCH 034/129] =?UTF-8?q?Boolti-181=20feat:=20=ED=83=88=ED=87=B4?= =?UTF-8?q?=20=ED=9B=84=2030=EC=9D=BC=20=EC=9D=B4=EB=82=B4=EC=97=90=20?= =?UTF-8?q?=EC=9E=AC=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/network/response/LoginResponse.kt | 11 ++++- .../data/repository/AuthRepositoryImpl.kt | 8 ++-- .../boolti/domain/model/LoginUserState.kt | 6 +++ .../domain/repository/AuthRepository.kt | 5 ++- .../presentation/screen/login/LoginEvent.kt | 1 + .../presentation/screen/login/LoginScreen.kt | 40 ++++++++++++++----- .../screen/login/LoginViewModel.kt | 6 ++- presentation/src/main/res/values/strings.xml | 1 + 8 files changed, 59 insertions(+), 19 deletions(-) create mode 100644 domain/src/main/java/com/nexters/boolti/domain/model/LoginUserState.kt diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/LoginResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/LoginResponse.kt index 73c7eab5..b7634def 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/LoginResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/LoginResponse.kt @@ -1,11 +1,18 @@ package com.nexters.boolti.data.network.response +import com.nexters.boolti.domain.model.LoginUserState import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class LoginResponse( - @SerialName("signUpRequired") val signUpRequired: Boolean, + @SerialName("signUpRequired") val signUpRequired: Boolean = false, + @SerialName("removeCancelled") val signOutCancelled: Boolean = false, @SerialName("accessToken") val accessToken: String?, @SerialName("refreshToken") val refreshToken: String?, -) +) { + fun toDomain(): LoginUserState = LoginUserState( + signUpRequired = signUpRequired, + signOutCancelled = signOutCancelled, + ) +} diff --git a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt index 69378e80..7ac94039 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt @@ -4,6 +4,8 @@ import com.nexters.boolti.data.datasource.AuthDataSource import com.nexters.boolti.data.datasource.SignUpDataSource import com.nexters.boolti.data.datasource.TokenDataSource import com.nexters.boolti.data.datasource.UserDataSource +import com.nexters.boolti.data.network.response.LoginResponse +import com.nexters.boolti.domain.model.LoginUserState import com.nexters.boolti.domain.model.User import com.nexters.boolti.domain.repository.AuthRepository import com.nexters.boolti.domain.request.LoginRequest @@ -26,14 +28,12 @@ class AuthRepositoryImpl @Inject constructor( override val cachedUser: Flow get() = authDataSource.user.map { it?.toDomain() } - override suspend fun kakaoLogin(request: LoginRequest): Result { + override suspend fun kakaoLogin(request: LoginRequest): Result { return authDataSource.login(request) .onSuccess { response -> tokenDataSource.saveTokens(response.accessToken ?: "", response.refreshToken ?: "") } - .mapCatching { - !it.signUpRequired - } + .mapCatching(LoginResponse::toDomain) } override suspend fun logout(): Result = authDataSource.logout() diff --git a/domain/src/main/java/com/nexters/boolti/domain/model/LoginUserState.kt b/domain/src/main/java/com/nexters/boolti/domain/model/LoginUserState.kt new file mode 100644 index 00000000..5d00c6a2 --- /dev/null +++ b/domain/src/main/java/com/nexters/boolti/domain/model/LoginUserState.kt @@ -0,0 +1,6 @@ +package com.nexters.boolti.domain.model + +data class LoginUserState( + val signUpRequired: Boolean = false, + val signOutCancelled: Boolean = false, +) diff --git a/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt b/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt index 81a362be..b579522b 100644 --- a/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt +++ b/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt @@ -1,5 +1,6 @@ package com.nexters.boolti.domain.repository +import com.nexters.boolti.domain.model.LoginUserState import com.nexters.boolti.domain.model.User import com.nexters.boolti.domain.request.LoginRequest import com.nexters.boolti.domain.request.SignUpRequest @@ -13,9 +14,9 @@ interface AuthRepository { * 잘못된 idToken 등의 사유로 로그인에 실패한 경우 400 에러 발생 * * @param request idToken 은 Kakao 로그인 성공 시 내려오는 token - * @return true 면 로그인 가능, false 면 회원가입 필요 + * @return [LoginUserState] 회원가입 여부, 탈퇴 후 재로그인 여부 */ - suspend fun kakaoLogin(request: LoginRequest): Result + suspend fun kakaoLogin(request: LoginRequest): Result suspend fun logout(): Result suspend fun signUp(signUpRequest: SignUpRequest): Result suspend fun signout(request: SignoutRequest): Result diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginEvent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginEvent.kt index 57ec7849..32786b1e 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginEvent.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginEvent.kt @@ -3,5 +3,6 @@ package com.nexters.boolti.presentation.screen.login sealed interface LoginEvent { data object Success : LoginEvent data object RequireSignUp : LoginEvent + data object SignOutCancelled : LoginEvent data object Invalid : LoginEvent } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt index 0fae236d..5235015c 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt @@ -24,6 +24,7 @@ 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.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -34,10 +35,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BTDialog import com.nexters.boolti.presentation.component.KakaoLoginButton import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.theme.Grey30 @@ -53,19 +56,15 @@ fun LoginScreen( ) { val context = LocalContext.current val sheetState = rememberModalBottomSheetState() + var showSignOutCancelledDialog by remember { mutableStateOf(false) } var isSheetOpen by rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.event.collect { when (it) { - LoginEvent.Success -> { - onBackPressed() - } - - LoginEvent.RequireSignUp -> { - isSheetOpen = true - } - + LoginEvent.Success -> onBackPressed() + LoginEvent.RequireSignUp -> isSheetOpen = true + LoginEvent.SignOutCancelled -> showSignOutCancelledDialog = true LoginEvent.Invalid -> Toast.makeText(context, "로그인 실패", Toast.LENGTH_SHORT).show() } } @@ -88,6 +87,10 @@ fun LoginScreen( } } + if (showSignOutCancelledDialog) { + SignOutCancelledDialog { showSignOutCancelledDialog = false } + } + Scaffold( topBar = { LoginAppBar(onBackPressed = onBackPressed) }, containerColor = MaterialTheme.colorScheme.background, @@ -162,7 +165,9 @@ private fun SignUpBottomSheet( modifier = modifier.padding(horizontal = 24.dp), ) { Text( - modifier = Modifier.padding(top = 24.dp, bottom = 12.dp).height(32.dp), + modifier = Modifier + .padding(top = 24.dp, bottom = 12.dp) + .height(32.dp), text = stringResource(id = R.string.signup_greeting), style = MaterialTheme.typography.headlineSmall ) @@ -195,4 +200,19 @@ private fun SignUpBottomSheet( onClick = signUp, ) } -} \ No newline at end of file +} + +@Composable +fun SignOutCancelledDialog(onDismiss: () -> Unit) { + BTDialog( + onDismiss = onDismiss, + onClickPositiveButton = onDismiss, + ) { + Text( + text = stringResource(R.string.signout_cancelled_message), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginViewModel.kt index bc13b333..da0ea68c 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginViewModel.kt @@ -33,7 +33,11 @@ class LoginViewModel @Inject constructor( viewModelScope.launch { authRepository.kakaoLogin(LoginRequest(accessToken)).onSuccess { - if (it) event(LoginEvent.Success) else event(LoginEvent.RequireSignUp) + when { + it.signUpRequired -> event(LoginEvent.RequireSignUp) + it.signOutCancelled -> event(LoginEvent.SignOutCancelled) + else -> event(LoginEvent.Success) + } }.onFailure { Timber.d("login failed: $it") event(LoginEvent.Invalid) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 04d10131..6b6895c9 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -64,6 +64,7 @@ 약관 동의하고 시작하기 불티를 찾아주셔서 감사합니다 불티 유저 + 탈퇴 후 30일 이내에 로그인하여, 계정 삭제가 취소되었어요\n불티를 다시 찾아주셔서 감사해요! 회원 탈퇴 From 334b7ce0fac14ef108bd1971081b4d071d0dd3de Mon Sep 17 00:00:00 2001 From: mangbaam Date: Thu, 29 Feb 2024 17:00:48 +0900 Subject: [PATCH 035/129] =?UTF-8?q?Boolti-181=20feat:=20=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EB=8B=AB=ED=9E=8C=20=ED=9B=84=20?= =?UTF-8?q?=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/presentation/screen/login/LoginScreen.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt index 5235015c..3b6fb020 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt @@ -88,7 +88,10 @@ fun LoginScreen( } if (showSignOutCancelledDialog) { - SignOutCancelledDialog { showSignOutCancelledDialog = false } + SignOutCancelledDialog { + showSignOutCancelledDialog = false + onBackPressed() + } } Scaffold( From a2216fbd69ccb88699806077de1bed50cfbfec5a Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 1 Mar 2024 03:30:40 +0900 Subject: [PATCH 036/129] =?UTF-8?q?refactor=20:=20=ED=8F=AC=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=20=EB=B9=84=EC=9C=A8=20=EC=83=81=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/nexters/boolti/presentation/component/ShowFeed.kt | 7 ++++--- .../nexters/boolti/presentation/constants/ShowConstants.kt | 3 +++ .../boolti/presentation/screen/show/SwipeableImage.kt | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/constants/ShowConstants.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt index b686e496..de993eaf 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt @@ -28,6 +28,7 @@ import coil.compose.AsyncImage import com.nexters.boolti.domain.model.Show import com.nexters.boolti.domain.model.ShowState import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.constants.posterRatio import com.nexters.boolti.presentation.extension.toPx import com.nexters.boolti.presentation.theme.Grey05 import com.nexters.boolti.presentation.theme.Grey20 @@ -57,7 +58,7 @@ fun ShowFeed( contentDescription = stringResource(id = R.string.description_poster), modifier = Modifier .fillMaxWidth() - .aspectRatio(210f / 297f) + .aspectRatio(posterRatio) .clip(RoundedCornerShape(borderRadius)) .border( width = 1.dp, @@ -71,7 +72,7 @@ fun ShowFeed( Box( modifier = Modifier .fillMaxWidth() - .aspectRatio(210f / 297f) + .aspectRatio(posterRatio) .background( brush = SolidColor(Color.Black), alpha = 0.5f, @@ -81,7 +82,7 @@ fun ShowFeed( Box( modifier = Modifier .fillMaxWidth() - .aspectRatio(210f / 297f) + .aspectRatio(posterRatio) .background( brush = Brush.verticalGradient( listOf(Color.Transparent, Grey95), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/constants/ShowConstants.kt b/presentation/src/main/java/com/nexters/boolti/presentation/constants/ShowConstants.kt new file mode 100644 index 00000000..79611f35 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/constants/ShowConstants.kt @@ -0,0 +1,3 @@ +package com.nexters.boolti.presentation.constants + +const val posterRatio = 210f / 297f \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/SwipeableImage.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/SwipeableImage.kt index 2d4d9d8d..f3ce0395 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/SwipeableImage.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/SwipeableImage.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import com.nexters.boolti.presentation.constants.posterRatio import com.nexters.boolti.presentation.theme.Grey95 @OptIn(ExperimentalFoundationApi::class) @@ -47,7 +48,7 @@ fun SwipeableImage( AsyncImage( modifier = Modifier .fillMaxWidth() - .aspectRatio(210f / 297f) + .aspectRatio(posterRatio) .clickable( interactionSource = interactionSource, indication = null, From a5bb142161eced4fc6e475e62868ddcc3c479fb9 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 1 Mar 2024 04:01:31 +0900 Subject: [PATCH 037/129] =?UTF-8?q?refactor=20:=20=ED=99=95=EC=9E=A5=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=B4=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/presentation/extension/Activity.kt | 2 +- .../com/nexters/boolti/presentation/extension/Context.kt | 2 +- .../presentation/service/BtFirebaseMessagingService.kt | 9 ++------- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/extension/Activity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/extension/Activity.kt index 3688ca64..3ea6b825 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/extension/Activity.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/extension/Activity.kt @@ -5,7 +5,7 @@ import android.content.Intent import androidx.core.app.ActivityCompat fun Activity.requestPermission(permission: String, requestCode: Int) { - if (!checkGranted(permission)) { + if (!checkGrantedPermission(permission)) { ActivityCompat.requestPermissions( this, arrayOf(permission), requestCode, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/extension/Context.kt b/presentation/src/main/java/com/nexters/boolti/presentation/extension/Context.kt index 407a4667..6a71a2ea 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/extension/Context.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/extension/Context.kt @@ -21,6 +21,6 @@ fun Context.requireActivity(): Activity { ) } -fun Context.checkGranted(permission: String): Boolean { +fun Context.checkGrantedPermission(permission: String): Boolean { return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt index aeb56b26..ed3c5958 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt @@ -1,14 +1,13 @@ package com.nexters.boolti.presentation.service import android.Manifest -import android.content.pm.PackageManager -import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.nexters.boolti.domain.repository.AuthRepository import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.extension.checkGrantedPermission import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -31,11 +30,7 @@ class BtFirebaseMessagingService : FirebaseMessagingService() { } override fun onMessageReceived(remoteMessage: RemoteMessage) { - if (ActivityCompat.checkSelfPermission( - this, - Manifest.permission.POST_NOTIFICATIONS - ) != PackageManager.PERMISSION_GRANTED - ) return + if(!checkGrantedPermission(Manifest.permission.POST_NOTIFICATIONS)) return remoteMessage.notification?.let { notification -> val defaultChannelId = getString(R.string.default_notification_channel_id) From bfc6300d05b439b562658f8a4a10f338cc2a80b6 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 1 Mar 2024 05:58:21 +0900 Subject: [PATCH 038/129] =?UTF-8?q?refactor=20:=20data=20layer=EC=97=90=20?= =?UTF-8?q?internal=20=EC=A0=91=EA=B7=BC=20=EC=A0=9C=ED=95=9C=EC=9E=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/nexters/boolti/data/datasource/AuthDataSource.kt | 2 +- .../nexters/boolti/data/datasource/DeviceTokenDataSource.kt | 4 ++-- .../java/com/nexters/boolti/data/datasource/HostDataSource.kt | 2 +- .../com/nexters/boolti/data/datasource/PolicyDataSource.kt | 2 +- .../nexters/boolti/data/datasource/RemoteConfigDataSource.kt | 2 +- .../nexters/boolti/data/datasource/ReservationDataSource.kt | 4 ++-- .../java/com/nexters/boolti/data/datasource/ShowDataSource.kt | 4 ++-- .../com/nexters/boolti/data/datasource/SignUpDataSource.kt | 4 ++-- .../com/nexters/boolti/data/datasource/TicketDataSource.kt | 2 +- .../com/nexters/boolti/data/datasource/TicketingDataSource.kt | 2 +- .../com/nexters/boolti/data/datasource/TokenDataSource.kt | 2 +- .../java/com/nexters/boolti/data/datasource/UserDataSource.kt | 2 +- data/src/main/java/com/nexters/boolti/data/db/AppSettings.kt | 4 ++-- data/src/main/java/com/nexters/boolti/data/db/DataStore.kt | 2 +- .../main/java/com/nexters/boolti/data/di/DataSourceModule.kt | 2 +- .../main/java/com/nexters/boolti/data/di/FirebaseModule.kt | 2 +- .../src/main/java/com/nexters/boolti/data/di/NetworkModule.kt | 2 +- .../main/java/com/nexters/boolti/data/di/RepositoryModule.kt | 2 +- .../java/com/nexters/boolti/data/network/AuthAuthenticator.kt | 2 +- .../java/com/nexters/boolti/data/network/AuthInterceptor.kt | 2 +- .../com/nexters/boolti/data/network/api/DeviceTokenService.kt | 4 ++-- .../java/com/nexters/boolti/data/network/api/HostService.kt | 2 +- .../java/com/nexters/boolti/data/network/api/LoginService.kt | 2 +- .../com/nexters/boolti/data/network/api/ReservationService.kt | 4 ++-- .../java/com/nexters/boolti/data/network/api/ShowService.kt | 2 +- .../java/com/nexters/boolti/data/network/api/SignUpService.kt | 4 ++-- .../java/com/nexters/boolti/data/network/api/TicketService.kt | 2 +- .../com/nexters/boolti/data/network/api/TicketingService.kt | 2 +- .../java/com/nexters/boolti/data/network/api/UserService.kt | 2 +- .../nexters/boolti/data/network/request/DeviceTokenRequest.kt | 2 +- .../com/nexters/boolti/data/network/request/RefreshRequest.kt | 2 +- .../data/network/request/ReservationInviteTicketRequest.kt | 4 ++-- .../data/network/request/ReservationSalesTicketRequest.kt | 4 ++-- .../boolti/data/network/response/CheckInviteCodeResponse.kt | 2 +- .../boolti/data/network/response/DeviceTokenResponse.kt | 4 ++-- .../com/nexters/boolti/data/network/response/HostedShowDto.kt | 2 +- .../com/nexters/boolti/data/network/response/ImageResponse.kt | 4 ++-- .../com/nexters/boolti/data/network/response/LoginResponse.kt | 2 +- .../nexters/boolti/data/network/response/ManagerCodeDto.kt | 2 +- .../boolti/data/network/response/ReservationDetailResponse.kt | 2 +- .../nexters/boolti/data/network/response/ReservationDto.kt | 2 +- .../boolti/data/network/response/ReservationResponse.kt | 4 ++-- .../boolti/data/network/response/SalesTicketTypesDto.kt | 2 +- .../boolti/data/network/response/ShowDetailResponse.kt | 4 ++-- .../com/nexters/boolti/data/network/response/ShowResponse.kt | 4 ++-- .../nexters/boolti/data/network/response/SignUpResponse.kt | 2 +- .../com/nexters/boolti/data/network/response/TicketDto.kt | 2 +- .../nexters/boolti/data/network/response/TicketingInfoDto.kt | 2 +- .../com/nexters/boolti/data/network/response/UserResponse.kt | 2 +- .../com/nexters/boolti/data/repository/AuthRepositoryImpl.kt | 2 +- .../nexters/boolti/data/repository/ConfigRepositoryImpl.kt | 2 +- .../com/nexters/boolti/data/repository/HostRepositoryImpl.kt | 2 +- .../boolti/data/repository/ReservationRepositoryImpl.kt | 4 ++-- .../com/nexters/boolti/data/repository/ShowRepositoryImpl.kt | 4 ++-- .../nexters/boolti/data/repository/TicketRepositoryImpl.kt | 2 +- .../nexters/boolti/data/repository/TicketingRepositoryImpl.kt | 2 +- 56 files changed, 73 insertions(+), 73 deletions(-) diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt index 6ff528c1..4de3e1ac 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import javax.inject.Inject -class AuthDataSource @Inject constructor( +internal class AuthDataSource @Inject constructor( private val context: Context, private val loginService: LoginService, ) { diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/DeviceTokenDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/DeviceTokenDataSource.kt index baed798c..954c96be 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/DeviceTokenDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/DeviceTokenDataSource.kt @@ -10,7 +10,7 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -class DeviceTokenDataSource @Inject constructor( +internal class DeviceTokenDataSource @Inject constructor( private val deviceTokenService: DeviceTokenService, ) { suspend fun sendFcmToken(): Result = runCatching { @@ -34,4 +34,4 @@ class DeviceTokenDataSource @Inject constructor( continuation.resume(token) }) } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/HostDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/HostDataSource.kt index 2bf05064..f297ac81 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/HostDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/HostDataSource.kt @@ -7,7 +7,7 @@ import com.nexters.boolti.domain.request.QrScanRequest import retrofit2.Response import javax.inject.Inject -class HostDataSource @Inject constructor( +internal class HostDataSource @Inject constructor( private val apiService: HostService, ) { suspend fun requestEntrance(request: QrScanRequest): Response = apiService.requestEntrance(request) diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/PolicyDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/PolicyDataSource.kt index e56a8a18..7311b486 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/PolicyDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/PolicyDataSource.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject -class PolicyDataSource @Inject constructor( +internal class PolicyDataSource @Inject constructor( private val context: Context, ) { private val dataStore: DataStore diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/RemoteConfigDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/RemoteConfigDataSource.kt index 6dcfa1f0..b848cebc 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/RemoteConfigDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/RemoteConfigDataSource.kt @@ -7,7 +7,7 @@ import timber.log.Timber import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -class RemoteConfigDataSource( +internal class RemoteConfigDataSource( private val remoteConfig: FirebaseRemoteConfig, ) { diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/ReservationDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/ReservationDataSource.kt index 54d9d5bd..3d586b24 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/ReservationDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/ReservationDataSource.kt @@ -6,7 +6,7 @@ import com.nexters.boolti.data.network.response.ReservationResponse import com.nexters.boolti.domain.request.RefundRequest import javax.inject.Inject -class ReservationDataSource @Inject constructor( +internal class ReservationDataSource @Inject constructor( private val reservationService: ReservationService, ) { suspend fun getReservations(): List = reservationService.getReservations() @@ -15,4 +15,4 @@ class ReservationDataSource @Inject constructor( reservationService.findReservationById(id) suspend fun refund(request: RefundRequest) = reservationService.refund(request) -} \ No newline at end of file +} diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/ShowDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/ShowDataSource.kt index 569d8c44..4ba15b99 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/ShowDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/ShowDataSource.kt @@ -5,7 +5,7 @@ import com.nexters.boolti.data.network.response.ShowDetailResponse import com.nexters.boolti.data.network.response.ShowResponse import javax.inject.Inject -class ShowDataSource @Inject constructor( +internal class ShowDataSource @Inject constructor( private val showService: ShowService, ) { suspend fun search(keyword: String): Result> = runCatching { @@ -15,4 +15,4 @@ class ShowDataSource @Inject constructor( suspend fun findShowById(id: String): Result = runCatching { showService.findShowById(id) } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/SignUpDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/SignUpDataSource.kt index 2a115338..116dc041 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/SignUpDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/SignUpDataSource.kt @@ -5,10 +5,10 @@ import com.nexters.boolti.data.network.response.SignUpResponse import com.nexters.boolti.domain.request.SignUpRequest import javax.inject.Inject -class SignUpDataSource @Inject constructor( +internal class SignUpDataSource @Inject constructor( private val signUpService: SignUpService, ) { suspend fun signUp(signUpRequest: SignUpRequest): Result = runCatching { signUpService.signup(signUpRequest) } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/TicketDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/TicketDataSource.kt index d78aaa9c..82380852 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/TicketDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/TicketDataSource.kt @@ -4,7 +4,7 @@ import com.nexters.boolti.data.network.api.TicketService import com.nexters.boolti.domain.model.Ticket import javax.inject.Inject -class TicketDataSource @Inject constructor( +internal class TicketDataSource @Inject constructor( private val apiService: TicketService, ) { suspend fun getTickets(): List = apiService.getTickets().map { it.toDomain() } diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/TicketingDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/TicketingDataSource.kt index 4b7b79dd..efffac41 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/TicketingDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/TicketingDataSource.kt @@ -12,7 +12,7 @@ import com.nexters.boolti.domain.request.TicketingInfoRequest import retrofit2.Response import javax.inject.Inject -class TicketingDataSource @Inject constructor( +internal class TicketingDataSource @Inject constructor( private val ticketingService: TicketingService, ) { suspend fun getSalesTickets(request: SalesTicketRequest): List { diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt index b25cd10c..996dfe9e 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/TokenDataSource.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import javax.inject.Inject -class TokenDataSource @Inject constructor( +internal class TokenDataSource @Inject constructor( private val context: Context, ) { private val dataStore: DataStore diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/UserDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/UserDataSource.kt index cc94a3eb..4073dcd9 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/UserDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/UserDataSource.kt @@ -5,7 +5,7 @@ import com.nexters.boolti.data.network.response.UserResponse import com.nexters.boolti.domain.request.SignoutRequest import javax.inject.Inject -class UserDataSource @Inject constructor( +internal class UserDataSource @Inject constructor( private val userService: UserService, ) { suspend fun getUser(): UserResponse = userService.getUser() diff --git a/data/src/main/java/com/nexters/boolti/data/db/AppSettings.kt b/data/src/main/java/com/nexters/boolti/data/db/AppSettings.kt index 490f782f..149ff4fd 100644 --- a/data/src/main/java/com/nexters/boolti/data/db/AppSettings.kt +++ b/data/src/main/java/com/nexters/boolti/data/db/AppSettings.kt @@ -8,7 +8,7 @@ import java.io.InputStream import java.io.OutputStream @Serializable -data class AppSettings( +internal data class AppSettings( val userId: String? = null, val loginType: String? = null, val nickname: String? = null, @@ -20,7 +20,7 @@ data class AppSettings( val refundPolicy: List = emptyList(), ) -object AppSettingsSerializer : Serializer { +internal object AppSettingsSerializer : Serializer { override val defaultValue: AppSettings = AppSettings() diff --git a/data/src/main/java/com/nexters/boolti/data/db/DataStore.kt b/data/src/main/java/com/nexters/boolti/data/db/DataStore.kt index 5d905780..b21a8d2a 100644 --- a/data/src/main/java/com/nexters/boolti/data/db/DataStore.kt +++ b/data/src/main/java/com/nexters/boolti/data/db/DataStore.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.dataStore -val Context.dataStore: DataStore by dataStore( +internal val Context.dataStore: DataStore by dataStore( "app-settings.json", AppSettingsSerializer, ) diff --git a/data/src/main/java/com/nexters/boolti/data/di/DataSourceModule.kt b/data/src/main/java/com/nexters/boolti/data/di/DataSourceModule.kt index 235e56f2..fe59d845 100644 --- a/data/src/main/java/com/nexters/boolti/data/di/DataSourceModule.kt +++ b/data/src/main/java/com/nexters/boolti/data/di/DataSourceModule.kt @@ -16,7 +16,7 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module -object DataSourceModule { +internal object DataSourceModule { @Singleton @Provides fun provideRemoteConfigDataSource(remoteConfig: FirebaseRemoteConfig): RemoteConfigDataSource = diff --git a/data/src/main/java/com/nexters/boolti/data/di/FirebaseModule.kt b/data/src/main/java/com/nexters/boolti/data/di/FirebaseModule.kt index a9e2a65d..27463b05 100644 --- a/data/src/main/java/com/nexters/boolti/data/di/FirebaseModule.kt +++ b/data/src/main/java/com/nexters/boolti/data/di/FirebaseModule.kt @@ -11,7 +11,7 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module -object FirebaseModule { +internal object FirebaseModule { @Singleton @Provides fun provideRemoteConfig(): FirebaseRemoteConfig { diff --git a/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt b/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt index 75adc147..595d7f12 100644 --- a/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt +++ b/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt @@ -31,7 +31,7 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module -object NetworkModule { +internal object NetworkModule { @Singleton @Provides @Named("auth") diff --git a/data/src/main/java/com/nexters/boolti/data/di/RepositoryModule.kt b/data/src/main/java/com/nexters/boolti/data/di/RepositoryModule.kt index 33646dc5..dba19763 100644 --- a/data/src/main/java/com/nexters/boolti/data/di/RepositoryModule.kt +++ b/data/src/main/java/com/nexters/boolti/data/di/RepositoryModule.kt @@ -21,7 +21,7 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) @Module -abstract class RepositoryModule { +internal abstract class RepositoryModule { @Binds abstract fun bindConfigRepository(repository: ConfigRepositoryImpl): ConfigRepository diff --git a/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt b/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt index d1e736e0..fe291b9c 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt @@ -9,7 +9,7 @@ import okhttp3.Response import okhttp3.Route import javax.inject.Inject -class AuthAuthenticator @Inject constructor( +internal class AuthAuthenticator @Inject constructor( private val tokenDataSource: TokenDataSource, private val authDataSource: AuthDataSource, ) : Authenticator { diff --git a/data/src/main/java/com/nexters/boolti/data/network/AuthInterceptor.kt b/data/src/main/java/com/nexters/boolti/data/network/AuthInterceptor.kt index 22e27439..4275709e 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/AuthInterceptor.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/AuthInterceptor.kt @@ -6,7 +6,7 @@ import okhttp3.Interceptor import okhttp3.Response import javax.inject.Inject -class AuthInterceptor @Inject constructor( +internal class AuthInterceptor @Inject constructor( private val tokenDataSource: TokenDataSource, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { diff --git a/data/src/main/java/com/nexters/boolti/data/network/api/DeviceTokenService.kt b/data/src/main/java/com/nexters/boolti/data/network/api/DeviceTokenService.kt index 12d932f9..78e4c6f3 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/api/DeviceTokenService.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/api/DeviceTokenService.kt @@ -7,7 +7,7 @@ import retrofit2.Response import retrofit2.http.Body import retrofit2.http.POST -interface DeviceTokenService { +internal interface DeviceTokenService { @POST("/app/papi/v1/device-token") suspend fun postFcmToken(@Body request: DeviceTokenRequest): Response -} \ No newline at end of file +} diff --git a/data/src/main/java/com/nexters/boolti/data/network/api/HostService.kt b/data/src/main/java/com/nexters/boolti/data/network/api/HostService.kt index a3990c39..cb2b6fa7 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/api/HostService.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/api/HostService.kt @@ -10,7 +10,7 @@ import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Path -interface HostService { +internal interface HostService { @GET("/app/api/v1/host/shows") suspend fun getHostedShows(): List diff --git a/data/src/main/java/com/nexters/boolti/data/network/api/LoginService.kt b/data/src/main/java/com/nexters/boolti/data/network/api/LoginService.kt index 039a7778..5a85eb7d 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/api/LoginService.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/api/LoginService.kt @@ -8,7 +8,7 @@ import retrofit2.Response import retrofit2.http.Body import retrofit2.http.POST -interface LoginService { +internal interface LoginService { @POST("/app/papi/v1/login/kakao") suspend fun kakaoLogin(@Body request: LoginRequest): LoginResponse diff --git a/data/src/main/java/com/nexters/boolti/data/network/api/ReservationService.kt b/data/src/main/java/com/nexters/boolti/data/network/api/ReservationService.kt index 8ba95c3b..7e594e98 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/api/ReservationService.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/api/ReservationService.kt @@ -8,7 +8,7 @@ import retrofit2.http.GET import retrofit2.http.PATCH import retrofit2.http.Path -interface ReservationService { +internal interface ReservationService { @GET("/app/api/v1/reservations") suspend fun getReservations(): List @@ -17,4 +17,4 @@ interface ReservationService { @PATCH("/app/api/v1/reservation/refund") suspend fun refund(@Body request: RefundRequest): Boolean -} \ No newline at end of file +} diff --git a/data/src/main/java/com/nexters/boolti/data/network/api/ShowService.kt b/data/src/main/java/com/nexters/boolti/data/network/api/ShowService.kt index 81c5e54e..81025cf3 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/api/ShowService.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/api/ShowService.kt @@ -6,7 +6,7 @@ import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query -interface ShowService { +internal interface ShowService { @GET("/app/papi/v1/shows/search") suspend fun search(@Query("nameLike") keyword: String): List diff --git a/data/src/main/java/com/nexters/boolti/data/network/api/SignUpService.kt b/data/src/main/java/com/nexters/boolti/data/network/api/SignUpService.kt index 15977cb0..08989e8f 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/api/SignUpService.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/api/SignUpService.kt @@ -5,7 +5,7 @@ import com.nexters.boolti.domain.request.SignUpRequest import retrofit2.http.Body import retrofit2.http.POST -interface SignUpService { +internal interface SignUpService { @POST("/app/papi/v1/signup/sns") suspend fun signup(@Body request: SignUpRequest): SignUpResponse -} \ No newline at end of file +} diff --git a/data/src/main/java/com/nexters/boolti/data/network/api/TicketService.kt b/data/src/main/java/com/nexters/boolti/data/network/api/TicketService.kt index 901b0e70..a2ee4827 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/api/TicketService.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/api/TicketService.kt @@ -5,7 +5,7 @@ import com.nexters.boolti.data.network.response.TicketDto import retrofit2.http.GET import retrofit2.http.Path -interface TicketService { +internal interface TicketService { @GET("/app/api/v1/tickets") suspend fun getTickets(): List diff --git a/data/src/main/java/com/nexters/boolti/data/network/api/TicketingService.kt b/data/src/main/java/com/nexters/boolti/data/network/api/TicketingService.kt index b9466904..081d3eab 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/api/TicketingService.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/api/TicketingService.kt @@ -13,7 +13,7 @@ import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query -interface TicketingService { +internal interface TicketingService { @GET("app/api/v1/sales-ticket-type/{showId}") suspend fun getSalesTickets( @Path("showId") showId: String, diff --git a/data/src/main/java/com/nexters/boolti/data/network/api/UserService.kt b/data/src/main/java/com/nexters/boolti/data/network/api/UserService.kt index 51d17ffd..8cfc7032 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/api/UserService.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/api/UserService.kt @@ -6,7 +6,7 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.HTTP -interface UserService { +internal interface UserService { @GET("/app/api/v1/user") suspend fun getUser(): UserResponse diff --git a/data/src/main/java/com/nexters/boolti/data/network/request/DeviceTokenRequest.kt b/data/src/main/java/com/nexters/boolti/data/network/request/DeviceTokenRequest.kt index faff6094..172135f5 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/request/DeviceTokenRequest.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/request/DeviceTokenRequest.kt @@ -3,7 +3,7 @@ package com.nexters.boolti.data.network.request import kotlinx.serialization.Serializable @Serializable -data class DeviceTokenRequest( +internal data class DeviceTokenRequest( val deviceToken: String, val deviceType: String, ) diff --git a/data/src/main/java/com/nexters/boolti/data/network/request/RefreshRequest.kt b/data/src/main/java/com/nexters/boolti/data/network/request/RefreshRequest.kt index c88bc120..f23d2bef 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/request/RefreshRequest.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/request/RefreshRequest.kt @@ -3,6 +3,6 @@ package com.nexters.boolti.data.network.request import kotlinx.serialization.Serializable @Serializable -data class RefreshRequest( +internal data class RefreshRequest( val refreshToken: String, ) diff --git a/data/src/main/java/com/nexters/boolti/data/network/request/ReservationInviteTicketRequest.kt b/data/src/main/java/com/nexters/boolti/data/network/request/ReservationInviteTicketRequest.kt index 36353fd0..ae9f1495 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/request/ReservationInviteTicketRequest.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/request/ReservationInviteTicketRequest.kt @@ -4,7 +4,7 @@ import com.nexters.boolti.domain.request.TicketingRequest import kotlinx.serialization.Serializable @Serializable -data class ReservationInviteTicketRequest( +internal data class ReservationInviteTicketRequest( val userId: String, val showId: String, val salesTicketTypeId: String, @@ -13,7 +13,7 @@ data class ReservationInviteTicketRequest( val inviteCode: String, ) -fun TicketingRequest.Invite.toData(): ReservationInviteTicketRequest { +internal fun TicketingRequest.Invite.toData(): ReservationInviteTicketRequest { return ReservationInviteTicketRequest( userId = userId, showId = showId, diff --git a/data/src/main/java/com/nexters/boolti/data/network/request/ReservationSalesTicketRequest.kt b/data/src/main/java/com/nexters/boolti/data/network/request/ReservationSalesTicketRequest.kt index fad0b578..6a9b57d1 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/request/ReservationSalesTicketRequest.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/request/ReservationSalesTicketRequest.kt @@ -5,7 +5,7 @@ import com.nexters.boolti.domain.request.TicketingRequest import kotlinx.serialization.Serializable @Serializable -data class ReservationSalesTicketRequest( +internal data class ReservationSalesTicketRequest( val userId: String, val showId: String, val salesTicketTypeId: String, @@ -18,7 +18,7 @@ data class ReservationSalesTicketRequest( val means: String, ) -fun TicketingRequest.Normal.toData(): ReservationSalesTicketRequest = ReservationSalesTicketRequest( +internal fun TicketingRequest.Normal.toData(): ReservationSalesTicketRequest = ReservationSalesTicketRequest( userId = userId, showId = showId, salesTicketTypeId = salesTicketTypeId, diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/CheckInviteCodeResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/CheckInviteCodeResponse.kt index 68a1db8c..8cc8f994 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/CheckInviteCodeResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/CheckInviteCodeResponse.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class CheckInviteCodeResponse( +internal data class CheckInviteCodeResponse( @SerialName("id") val id: String, @SerialName("code") val inviteCode: String, @SerialName("isUsed") val isUsed: Boolean, diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/DeviceTokenResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/DeviceTokenResponse.kt index 412d6299..ac450aed 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/DeviceTokenResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/DeviceTokenResponse.kt @@ -3,6 +3,6 @@ package com.nexters.boolti.data.network.response import kotlinx.serialization.Serializable @Serializable -data class DeviceTokenResponse( +internal data class DeviceTokenResponse( val tokenId: String -) \ No newline at end of file +) diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/HostedShowDto.kt b/data/src/main/java/com/nexters/boolti/data/network/response/HostedShowDto.kt index f7251d8b..29708fe8 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/HostedShowDto.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/HostedShowDto.kt @@ -7,7 +7,7 @@ import java.time.LocalDate import java.time.LocalDateTime @Serializable -data class HostedShowDto( +internal data class HostedShowDto( @SerialName("showId") val showId: String, @SerialName("showName") val showName: String, ) { diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/ImageResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/ImageResponse.kt index 73486a74..3d4b2b51 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/ImageResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/ImageResponse.kt @@ -4,7 +4,7 @@ import com.nexters.boolti.domain.model.ImagePair import kotlinx.serialization.Serializable @Serializable -data class ImageResponse( +internal data class ImageResponse( val id: String, val path: String, val thumbnailPath: String, @@ -19,7 +19,7 @@ data class ImageResponse( } } -fun List.toDomains(): List { +internal fun List.toDomains(): List { return this.asSequence() .sortedBy { it.sequence } .map { it.toDomain() } diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/LoginResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/LoginResponse.kt index 73c7eab5..0353b642 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/LoginResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/LoginResponse.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class LoginResponse( +internal data class LoginResponse( @SerialName("signUpRequired") val signUpRequired: Boolean, @SerialName("accessToken") val accessToken: String?, @SerialName("refreshToken") val refreshToken: String?, diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/ManagerCodeDto.kt b/data/src/main/java/com/nexters/boolti/data/network/response/ManagerCodeDto.kt index c23a3703..0a41d27d 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/ManagerCodeDto.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/ManagerCodeDto.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ManagerCodeDto( +internal data class ManagerCodeDto( @SerialName("managerCode") val code: String, ) { fun toDomain(): String = code diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt index e11ab0cd..3ab1b0be 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt @@ -7,7 +7,7 @@ import com.nexters.boolti.domain.model.ReservationDetail import kotlinx.serialization.Serializable @Serializable -data class ReservationDetailResponse( +internal data class ReservationDetailResponse( val reservationId: String, val showImg: String, val showName: String, diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDto.kt b/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDto.kt index 3cfbf9c0..c236e5d2 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDto.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDto.kt @@ -3,6 +3,6 @@ package com.nexters.boolti.data.network.response import kotlinx.serialization.Serializable @Serializable -data class ReservationDto( +internal data class ReservationDto( val reservationId: String, ) diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/ReservationResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/ReservationResponse.kt index 35dfd656..8cb48216 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/ReservationResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/ReservationResponse.kt @@ -6,7 +6,7 @@ import com.nexters.boolti.domain.model.Reservation import kotlinx.serialization.Serializable @Serializable -data class ReservationResponse( +internal data class ReservationResponse( val reservationId: String, val reservationStatus: String, val reservationDate: String, @@ -30,4 +30,4 @@ data class ReservationResponse( } } -fun List.toDomains(): List = this.map { it.toDomain() } \ No newline at end of file +internal fun List.toDomains(): List = this.map { it.toDomain() } diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/SalesTicketTypesDto.kt b/data/src/main/java/com/nexters/boolti/data/network/response/SalesTicketTypesDto.kt index ba018287..94aa8fde 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/SalesTicketTypesDto.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/SalesTicketTypesDto.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class SalesTicketTypeDto( +internal data class SalesTicketTypeDto( @SerialName("id") val id: String, @SerialName("showId") val showId: String, @SerialName("ticketType") val ticketType: String, diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/ShowDetailResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/ShowDetailResponse.kt index a530aef0..3f4e6de3 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/ShowDetailResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/ShowDetailResponse.kt @@ -6,7 +6,7 @@ import com.nexters.boolti.domain.model.ShowDetail import kotlinx.serialization.Serializable @Serializable -data class ShowDetailResponse( +internal data class ShowDetailResponse( val id: String, val name: String, val placeName: String, @@ -40,4 +40,4 @@ data class ShowDetailResponse( isReserved = reservationStatus, ) } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/ShowResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/ShowResponse.kt index 20946e9b..f832d905 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/ShowResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/ShowResponse.kt @@ -6,7 +6,7 @@ import com.nexters.boolti.domain.model.Show import kotlinx.serialization.Serializable @Serializable -data class ShowResponse( +internal data class ShowResponse( val id: String, val name: String, val date: String, @@ -26,4 +26,4 @@ data class ShowResponse( } } -fun List.toDomains(): List = this.map { it.toDomain() } +internal fun List.toDomains(): List = this.map { it.toDomain() } diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/SignUpResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/SignUpResponse.kt index a451e04a..b597ab04 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/SignUpResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/SignUpResponse.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class SignUpResponse( +internal data class SignUpResponse( @SerialName("accessToken") val accessToken: String, @SerialName("refreshToken") val refreshToken: String, ) diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/TicketDto.kt b/data/src/main/java/com/nexters/boolti/data/network/response/TicketDto.kt index 52353f60..5fd738ed 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/TicketDto.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/TicketDto.kt @@ -7,7 +7,7 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter @Serializable -data class TicketDto( +internal data class TicketDto( val userId: String, val ticketId: String, val showName: String, diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/TicketingInfoDto.kt b/data/src/main/java/com/nexters/boolti/data/network/response/TicketingInfoDto.kt index 391c3a9c..6e2f8464 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/TicketingInfoDto.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/TicketingInfoDto.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class TicketingInfoDto( +internal data class TicketingInfoDto( @SerialName("meansType") val meansType: String, @SerialName("salesTicketType") diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/UserResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/UserResponse.kt index acb8968c..d5182e35 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/UserResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/UserResponse.kt @@ -4,7 +4,7 @@ import com.nexters.boolti.domain.model.User import kotlinx.serialization.Serializable @Serializable -data class UserResponse( +internal data class UserResponse( val id: String, val nickname: String? = null, val email: String? = null, diff --git a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt index 5922bfae..4e147e3f 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import javax.inject.Inject -class AuthRepositoryImpl @Inject constructor( +internal class AuthRepositoryImpl @Inject constructor( private val authDataSource: AuthDataSource, private val tokenDataSource: TokenDataSource, private val signUpDataSource: SignUpDataSource, diff --git a/data/src/main/java/com/nexters/boolti/data/repository/ConfigRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/ConfigRepositoryImpl.kt index f32312f8..50c0b20d 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/ConfigRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/ConfigRepositoryImpl.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.flow import timber.log.Timber import javax.inject.Inject -class ConfigRepositoryImpl @Inject constructor( +internal class ConfigRepositoryImpl @Inject constructor( private val remoteConfigDataSource: RemoteConfigDataSource, private val policyDataSource: PolicyDataSource, ) : ConfigRepository { diff --git a/data/src/main/java/com/nexters/boolti/data/repository/HostRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/HostRepositoryImpl.kt index 59a696d3..e39f6a4c 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/HostRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/HostRepositoryImpl.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import javax.inject.Inject -class HostRepositoryImpl @Inject constructor( +internal class HostRepositoryImpl @Inject constructor( private val dataSource: HostDataSource, ) : HostRepository { override fun requestEntrance(request: QrScanRequest): Flow = flow { diff --git a/data/src/main/java/com/nexters/boolti/data/repository/ReservationRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/ReservationRepositoryImpl.kt index 6709f9a1..371a2d7a 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/ReservationRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/ReservationRepositoryImpl.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import javax.inject.Inject -class ReservationRepositoryImpl @Inject constructor( +internal class ReservationRepositoryImpl @Inject constructor( private val reservationDataSource: ReservationDataSource, ) : ReservationRepository { override fun getReservations(): Flow> = flow { @@ -29,4 +29,4 @@ class ReservationRepositoryImpl @Inject constructor( throw RuntimeException("환불 실패") } } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/nexters/boolti/data/repository/ShowRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/ShowRepositoryImpl.kt index d1042752..dd276245 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/ShowRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/ShowRepositoryImpl.kt @@ -7,7 +7,7 @@ import com.nexters.boolti.domain.model.ShowDetail import com.nexters.boolti.domain.repository.ShowRepository import javax.inject.Inject -class ShowRepositoryImpl @Inject constructor( +internal class ShowRepositoryImpl @Inject constructor( private val showDateSource: ShowDataSource, ) : ShowRepository { override suspend fun search(keyword: String): Result> { @@ -21,4 +21,4 @@ class ShowRepositoryImpl @Inject constructor( it.toDomain() } } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/nexters/boolti/data/repository/TicketRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/TicketRepositoryImpl.kt index da69674b..f5828026 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/TicketRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/TicketRepositoryImpl.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import javax.inject.Inject -class TicketRepositoryImpl @Inject constructor( +internal class TicketRepositoryImpl @Inject constructor( private val dataSource: TicketDataSource, private val hostDataSource: HostDataSource, ) : TicketRepository { diff --git a/data/src/main/java/com/nexters/boolti/data/repository/TicketingRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/TicketingRepositoryImpl.kt index 75c3c66f..5514c2b5 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/TicketingRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/TicketingRepositoryImpl.kt @@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import javax.inject.Inject -class TicketingRepositoryImpl @Inject constructor( +internal class TicketingRepositoryImpl @Inject constructor( private val dataSource: TicketingDataSource, private val reservationDataSource: ReservationDataSource, ) : TicketingRepository { From 2b16728bbe58801362ac2bdce1eac530d5134006 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 1 Mar 2024 06:00:20 +0900 Subject: [PATCH 039/129] =?UTF-8?q?refactor=20:=20=EC=95=88=20=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20UseCase=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/domain/usecase/IsLoggedInUseCase.kt | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 domain/src/main/java/com/nexters/boolti/domain/usecase/IsLoggedInUseCase.kt diff --git a/domain/src/main/java/com/nexters/boolti/domain/usecase/IsLoggedInUseCase.kt b/domain/src/main/java/com/nexters/boolti/domain/usecase/IsLoggedInUseCase.kt deleted file mode 100644 index 86ad9ab6..00000000 --- a/domain/src/main/java/com/nexters/boolti/domain/usecase/IsLoggedInUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.nexters.boolti.domain.usecase - -import com.nexters.boolti.domain.repository.AuthRepository -import kotlinx.coroutines.flow.Flow -import javax.inject.Inject - -class IsLoggedInUseCase @Inject constructor( - private val authRepository: AuthRepository, -) { - operator fun invoke(): Flow = authRepository.loggedIn -} From b71b146133bc53d511167ac821382bce747a6e85 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 1 Mar 2024 08:12:42 +0900 Subject: [PATCH 040/129] =?UTF-8?q?refactor=20:=20MainDestination=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/MainDestination.kt | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt new file mode 100644 index 00000000..2676f1a0 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt @@ -0,0 +1,64 @@ +package com.nexters.boolti.presentation.screen + +import androidx.navigation.NavType +import androidx.navigation.navArgument + +sealed class MainDestination(val route: String) { + data object Home : MainDestination(route = "home") + data object ShowDetail : MainDestination(route = "shows") { + val arguments = listOf(navArgument(showId) { type = NavType.StringType }) + } + + data object Ticketing : MainDestination(route = "ticketing") { + val arguments = listOf( + navArgument(showId) { type = NavType.StringType }, + navArgument(salesTicketId) { type = NavType.StringType }, + navArgument(ticketCount) { type = NavType.IntType }, + navArgument(isInviteTicket) { type = NavType.BoolType }, + ) + } + + data object Payment : MainDestination(route = "payment") { + val arguments = listOf( + navArgument(reservationId) { type = NavType.StringType }, + navArgument(showId) { type = NavType.StringType } + ) + } + + data object TicketDetail : MainDestination(route = "tickets") { + val arguments = listOf(navArgument(ticketId) { type = NavType.StringType }) + } + + data object Qr : MainDestination(route = "qr") { + val arguments = listOf( + navArgument(data) { type = NavType.StringType }, + navArgument(ticketName) { type = NavType.StringType }, + ) + } + + data object Reservations : MainDestination(route = "reservations") + data object ReservationDetail : MainDestination(route = "reservations") { + val arguments = listOf(navArgument(reservationId) { type = NavType.StringType }) + } + + data object Refund : MainDestination(route = "refund") { + val arguments = listOf(navArgument(reservationId) { type = NavType.StringType }) + } + + data object HostedShows : MainDestination(route = "hostedShows") + + data object SignOut : MainDestination(route = "signOut") + data object Login : MainDestination(route = "login") +} + +/** + * arguments + */ +const val showId = "showId" +const val ticketId = "ticketId" +const val ticketName = "ticketName" +const val data = "data" +const val reservationId = "reservationId" +const val salesTicketId = "salesTicketId" +const val ticketCount = "ticketCount" +const val isInviteTicket = "isInviteTicket" From 099c92b3e61f3722edd9175b39938341acb7d45a Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 1 Mar 2024 08:28:09 +0900 Subject: [PATCH 041/129] =?UTF-8?q?refactor=20:=20route=EB=A5=BC=20MainDes?= =?UTF-8?q?tination=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 61 ++++++++----------- .../presentation/screen/MainDestination.kt | 4 +- 2 files changed, 28 insertions(+), 37 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 6005be5c..47fa3003 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -24,6 +24,7 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.navigateToHome +import com.nexters.boolti.presentation.screen.MainDestination.* import com.nexters.boolti.presentation.screen.home.HomeScreen import com.nexters.boolti.presentation.screen.login.LoginScreen import com.nexters.boolti.presentation.screen.payment.PaymentScreen @@ -86,15 +87,15 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: // TODO: 하드코딩 된 route 를 각 화면에 정의 NavHost( navController = navController, - startDestination = "home", + startDestination = Home.route, ) { composable( - route = "home", + route = Home.route, ) { HomeScreen( modifier = modifier, onClickShowItem = { - navController.navigate("show/$it") + navController.navigate("${ShowDetail.route}/$it") }, onClickTicket = { navController.navigate("tickets/$it") @@ -119,7 +120,7 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: } composable( - route = "login", + route = Login.route, ) { LoginScreen( modifier = modifier, @@ -128,7 +129,7 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: } } composable( - route = "signout", + route = SignOut.route, ) { SignoutScreen( navigateToHome = { navController.navigateToHome() }, @@ -137,7 +138,7 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: } composable( - route = "reservations", + route = Reservations.route, ) { ReservationsScreen(onBackPressed = { navController.popBackStack() @@ -147,8 +148,8 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: } composable( - route = "reservations/{reservationId}", - arguments = listOf(navArgument("reservationId") { type = NavType.StringType }), + route = "${ReservationDetail.route}/{$reservationId}", + arguments = ReservationDetail.arguments, ) { ReservationDetailScreen( onBackPressed = { navController.popBackStack() }, @@ -157,8 +158,8 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: } composable( - route = "refund/{reservationId}", - arguments = listOf(navArgument("reservationId") { type = NavType.StringType }), + route = "${Refund.route}/{$reservationId}", + arguments = Refund.arguments, ) { RefundScreen( onBackPressed = { navController.popBackStack() }, @@ -166,9 +167,9 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: } navigation( - route = "show/{showId}", + route = "${ShowDetail.route}/{$showId}", startDestination = "detail", - arguments = listOf(navArgument("showId") { type = NavType.StringType }), + arguments = ShowDetail.arguments, ) { composable( route = "detail", @@ -233,8 +234,8 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: } composable( - route = "tickets/{ticketId}", - arguments = listOf(navArgument("ticketId") { type = NavType.StringType }), + route = "${TicketDetail.route}/{$ticketId}", + arguments = TicketDetail.arguments, ) { TicketDetailScreen(modifier = modifier, onBackClicked = { navController.popBackStack() }, @@ -243,17 +244,12 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: "qr/${code.filter { c -> c.isLetterOrDigit() }}?ticketName=$ticketName" ) }, - navigateToShowDetail = { navController.navigate("show/$it") } + navigateToShowDetail = { navController.navigate("${ShowDetail.route}/$it") } ) } composable( - route = "ticketing/{showId}?salesTicketId={salesTicketId}&ticketCount={ticketCount}&inviteTicket={isInviteTicket}", - arguments = listOf( - navArgument("showId") { type = NavType.StringType }, - navArgument("salesTicketId") { type = NavType.StringType }, - navArgument("ticketCount") { type = NavType.IntType }, - navArgument("isInviteTicket") { type = NavType.BoolType }, - ), + route = "${Ticketing.route}/{$showId}?salesTicketId={$salesTicketId}&ticketCount={$ticketCount}&inviteTicket={$isInviteTicket}", + arguments = Ticketing.arguments, ) { TicketingScreen( modifier = modifier, @@ -265,18 +261,15 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: } composable( - route = "qr/{data}?ticketName={ticketName}", - arguments = listOf( - navArgument("data") { type = NavType.StringType }, - navArgument("ticketName") { type = NavType.StringType }, - ), + route = "${Qr.route}/{$data}?ticketName={$ticketName}", + arguments = Qr.arguments, ) { QrFullScreen(modifier = modifier) { navController.popBackStack() } } composable( - route = "hostedShows" + route = HostedShows.route ) { HostedShowScreen( modifier = modifier, @@ -288,18 +281,16 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: } composable( - route = "payment/{reservationId}?showId={showId}", - arguments = listOf( - navArgument("reservationId") { type = NavType.StringType }, - navArgument("showId") { type = NavType.StringType }), + route = "${Payment.route}/{$reservationId}?showId={$showId}", + arguments = Payment.arguments, ) { - val showId = it.arguments?.getString("showId") + val showId = it.arguments?.getString(showId) PaymentScreen( onClickHome = { navController.navigateToHome() }, onClickClose = { showId?.let { showId -> - navController.popBackStack("show/$showId", inclusive = true) - navController.navigate("show/$showId") + navController.popBackStack("${ShowDetail.route}/$showId", inclusive = true) + navController.navigate("${ShowDetail.route}/$showId") } ?: navController.popBackStack() }, ) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt index 2676f1a0..705bcd17 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt @@ -5,7 +5,7 @@ import androidx.navigation.navArgument sealed class MainDestination(val route: String) { data object Home : MainDestination(route = "home") - data object ShowDetail : MainDestination(route = "shows") { + data object ShowDetail : MainDestination(route = "show") { val arguments = listOf(navArgument(showId) { type = NavType.StringType }) } @@ -47,7 +47,7 @@ sealed class MainDestination(val route: String) { data object HostedShows : MainDestination(route = "hostedShows") - data object SignOut : MainDestination(route = "signOut") + data object SignOut : MainDestination(route = "signout") data object Login : MainDestination(route = "login") } From 637d4809ae6e0d83c15ebfa180b0ead2fd88a965 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 1 Mar 2024 08:38:47 +0900 Subject: [PATCH 042/129] =?UTF-8?q?refactor=20:=20HomeScreen=20composable?= =?UTF-8?q?=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 46 +++++++------------ .../presentation/screen/home/HomeScreen.kt | 39 ++++++++++++++-- 2 files changed, 51 insertions(+), 34 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 47fa3003..72effd97 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -24,7 +24,18 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.navigateToHome -import com.nexters.boolti.presentation.screen.MainDestination.* +import com.nexters.boolti.presentation.screen.MainDestination.Home +import com.nexters.boolti.presentation.screen.MainDestination.HostedShows +import com.nexters.boolti.presentation.screen.MainDestination.Login +import com.nexters.boolti.presentation.screen.MainDestination.Payment +import com.nexters.boolti.presentation.screen.MainDestination.Qr +import com.nexters.boolti.presentation.screen.MainDestination.Refund +import com.nexters.boolti.presentation.screen.MainDestination.ReservationDetail +import com.nexters.boolti.presentation.screen.MainDestination.Reservations +import com.nexters.boolti.presentation.screen.MainDestination.ShowDetail +import com.nexters.boolti.presentation.screen.MainDestination.SignOut +import com.nexters.boolti.presentation.screen.MainDestination.TicketDetail +import com.nexters.boolti.presentation.screen.MainDestination.Ticketing import com.nexters.boolti.presentation.screen.home.HomeScreen import com.nexters.boolti.presentation.screen.login.LoginScreen import com.nexters.boolti.presentation.screen.payment.PaymentScreen @@ -89,35 +100,10 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: navController = navController, startDestination = Home.route, ) { - composable( - route = Home.route, - ) { - HomeScreen( - modifier = modifier, - onClickShowItem = { - navController.navigate("${ShowDetail.route}/$it") - }, - onClickTicket = { - navController.navigate("tickets/$it") - }, - onClickQr = { code, ticketName -> - navController.navigate( - "qr/${code.filter { c -> c.isLetterOrDigit() }}?ticketName=$ticketName" - ) - }, - onClickQrScan = { - navController.navigate("hostedShows") - }, - onClickSignout = { - navController.navigate("signout") - }, - navigateToReservations = { - navController.navigate("reservations") - } - ) { - navController.navigate("login") - } - } + HomeScreen( + modifier = modifier, + navController = navController, + ) composable( route = Login.route, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt index 0fd5754e..ec1e537d 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt @@ -22,13 +22,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.screen.HomeViewModel +import com.nexters.boolti.presentation.screen.MainDestination import com.nexters.boolti.presentation.screen.my.MyScreen import com.nexters.boolti.presentation.screen.show.ShowScreen import com.nexters.boolti.presentation.screen.ticket.TicketLoginScreen @@ -37,10 +40,32 @@ import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey50 import com.nexters.boolti.presentation.theme.Grey85 +fun NavGraphBuilder.HomeScreen( + navController: NavController, + modifier: Modifier = Modifier, +) { + composable( + route = MainDestination.Home.route, + ) { + HomeScreen( + modifier = modifier, + onClickShowItem = { navController.navigate("${MainDestination.ShowDetail.route}/$it") }, + onClickTicket = { navController.navigate("${MainDestination.TicketDetail.route}/$it") }, + onClickQr = { code, ticketName -> + navController.navigate( + "${MainDestination.Qr.route}/${code.filter { c -> c.isLetterOrDigit() }}?ticketName=$ticketName" + ) + }, + onClickQrScan = { navController.navigate(MainDestination.HostedShows.route) }, + onClickSignout = { navController.navigate(MainDestination.SignOut.route) }, + navigateToReservations = { navController.navigate(MainDestination.Reservations.route) }, + requireLogin = { navController.navigate(MainDestination.Login.route) } + ) + } +} + @Composable -fun HomeScreen( - modifier: Modifier, - viewModel: HomeViewModel = hiltViewModel(), +private fun HomeScreen( onClickShowItem: (showId: String) -> Unit, onClickTicket: (ticketId: String) -> Unit, onClickQr: (data: String, ticketName: String) -> Unit, @@ -48,6 +73,8 @@ fun HomeScreen( onClickSignout: () -> Unit, navigateToReservations: () -> Unit, requireLogin: () -> Unit, + modifier: Modifier, + viewModel: HomeViewModel = hiltViewModel(), ) { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() @@ -93,7 +120,11 @@ fun HomeScreen( modifier = modifier.padding(innerPadding), ) - false -> TicketLoginScreen(modifier.padding(innerPadding), onLoginClick = requireLogin) + false -> TicketLoginScreen( + modifier.padding(innerPadding), + onLoginClick = requireLogin + ) + else -> Unit // 로그인 여부를 불러오는 중 } } From 24107c494e74b8af9169857be19d91f2e1c63a66 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 1 Mar 2024 08:43:24 +0900 Subject: [PATCH 043/129] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20=ED=99=94=EB=A9=B4=20composabl?= =?UTF-8?q?e=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 28 ++++++------------- .../presentation/screen/login/LoginScreen.kt | 24 ++++++++++++++-- .../screen/signout/SignoutScreen.kt | 20 ++++++++++++- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 72effd97..a8d1acc7 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -26,14 +26,12 @@ import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.navigateToHome import com.nexters.boolti.presentation.screen.MainDestination.Home import com.nexters.boolti.presentation.screen.MainDestination.HostedShows -import com.nexters.boolti.presentation.screen.MainDestination.Login import com.nexters.boolti.presentation.screen.MainDestination.Payment import com.nexters.boolti.presentation.screen.MainDestination.Qr import com.nexters.boolti.presentation.screen.MainDestination.Refund import com.nexters.boolti.presentation.screen.MainDestination.ReservationDetail import com.nexters.boolti.presentation.screen.MainDestination.Reservations import com.nexters.boolti.presentation.screen.MainDestination.ShowDetail -import com.nexters.boolti.presentation.screen.MainDestination.SignOut import com.nexters.boolti.presentation.screen.MainDestination.TicketDetail import com.nexters.boolti.presentation.screen.MainDestination.Ticketing import com.nexters.boolti.presentation.screen.home.HomeScreen @@ -95,7 +93,6 @@ fun Main(onClickQrScan: (showId: String, showName: String) -> Unit) { fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: String) -> Unit) { val navController = rememberNavController() - // TODO: 하드코딩 된 route 를 각 화면에 정의 NavHost( navController = navController, startDestination = Home.route, @@ -105,23 +102,14 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: navController = navController, ) - composable( - route = Login.route, - ) { - LoginScreen( - modifier = modifier, - ) { - navController.popBackStack() - } - } - composable( - route = SignOut.route, - ) { - SignoutScreen( - navigateToHome = { navController.navigateToHome() }, - navigateBack = { navController.popBackStack() }, - ) - } + LoginScreen( + modifier = modifier, + navController = navController, + ) + + SignoutScreen( + navController = navController, + ) composable( route = Reservations.route, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt index 0fae236d..4dc64440 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt @@ -37,19 +37,37 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.KakaoLoginButton import com.nexters.boolti.presentation.component.MainButton +import com.nexters.boolti.presentation.screen.MainDestination import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.subTextPadding +fun NavGraphBuilder.LoginScreen( + navController: NavController, + modifier: Modifier = Modifier, +) { + composable( + route = MainDestination.Login.route, + ) { + LoginScreen( + modifier = modifier, + onBackPressed = { navController.popBackStack() } + ) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LoginScreen( +private fun LoginScreen( + onBackPressed: () -> Unit, modifier: Modifier = Modifier, viewModel: LoginViewModel = hiltViewModel(), - onBackPressed: () -> Unit, ) { val context = LocalContext.current val sheetState = rememberModalBottomSheetState() @@ -195,4 +213,4 @@ private fun SignUpBottomSheet( onClick = signUp, ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt index 3861a6f2..3deb9785 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt @@ -15,13 +15,31 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BtAppBar import com.nexters.boolti.presentation.component.MainButton +import com.nexters.boolti.presentation.extension.navigateToHome +import com.nexters.boolti.presentation.screen.MainDestination import com.nexters.boolti.presentation.theme.marginHorizontal +fun NavGraphBuilder.SignoutScreen( + navController: NavController, +) { + composable( + route = MainDestination.SignOut.route, + ) { + SignoutScreen( + navigateToHome = { navController.navigateToHome() }, + navigateBack = { navController.popBackStack() }, + ) + } +} + @Composable -fun SignoutScreen( +private fun SignoutScreen( navigateToHome: () -> Unit, navigateBack: () -> Unit, viewModel: SignoutViewModel = hiltViewModel(), From 39e5f2e1ffc3383e4ab8b55fba5f4c792bdeca4b Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 1 Mar 2024 08:57:37 +0900 Subject: [PATCH 044/129] =?UTF-8?q?refactor=20:=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=99=94=EB=A9=B4=20composable=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 38 ++----------------- .../screen/refund/RefundScreen.kt | 28 ++++++++++---- .../reservations/ReservationDetailScreen.kt | 27 ++++++++++--- .../screen/reservations/ReservationsScreen.kt | 25 ++++++++++-- 4 files changed, 67 insertions(+), 51 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index a8d1acc7..4d9a1756 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -29,8 +29,6 @@ import com.nexters.boolti.presentation.screen.MainDestination.HostedShows import com.nexters.boolti.presentation.screen.MainDestination.Payment import com.nexters.boolti.presentation.screen.MainDestination.Qr import com.nexters.boolti.presentation.screen.MainDestination.Refund -import com.nexters.boolti.presentation.screen.MainDestination.ReservationDetail -import com.nexters.boolti.presentation.screen.MainDestination.Reservations import com.nexters.boolti.presentation.screen.MainDestination.ShowDetail import com.nexters.boolti.presentation.screen.MainDestination.TicketDetail import com.nexters.boolti.presentation.screen.MainDestination.Ticketing @@ -107,38 +105,10 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: navController = navController, ) - SignoutScreen( - navController = navController, - ) - - composable( - route = Reservations.route, - ) { - ReservationsScreen(onBackPressed = { - navController.popBackStack() - }, navigateToDetail = { reservationId -> - navController.navigate("reservations/$reservationId") - }) - } - - composable( - route = "${ReservationDetail.route}/{$reservationId}", - arguments = ReservationDetail.arguments, - ) { - ReservationDetailScreen( - onBackPressed = { navController.popBackStack() }, - navigateToRefund = { id -> navController.navigate("refund/$id") }, - ) - } - - composable( - route = "${Refund.route}/{$reservationId}", - arguments = Refund.arguments, - ) { - RefundScreen( - onBackPressed = { navController.popBackStack() }, - ) - } + SignoutScreen(navController = navController) + ReservationsScreen(navController = navController) + ReservationDetailScreen(navController = navController) + RefundScreen(navController = navController) navigation( route = "${ShowDetail.route}/{$showId}", diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt index b812c4f7..b3562f30 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt @@ -1,6 +1,5 @@ package com.nexters.boolti.presentation.screen.refund -import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi @@ -55,17 +54,18 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import coil.compose.AsyncImage import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.presentation.R @@ -75,6 +75,8 @@ import com.nexters.boolti.presentation.component.BtAppBar import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.extension.filterToPhoneNumber import com.nexters.boolti.presentation.screen.LocalSnackbarController +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.reservationId import com.nexters.boolti.presentation.theme.Error import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey15 @@ -89,11 +91,23 @@ import com.nexters.boolti.presentation.theme.point4 import com.nexters.boolti.presentation.util.PhoneNumberVisualTransformation import kotlinx.coroutines.launch +fun NavGraphBuilder.RefundScreen( + navController: NavController, +) { + composable( + route = "${MainDestination.Refund.route}/{$reservationId}", + arguments = MainDestination.Refund.arguments, + ) { + RefundScreen( + onBackPressed = { navController.popBackStack() }, + ) + } +} + @OptIn(ExperimentalFoundationApi::class) @Composable -fun RefundScreen( +private fun RefundScreen( onBackPressed: () -> Unit, - modifier: Modifier = Modifier, viewModel: RefundViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -121,7 +135,7 @@ fun RefundScreen( title = stringResource(id = R.string.refund_button), onBackPressed = onBackPressed ) }, - modifier = modifier, + modifier = Modifier, ) { innerPadding -> val reservation = uiState.reservation ?: return@Scaffold @@ -654,4 +668,4 @@ fun InfoRow( style = MaterialTheme.typography.bodySmall.copy(color = Grey15), ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index 50a5319a..292f8b04 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -27,14 +27,12 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState 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.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -49,16 +47,20 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import coil.compose.AsyncImage import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.domain.model.ReservationState import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.CopyButton -import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.constants.datetimeFormat import com.nexters.boolti.presentation.extension.toDescriptionAndColorPair import com.nexters.boolti.presentation.screen.LocalSnackbarController +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.reservationId import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey15 import com.nexters.boolti.presentation.theme.Grey20 @@ -68,10 +70,23 @@ import com.nexters.boolti.presentation.theme.Grey80 import com.nexters.boolti.presentation.theme.Grey90 import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.point2 -import kotlinx.coroutines.launch + +fun NavGraphBuilder.ReservationDetailScreen( + navController: NavController, +) { + composable( + route = "${MainDestination.ReservationDetail.route}/{$reservationId}", + arguments = MainDestination.ReservationDetail.arguments, + ) { + ReservationDetailScreen( + onBackPressed = { navController.popBackStack() }, + navigateToRefund = { id -> navController.navigate("${MainDestination.Refund.route}/$id") }, + ) + } +} @Composable -fun ReservationDetailScreen( +private fun ReservationDetailScreen( onBackPressed: () -> Unit, navigateToRefund: (id: String) -> Unit, modifier: Modifier = Modifier, @@ -539,4 +554,4 @@ private fun RefundButton( style = MaterialTheme.typography.titleMedium ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt index 4180fab3..bfa0d6fd 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt @@ -31,6 +31,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import coil.compose.AsyncImage import com.nexters.boolti.domain.model.Reservation import com.nexters.boolti.domain.model.ReservationState @@ -38,6 +41,7 @@ import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BtAppBar import com.nexters.boolti.presentation.component.BtCircularProgressIndicator import com.nexters.boolti.presentation.extension.toDescriptionAndColorPair +import com.nexters.boolti.presentation.screen.MainDestination import com.nexters.boolti.presentation.theme.Grey05 import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey50 @@ -48,11 +52,24 @@ import com.nexters.boolti.presentation.theme.point1 import com.nexters.boolti.presentation.theme.subTextPadding import java.time.format.DateTimeFormatter +fun NavGraphBuilder.ReservationsScreen( + navController: NavController, +) { + composable( + route = MainDestination.Reservations.route, + ) { + ReservationsScreen( + onBackPressed = { navController.popBackStack() }, + navigateToDetail = { reservationId -> + navController.navigate("${MainDestination.Reservations.route}/$reservationId") + }) + } +} + @Composable -fun ReservationsScreen( +private fun ReservationsScreen( onBackPressed: () -> Unit, navigateToDetail: (reservationId: String) -> Unit, - modifier: Modifier = Modifier, viewModel: ReservationsViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -66,7 +83,7 @@ fun ReservationsScreen( } ) { innerPadding -> Box( - modifier = modifier + modifier = Modifier .padding(innerPadding) .fillMaxSize(), ) { @@ -236,4 +253,4 @@ fun ReservationStateLabel( text = stringResource(id = stringId), style = MaterialTheme.typography.bodySmall.copy(color = color), ) -} \ No newline at end of file +} From 5b110cdbd3e7387fa99782318ad241ca35e09d8a Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 1 Mar 2024 09:09:17 +0900 Subject: [PATCH 045/129] =?UTF-8?q?refactor=20:=20Show=20Detail=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=99=94=EB=A9=B4=20composable=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 75 ++++--------------- .../screen/report/ReportScreen.kt | 31 +++++--- .../screen/show/ShowDetailContentScreen.kt | 26 ++++++- .../screen/show/ShowDetailScreen.kt | 40 +++++++++- .../screen/show/ShowImagesScreen.kt | 29 ++++++- 5 files changed, 122 insertions(+), 79 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 4d9a1756..ef282282 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -16,19 +16,16 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.navigateToHome import com.nexters.boolti.presentation.screen.MainDestination.Home import com.nexters.boolti.presentation.screen.MainDestination.HostedShows import com.nexters.boolti.presentation.screen.MainDestination.Payment import com.nexters.boolti.presentation.screen.MainDestination.Qr -import com.nexters.boolti.presentation.screen.MainDestination.Refund import com.nexters.boolti.presentation.screen.MainDestination.ShowDetail import com.nexters.boolti.presentation.screen.MainDestination.TicketDetail import com.nexters.boolti.presentation.screen.MainDestination.Ticketing @@ -43,7 +40,6 @@ import com.nexters.boolti.presentation.screen.reservations.ReservationDetailScre import com.nexters.boolti.presentation.screen.reservations.ReservationsScreen import com.nexters.boolti.presentation.screen.show.ShowDetailContentScreen import com.nexters.boolti.presentation.screen.show.ShowDetailScreen -import com.nexters.boolti.presentation.screen.show.ShowDetailViewModel import com.nexters.boolti.presentation.screen.show.ShowImagesScreen import com.nexters.boolti.presentation.screen.signout.SignoutScreen import com.nexters.boolti.presentation.screen.ticket.detail.TicketDetailScreen @@ -115,66 +111,21 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: startDestination = "detail", arguments = ShowDetail.arguments, ) { - composable( - route = "detail", - ) { entry -> - val showViewModel: ShowDetailViewModel = - entry.sharedViewModel(navController = navController) - - ShowDetailScreen( - onBack = { navController.popBackStack() }, - onClickHome = { navController.navigateToHome() }, - onClickContent = { - navController.navigate("content") - }, - modifier = modifier, - onTicketSelected = { showId, ticketId, ticketCount, isInviteTicket -> - navController.navigate("ticketing/$showId?salesTicketId=$ticketId&ticketCount=$ticketCount&inviteTicket=$isInviteTicket") - }, - viewModel = showViewModel, - navigateToLogin = { navController.navigate("login") }, - navigateToImages = { index -> navController.navigate("images/$index") }, - navigateToReport = { - val showId = entry.arguments?.getString("showId") - navController.navigate("report/$showId") - } - ) - } - composable( - route = "images/{index}", - arguments = listOf(navArgument("index") { type = NavType.IntType }), - ) { entry -> - val showViewModel: ShowDetailViewModel = - entry.sharedViewModel(navController = navController) - val index = entry.arguments!!.getInt("index") + ShowDetailScreen( + modifier = modifier, + navController = navController, + ) - ShowImagesScreen( - index = index, - viewModel = showViewModel, - onBackPressed = { navController.popBackStack() }, - ) - } - composable( - route = "content", - ) { entry -> - val showViewModel: ShowDetailViewModel = - entry.sharedViewModel(navController = navController) + ShowImagesScreen(navController = navController) + ShowDetailContentScreen( + modifier = modifier, + navController = navController, + ) - ShowDetailContentScreen( - modifier = modifier, - viewModel = showViewModel, - onBackPressed = { navController.popBackStack() } - ) - } - composable( - route = "report/{showId}", - ) { - ReportScreen( - onBackPressed = { navController.popBackStack() }, - popupToHome = { navController.navigateToHome() }, - modifier = modifier, - ) - } + ReportScreen( + modifier = modifier, + navController = navController, + ) } composable( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt index 5a099ec7..60df5479 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt @@ -8,37 +8,48 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTTextField import com.nexters.boolti.presentation.component.BtAppBar import com.nexters.boolti.presentation.component.MainButton -import com.nexters.boolti.presentation.theme.Grey10 +import com.nexters.boolti.presentation.extension.navigateToHome import com.nexters.boolti.presentation.theme.Grey30 -import com.nexters.boolti.presentation.theme.Grey70 -import com.nexters.boolti.presentation.theme.Grey85 import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.point4 +fun NavGraphBuilder.ReportScreen( + navController: NavController, + modifier: Modifier = Modifier, +) { + composable( + route = "report/{showId}", + ) { + ReportScreen( + onBackPressed = { navController.popBackStack() }, + popupToHome = { navController.navigateToHome() }, + modifier = modifier, + ) + } +} + @Composable -fun ReportScreen( +private fun ReportScreen( onBackPressed: () -> Unit, popupToHome: () -> Unit, modifier: Modifier = Modifier, @@ -103,4 +114,4 @@ fun ReportScreen( ) } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt index b85253e5..d6d6d426 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt @@ -22,13 +22,35 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.screen.sharedViewModel import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.marginHorizontal +fun NavGraphBuilder.ShowDetailContentScreen( + navController: NavController, + modifier: Modifier = Modifier, +) { + composable( + route = "content", + ) { entry -> + val showViewModel: ShowDetailViewModel = + entry.sharedViewModel(navController = navController) + + ShowDetailContentScreen( + modifier = modifier, + viewModel = showViewModel, + onBackPressed = { navController.popBackStack() } + ) + } +} + @Composable -fun ShowDetailContentScreen( +private fun ShowDetailContentScreen( onBackPressed: () -> Unit, modifier: Modifier = Modifier, viewModel: ShowDetailViewModel = hiltViewModel(), @@ -80,4 +102,4 @@ private fun ShowDetailContentAppBar( style = MaterialTheme.typography.titleMedium.copy(color = Grey10), ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index d54e279b..3da59d2a 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -28,7 +28,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -53,14 +52,18 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import com.nexters.boolti.domain.model.ShowDetail import com.nexters.boolti.domain.model.ShowState import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.CopyButton import com.nexters.boolti.presentation.component.MainButton -import com.nexters.boolti.presentation.component.ToastSnackbarHost +import com.nexters.boolti.presentation.extension.navigateToHome import com.nexters.boolti.presentation.extension.requireActivity import com.nexters.boolti.presentation.screen.LocalSnackbarController +import com.nexters.boolti.presentation.screen.sharedViewModel import com.nexters.boolti.presentation.screen.ticketing.ChooseTicketBottomSheet import com.nexters.boolti.presentation.theme.Grey20 import com.nexters.boolti.presentation.theme.Grey30 @@ -73,8 +76,39 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.time.format.DateTimeFormatter +fun NavGraphBuilder.ShowDetailScreen( + modifier: Modifier = Modifier, + navController: NavController, +) { + composable( + route = "detail", + ) { entry -> + val showViewModel: ShowDetailViewModel = + entry.sharedViewModel(navController = navController) + + ShowDetailScreen( + modifier = modifier, + onBack = { navController.popBackStack() }, + onClickHome = { navController.navigateToHome() }, + onClickContent = { + navController.navigate("content") + }, + onTicketSelected = { showId, ticketId, ticketCount, isInviteTicket -> + navController.navigate("ticketing/$showId?salesTicketId=$ticketId&ticketCount=$ticketCount&inviteTicket=$isInviteTicket") + }, + viewModel = showViewModel, + navigateToLogin = { navController.navigate("login") }, + navigateToImages = { index -> navController.navigate("images/$index") }, + navigateToReport = { + val showId = entry.arguments?.getString("showId") + navController.navigate("report/$showId") + } + ) + } +} + @Composable -fun ShowDetailScreen( +private fun ShowDetailScreen( onBack: () -> Unit, onClickHome: () -> Unit, onClickContent: () -> Unit, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt index 38218df5..8fe80c24 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt @@ -24,15 +24,40 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument import coil.compose.AsyncImage import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BtAppBar +import com.nexters.boolti.presentation.screen.sharedViewModel import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.zoomable +fun NavGraphBuilder.ShowImagesScreen( + navController: NavController, +) { + composable( + route = "images/{index}", + arguments = listOf(navArgument("index") { type = NavType.IntType }), + ) { entry -> + val showViewModel: ShowDetailViewModel = + entry.sharedViewModel(navController = navController) + val index = entry.arguments!!.getInt("index") + + ShowImagesScreen( + index = index, + viewModel = showViewModel, + onBackPressed = { navController.popBackStack() }, + ) + } +} + @OptIn(ExperimentalFoundationApi::class) @Composable -fun ShowImagesScreen( +private fun ShowImagesScreen( index: Int, onBackPressed: () -> Unit, modifier: Modifier = Modifier, @@ -103,4 +128,4 @@ private fun Indicator( ) } } -} \ No newline at end of file +} From 8ccd472abf8df3dbd5a026cc4b95fba30447b2dc Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 1 Mar 2024 09:22:49 +0900 Subject: [PATCH 046/129] =?UTF-8?q?refactor=20:=20=EB=82=98=EB=A8=B8?= =?UTF-8?q?=EC=A7=80=20=ED=99=94=EB=A9=B4=20composable=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 106 +++--------------- .../screen/payment/PaymentScreen.kt | 37 +++++- .../screen/qr/HostedShowScreen.kt | 26 ++++- .../presentation/screen/qr/QrFullScreen.kt | 22 +++- .../ticket/detail/TicketDetailScreen.kt | 59 ++++++++-- .../screen/ticketing/TicketingScreen.kt | 28 ++++- 6 files changed, 172 insertions(+), 106 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index ef282282..10e5a300 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -17,18 +17,11 @@ import androidx.lifecycle.ViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import com.nexters.boolti.presentation.component.ToastSnackbarHost -import com.nexters.boolti.presentation.extension.navigateToHome import com.nexters.boolti.presentation.screen.MainDestination.Home -import com.nexters.boolti.presentation.screen.MainDestination.HostedShows -import com.nexters.boolti.presentation.screen.MainDestination.Payment -import com.nexters.boolti.presentation.screen.MainDestination.Qr import com.nexters.boolti.presentation.screen.MainDestination.ShowDetail -import com.nexters.boolti.presentation.screen.MainDestination.TicketDetail -import com.nexters.boolti.presentation.screen.MainDestination.Ticketing import com.nexters.boolti.presentation.screen.home.HomeScreen import com.nexters.boolti.presentation.screen.login.LoginScreen import com.nexters.boolti.presentation.screen.payment.PaymentScreen @@ -91,16 +84,8 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: navController = navController, startDestination = Home.route, ) { - HomeScreen( - modifier = modifier, - navController = navController, - ) - - LoginScreen( - modifier = modifier, - navController = navController, - ) - + HomeScreen(modifier = modifier, navController = navController) + LoginScreen(modifier = modifier, navController = navController) SignoutScreen(navController = navController) ReservationsScreen(navController = navController) ReservationDetailScreen(navController = navController) @@ -111,85 +96,22 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: startDestination = "detail", arguments = ShowDetail.arguments, ) { - ShowDetailScreen( - modifier = modifier, - navController = navController, - ) - + ShowDetailScreen(modifier = modifier, navController = navController) ShowImagesScreen(navController = navController) - ShowDetailContentScreen( - modifier = modifier, - navController = navController, - ) - - ReportScreen( - modifier = modifier, - navController = navController, - ) - } - - composable( - route = "${TicketDetail.route}/{$ticketId}", - arguments = TicketDetail.arguments, - ) { - TicketDetailScreen(modifier = modifier, - onBackClicked = { navController.popBackStack() }, - onClickQr = { code, ticketName -> - navController.navigate( - "qr/${code.filter { c -> c.isLetterOrDigit() }}?ticketName=$ticketName" - ) - }, - navigateToShowDetail = { navController.navigate("${ShowDetail.route}/$it") } - ) - } - composable( - route = "${Ticketing.route}/{$showId}?salesTicketId={$salesTicketId}&ticketCount={$ticketCount}&inviteTicket={$isInviteTicket}", - arguments = Ticketing.arguments, - ) { - TicketingScreen( - modifier = modifier, - onBackClicked = { navController.popBackStack() }, - onReserved = { reservationId, showId -> - navController.navigate("payment/$reservationId?showId=$showId") - } - ) + ShowDetailContentScreen(modifier = modifier, navController = navController) + ReportScreen(modifier = modifier, navController = navController) } - composable( - route = "${Qr.route}/{$data}?ticketName={$ticketName}", - arguments = Qr.arguments, - ) { - QrFullScreen(modifier = modifier) { - navController.popBackStack() - } - } - composable( - route = HostedShows.route - ) { - HostedShowScreen( - modifier = modifier, - onClickShow = onClickQrScan, - onClickBack = { - navController.popBackStack() - } - ) - } + TicketDetailScreen(modifier = modifier, navController = navController) + TicketingScreen(modifier = modifier, navController = navController) + QrFullScreen(modifier = modifier, navController = navController) + HostedShowScreen( + modifier = modifier, + onClickShow = onClickQrScan, + navController = navController, + ) - composable( - route = "${Payment.route}/{$reservationId}?showId={$showId}", - arguments = Payment.arguments, - ) { - val showId = it.arguments?.getString(showId) - PaymentScreen( - onClickHome = { navController.navigateToHome() }, - onClickClose = { - showId?.let { showId -> - navController.popBackStack("${ShowDetail.route}/$showId", inclusive = true) - navController.navigate("${ShowDetail.route}/$showId") - } ?: navController.popBackStack() - }, - ) - } + PaymentScreen(navController = navController) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt index ffde4b4c..587f0609 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt @@ -16,15 +16,45 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.domain.model.ReservationState import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.ToastSnackbarHost +import com.nexters.boolti.presentation.extension.navigateToHome +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.reservationId +import com.nexters.boolti.presentation.screen.showId import kotlinx.coroutines.launch +fun NavGraphBuilder.PaymentScreen( + navController: NavController, +) { + composable( + route = "${MainDestination.Payment.route}/{$reservationId}?showId={$showId}", + arguments = MainDestination.Payment.arguments, + ) { + val showId = it.arguments?.getString(showId) + PaymentScreen( + onClickHome = { navController.navigateToHome() }, + onClickClose = { + showId?.let { showId -> + navController.popBackStack( + "${MainDestination.ShowDetail.route}/$showId", + inclusive = true + ) + navController.navigate("${MainDestination.ShowDetail.route}/$showId") + } ?: navController.popBackStack() + }, + ) + } +} + @Composable -fun PaymentScreen( +private fun PaymentScreen( onClickHome: () -> Unit, onClickClose: () -> Unit, viewModel: PaymentViewModel = hiltViewModel(), @@ -41,7 +71,10 @@ fun PaymentScreen( Scaffold( topBar = { PaymentToolbar(onClickHome = onClickHome, onClickClose = onClickClose) }, snackbarHost = { - ToastSnackbarHost(hostState = snackbarHostState, modifier = Modifier.padding(bottom = 40.dp)) + ToastSnackbarHost( + hostState = snackbarHostState, + modifier = Modifier.padding(bottom = 40.dp) + ) }, ) { innerPadding -> when (uiState) { diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt index 64f22283..8522869b 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt @@ -34,8 +34,12 @@ import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import com.nexters.boolti.domain.model.Show import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.screen.MainDestination import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey60 @@ -43,11 +47,29 @@ import com.nexters.boolti.presentation.theme.point1 import java.time.LocalDate import java.time.LocalDateTime -@Composable -fun HostedShowScreen( +fun NavGraphBuilder.HostedShowScreen( + onClickShow: (showId: String, showName: String) -> Unit, + navController: NavController, modifier: Modifier = Modifier, +) { + composable( + route = MainDestination.HostedShows.route + ) { + HostedShowScreen( + modifier = modifier, + onClickShow = onClickShow, + onClickBack = { + navController.popBackStack() + } + ) + } +} + +@Composable +private fun HostedShowScreen( onClickBack: () -> Unit, onClickShow: (showId: String, showName: String) -> Unit, + modifier: Modifier = Modifier, viewModel: HostedShowViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt index c64ede2e..1172a0f2 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt @@ -22,14 +22,34 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.data +import com.nexters.boolti.presentation.screen.ticketName import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey85 import com.nexters.boolti.presentation.theme.Grey90 import com.nexters.boolti.presentation.util.rememberQrBitmapPainter +fun NavGraphBuilder.QrFullScreen( + navController: NavController, + modifier: Modifier = Modifier, +) { + composable( + route = "${MainDestination.Qr.route}/{$data}?ticketName={$ticketName}", + arguments = MainDestination.Qr.arguments, + ) { + QrFullScreen(modifier = modifier) { + navController.popBackStack() + } + } +} + @Composable -fun QrFullScreen( +private fun QrFullScreen( modifier: Modifier = Modifier, viewModel: QrFullViewModel = hiltViewModel(), onClose: () -> Unit, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt index 16580443..3cc401c9 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt @@ -77,6 +77,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import coil.compose.AsyncImage import com.nexters.boolti.domain.model.TicketState import com.nexters.boolti.presentation.R @@ -87,6 +90,8 @@ import com.nexters.boolti.presentation.extension.dayOfWeekString import com.nexters.boolti.presentation.extension.format import com.nexters.boolti.presentation.extension.toDp import com.nexters.boolti.presentation.extension.toPx +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.ticketId import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey20 import com.nexters.boolti.presentation.theme.Grey30 @@ -104,9 +109,30 @@ import kotlinx.coroutines.launch import java.time.LocalDate import java.time.LocalDateTime +fun NavGraphBuilder.TicketDetailScreen( + navController: NavController, + modifier: Modifier = Modifier, +) { + composable( + route = "${MainDestination.TicketDetail.route}/{$ticketId}", + arguments = MainDestination.TicketDetail.arguments, + ) { + TicketDetailScreen( + modifier = modifier, + onBackClicked = { navController.popBackStack() }, + onClickQr = { code, ticketName -> + navController.navigate( + "${MainDestination.Qr.route}/${code.filter { c -> c.isLetterOrDigit() }}?ticketName=$ticketName" + ) + }, + navigateToShowDetail = { navController.navigate("${MainDestination.ShowDetail.route}/$it") } + ) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TicketDetailScreen( +private fun TicketDetailScreen( modifier: Modifier = Modifier, viewModel: TicketDetailViewModel = hiltViewModel(), onBackClicked: () -> Unit, @@ -125,7 +151,8 @@ fun TicketDetailScreen( var contentWidth by remember { mutableFloatStateOf(0f) } var ticketSectionHeight by remember { mutableFloatStateOf(0f) } var ticketSectionHeightUntilTicketInfo by remember { mutableFloatStateOf(0f) } - val bottomAreaHeight = ticketSectionHeight - ticketSectionHeightUntilTicketInfo + ticketInfoHeight.toPx() + val bottomAreaHeight = + ticketSectionHeight - ticketSectionHeightUntilTicketInfo + ticketInfoHeight.toPx() val uiState by viewModel.uiState.collectAsStateWithLifecycle() val managerCodeState by viewModel.managerCodeState.collectAsStateWithLifecycle() @@ -137,7 +164,10 @@ fun TicketDetailScreen( LaunchedEffect(viewModel.event) { viewModel.event.collect { when (it) { - TicketDetailEvent.ManagerCodeValid -> snackbarHostState.showSnackbar(entranceSuccessMsg) + TicketDetailEvent.ManagerCodeValid -> snackbarHostState.showSnackbar( + entranceSuccessMsg + ) + TicketDetailEvent.OnRefresh -> showEnterCodeDialog = false } } @@ -181,7 +211,12 @@ fun TicketDetailScreen( .clip(ticketShape) .border( width = 1.dp, - brush = Brush.verticalGradient(listOf(White.copy(.5f), White.copy(.2f))), + brush = Brush.verticalGradient( + listOf( + White.copy(.5f), + White.copy(.2f) + ) + ), shape = ticketShape, ), ) { @@ -207,16 +242,23 @@ fun TicketDetailScreen( ) .background( brush = Brush.linearGradient( - colors = listOf(Color(0x33C5CACD), Grey95.copy(alpha = .2f)), + colors = listOf( + Color(0x33C5CACD), + Grey95.copy(alpha = .2f) + ), start = Offset.Zero, - end = Offset(x = contentWidth, y = ticketSectionHeightUntilTicketInfo), + end = Offset( + x = contentWidth, + y = ticketSectionHeightUntilTicketInfo + ), ), ) ) Column( modifier = Modifier .onGloballyPositioned { coordinates -> - ticketSectionHeightUntilTicketInfo = coordinates.size.height.toFloat() + ticketSectionHeightUntilTicketInfo = + coordinates.size.height.toFloat() } ) { Title(ticketName = ticket.ticketName) @@ -266,7 +308,8 @@ fun TicketDetailScreen( Notice(notice = ticket.ticketNotice) - val copiedMessage = stringResource(id = R.string.ticketing_address_copied_message) + val copiedMessage = + stringResource(id = R.string.ticketing_address_copied_message) Inquiry( hostName = ticket.hostName, hostPhoneNumber = ticket.hostPhoneNumber, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index d4fb414c..2a7b77d3 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -64,6 +64,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import coil.compose.AsyncImage import com.nexters.boolti.domain.model.InviteCodeStatus import com.nexters.boolti.presentation.R @@ -73,6 +76,11 @@ import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.dayOfWeekString import com.nexters.boolti.presentation.extension.filterToPhoneNumber import com.nexters.boolti.presentation.extension.format +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.isInviteTicket +import com.nexters.boolti.presentation.screen.salesTicketId +import com.nexters.boolti.presentation.screen.showId +import com.nexters.boolti.presentation.screen.ticketCount import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Error import com.nexters.boolti.presentation.theme.Grey05 @@ -88,9 +96,27 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.time.LocalDateTime +fun NavGraphBuilder.TicketingScreen( + navController: NavController, + modifier: Modifier = Modifier, +) { + composable( + route = "${MainDestination.Ticketing.route}/{$showId}?salesTicketId={$salesTicketId}&ticketCount={$ticketCount}&inviteTicket={$isInviteTicket}", + arguments = MainDestination.Ticketing.arguments, + ) { + TicketingScreen( + modifier = modifier, + onBackClicked = { navController.popBackStack() }, + onReserved = { reservationId, showId -> + navController.navigate("${MainDestination.Payment.route}/$reservationId?showId=$showId") + } + ) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TicketingScreen( +private fun TicketingScreen( modifier: Modifier = Modifier, viewModel: TicketingViewModel = hiltViewModel(), onBackClicked: () -> Unit = {}, From b096b3844f0b23a6505a1e634bcb961cb5010354 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Mon, 4 Mar 2024 22:49:47 +0900 Subject: [PATCH 047/129] =?UTF-8?q?Boolti-185=20fix:=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=20=EB=B3=91=ED=95=A9=EB=90=9C=20=ED=8C=8C=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/data/repository/AuthRepositoryImpl.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt index d758a2bc..08dd4869 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt @@ -30,13 +30,11 @@ internal class AuthRepositoryImpl @Inject constructor( override val cachedUser: Flow get() = authDataSource.user.map { it?.toDomain() } - override suspend fun kakaoLogin(request: LoginRequest): Result { + override suspend fun kakaoLogin(request: LoginRequest): Result { return authDataSource.login(request).onSuccess { response -> tokenDataSource.saveTokens(response.accessToken ?: "", response.refreshToken ?: "") deviceTokenDataSource.sendFcmToken() - }.mapCatching { - !it.signUpRequired - } + }.mapCatching(LoginResponse::toDomain) } override suspend fun logout(): Result = authDataSource.logout() From b36eb3abfdfe03459d7200b6d6a9010bf5d7908a Mon Sep 17 00:00:00 2001 From: algosketch Date: Thu, 7 Mar 2024 20:46:00 +0900 Subject: [PATCH 048/129] update app version --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c255609b..5fa58f4d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] minSdk = "26" targetSdk = "34" -versionCode = "6" -versionName = "1.1.2" +versionCode = "7" +versionName = "1.2.0" packageName = "com.nexters.boolti" compileSdk = "34" targetJvm = "17" @@ -130,4 +130,4 @@ db = ["androidx-datastore", "androidx-datastore-preferences-core", "androidx-roo firebase = ["firebase-analytics-ktx", "firebase-crashlytics-ktx", "firebase-messaging-ktx"] kotest = ["kotest-assertions-core", "kotest-property", "kotest-runner-junit5"] coil = ["coil", "coil-compose"] -compose = ["androidx-activity-compose", "androidx-navigation-compose", "androidx-material3-android", "androidx-compose-ui-ui", "androidx-compose-ui-ui-graphics", "androidx-compose-ui-tooling-preview", "androidx-compose-ui-ui-util", "androidx-lifecycle-runtime-compose"] \ No newline at end of file +compose = ["androidx-activity-compose", "androidx-navigation-compose", "androidx-material3-android", "androidx-compose-ui-ui", "androidx-compose-ui-ui-graphics", "androidx-compose-ui-tooling-preview", "androidx-compose-ui-ui-util", "androidx-lifecycle-runtime-compose"] From d3d8ae8870814b8be5b24141b77fdc068962f09b Mon Sep 17 00:00:00 2001 From: algosketch Date: Thu, 7 Mar 2024 20:50:17 +0900 Subject: [PATCH 049/129] update app gitignore --- app/.gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/.gitignore b/app/.gitignore index 42afabfd..46a46b2f 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,3 @@ -/build \ No newline at end of file +/build +*.jks +*keystore From 5b41102a04da1d43b32c9be636144ffb9e4584a6 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 8 Mar 2024 04:55:26 +0900 Subject: [PATCH 050/129] =?UTF-8?q?feat=20:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=98=88=EC=8B=9C=20=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/base/BaseViewModel.kt | 26 +++++++++++++++++++ .../presentation/screen/HomeViewModel.kt | 9 +++---- 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/base/BaseViewModel.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/base/BaseViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/base/BaseViewModel.kt new file mode 100644 index 00000000..104d2fc2 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/base/BaseViewModel.kt @@ -0,0 +1,26 @@ +package com.nexters.boolti.presentation.base + +import androidx.lifecycle.ViewModel +import com.google.firebase.crashlytics.FirebaseCrashlytics +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import timber.log.Timber + +open class BaseViewModel : ViewModel() { + private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + protected val recordExceptionHandler = CoroutineExceptionHandler { _, throwable -> + analyticsScope.launch { + FirebaseCrashlytics.getInstance().recordException(throwable) + Timber.e(throwable) + } + } + + override fun onCleared() { + super.onCleared() + analyticsScope.cancel() + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt index 240c8bad..83be0f8d 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt @@ -1,21 +1,21 @@ package com.nexters.boolti.presentation.screen -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.AuthRepository +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val authRepository: AuthRepository, -) : ViewModel() { +) : BaseViewModel() { val loggedIn = authRepository.loggedIn.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), @@ -29,8 +29,7 @@ class HomeViewModel @Inject constructor( private fun initUserInfo() { authRepository.getUserAndCache() - .catch { } - .launchIn(viewModelScope) + .launchIn(viewModelScope + recordExceptionHandler) } private fun sendFcmToken() { From 172029e37e6803185f7475da418f1524861ce82b Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 8 Mar 2024 20:29:07 +0900 Subject: [PATCH 051/129] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/base/BaseViewModel.kt | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/base/BaseViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/base/BaseViewModel.kt index 104d2fc2..8a5bd4de 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/base/BaseViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/base/BaseViewModel.kt @@ -1,26 +1,18 @@ package com.nexters.boolti.presentation.base import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.google.firebase.crashlytics.FirebaseCrashlytics import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import timber.log.Timber open class BaseViewModel : ViewModel() { - private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) protected val recordExceptionHandler = CoroutineExceptionHandler { _, throwable -> - analyticsScope.launch { + viewModelScope.launch(Dispatchers.IO) { FirebaseCrashlytics.getInstance().recordException(throwable) Timber.e(throwable) } } - - override fun onCleared() { - super.onCleared() - analyticsScope.cancel() - } } From eb1c64e9b307eb3fc79b61f0f204d423fc5b1212 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 8 Mar 2024 20:56:33 +0900 Subject: [PATCH 052/129] =?UTF-8?q?fix=20:=20launchedIn=20=EC=B9=9C?= =?UTF-8?q?=EA=B5=AC=EB=93=A4=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/my/MyViewModel.kt | 17 ++++------------- .../screen/refund/RefundViewModel.kt | 17 ++++++----------- .../reservations/ReservationDetailViewModel.kt | 11 ++++++----- .../reservations/ReservationsViewModel.kt | 12 ++++++------ .../presentation/screen/show/ShowViewModel.kt | 16 ++++++---------- .../ticket/detail/TicketDetailViewModel.kt | 10 +++++----- .../screen/ticketing/TicketingViewModel.kt | 7 ++++--- 7 files changed, 37 insertions(+), 53 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyViewModel.kt index f33082ca..83a22b8a 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyViewModel.kt @@ -1,26 +1,20 @@ package com.nexters.boolti.presentation.screen.my -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.AuthRepository +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import javax.inject.Inject @HiltViewModel class MyViewModel @Inject constructor( private val authRepository: AuthRepository, -) : ViewModel() { +) : BaseViewModel() { val user = authRepository.cachedUser.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), @@ -35,9 +29,6 @@ class MyViewModel @Inject constructor( fun fetchMyInfo() { authRepository.getUserAndCache() - .catch { - // TODO 예외처리 - } - .launchIn(viewModelScope) + .launchIn(viewModelScope + recordExceptionHandler) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundViewModel.kt index d46bf03f..a1120693 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundViewModel.kt @@ -1,10 +1,10 @@ package com.nexters.boolti.presentation.screen.refund import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.ReservationRepository import com.nexters.boolti.domain.request.RefundRequest +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -12,18 +12,18 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import javax.inject.Inject @HiltViewModel class RefundViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val reservationRepository: ReservationRepository, -) : ViewModel() { +) : BaseViewModel() { private val reservationId: String = checkNotNull(savedStateHandle["reservationId"]) { "reservationId가 전달되어야 합니다." } @@ -49,10 +49,7 @@ class RefundViewModel @Inject constructor( .onEach { reservation -> _uiState.update { it.copy(reservation = reservation) } } - .catch { - it.printStackTrace() - } - .launchIn(viewModelScope) + .launchIn(viewModelScope + recordExceptionHandler) } fun refund() { @@ -66,9 +63,7 @@ class RefundViewModel @Inject constructor( ) reservationRepository.refund(request).onEach { sendEvent(RefundEvent.SuccessfullyRefunded) - }.catch { - it.printStackTrace() - }.launchIn(viewModelScope) + }.launchIn(viewModelScope + recordExceptionHandler) } fun updateReason(newReason: String) { @@ -90,4 +85,4 @@ class RefundViewModel @Inject constructor( fun updateAccountNumber(newAccountNumber: String) { _uiState.update { it.copy(accountNumber = newAccountNumber) } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailViewModel.kt index 2f2bd52a..81c5fb96 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.ConfigRepository import com.nexters.boolti.domain.repository.ReservationRepository import com.nexters.boolti.domain.usecase.GetRefundPolicyUsecase +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -15,6 +16,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update +import kotlinx.coroutines.plus import javax.inject.Inject @HiltViewModel @@ -22,7 +24,7 @@ class ReservationDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val reservationRepository: ReservationRepository, private val getRefundPolicyUsecase: GetRefundPolicyUsecase, -) : ViewModel() { +) : BaseViewModel() { private val reservationId: String = checkNotNull(savedStateHandle["reservationId"]) { "reservationId가 전달되어야 합니다." } @@ -46,18 +48,17 @@ class ReservationDetailViewModel @Inject constructor( _uiState.update { ReservationDetailUiState.Success(reservation) } } .catch { - it.printStackTrace() _uiState.update { ReservationDetailUiState.Error() } + throw it } - .launchIn(viewModelScope) + .launchIn(viewModelScope + recordExceptionHandler) } private fun fetchRefundPolicy() { getRefundPolicyUsecase() - .catch { it.printStackTrace() } .onEach { refundPolicy -> _refundPolicy.value = refundPolicy } - .launchIn(viewModelScope) + .launchIn(viewModelScope + recordExceptionHandler) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsViewModel.kt index 390686a1..3e3cf723 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsViewModel.kt @@ -1,10 +1,9 @@ package com.nexters.boolti.presentation.screen.reservations -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.ReservationRepository +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -13,12 +12,13 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update +import kotlinx.coroutines.plus import javax.inject.Inject @HiltViewModel class ReservationsViewModel @Inject constructor( private val reservationRepository: ReservationRepository, -) : ViewModel() { +) : BaseViewModel() { private val _uiState: MutableStateFlow = MutableStateFlow(ReservationsUiState.Loading) val uiState: StateFlow = _uiState.asStateFlow() @@ -36,9 +36,9 @@ class ReservationsViewModel @Inject constructor( _uiState.update { ReservationsUiState.Success(reservations) } } .catch { - it.printStackTrace() _uiState.update { ReservationsUiState.Error } + throw it } - .launchIn(viewModelScope) + .launchIn(viewModelScope + recordExceptionHandler) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowViewModel.kt index 30f21de4..46dafb73 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowViewModel.kt @@ -1,13 +1,12 @@ package com.nexters.boolti.presentation.screen.show -import android.view.SearchEvent -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.model.ReservationState -import com.nexters.boolti.domain.repository.ReservationRepository import com.nexters.boolti.domain.model.User import com.nexters.boolti.domain.repository.AuthRepository +import com.nexters.boolti.domain.repository.ReservationRepository import com.nexters.boolti.domain.repository.ShowRepository +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -16,13 +15,13 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import timber.log.Timber import javax.inject.Inject @@ -31,7 +30,7 @@ class ShowViewModel @Inject constructor( private val showRepository: ShowRepository, private val reservationRepository: ReservationRepository, authRepository: AuthRepository, -) : ViewModel() { +) : BaseViewModel() { val user: StateFlow = authRepository.cachedUser.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), @@ -79,9 +78,6 @@ class ShowViewModel @Inject constructor( it.copy(hasPendingTicket = hasPendingTicket) } } - .catch { - // TODO 예외 처리 - } - .launchIn(viewModelScope) + .launchIn(viewModelScope + recordExceptionHandler) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailViewModel.kt index 1ce79556..3f323069 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailViewModel.kt @@ -7,6 +7,7 @@ import com.nexters.boolti.domain.exception.ManagerCodeException import com.nexters.boolti.domain.repository.TicketRepository import com.nexters.boolti.domain.request.ManagerCodeRequest import com.nexters.boolti.domain.usecase.GetRefundPolicyUsecase +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel @@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import javax.inject.Inject @HiltViewModel @@ -26,7 +28,7 @@ class TicketDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val repository: TicketRepository, private val getRefundPolicyUsecase: GetRefundPolicyUsecase, -) : ViewModel() { +) : BaseViewModel() { private val ticketId: String = requireNotNull(savedStateHandle["ticketId"]) { "TicketDetailViewModel 에 ticketId 가 전달되지 않았습니다." } @@ -52,11 +54,9 @@ class TicketDetailViewModel @Inject constructor( _uiState.update { it.copy(ticket = ticket) } } - getRefundPolicyUsecase().catch { e -> - e.printStackTrace() - }.onEach { refundPolicy -> + getRefundPolicyUsecase().onEach { refundPolicy -> _uiState.update { it.copy(refundPolicy = refundPolicy) } - }.launchIn(viewModelScope) + }.launchIn(viewModelScope + recordExceptionHandler) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt index 8ff018c2..ea6009a3 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt @@ -10,6 +10,7 @@ import com.nexters.boolti.domain.request.TicketingInfoRequest import com.nexters.boolti.domain.request.TicketingRequest import com.nexters.boolti.domain.usecase.GetRefundPolicyUsecase import com.nexters.boolti.domain.usecase.GetUserUsecase +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import timber.log.Timber import javax.inject.Inject @@ -31,7 +33,7 @@ class TicketingViewModel @Inject constructor( private val repository: TicketingRepository, getUserUsecase: GetUserUsecase, private val getRefundPolicyUsecase: GetRefundPolicyUsecase, -) : ViewModel() { +) : BaseViewModel() { private val showId: String = requireNotNull(savedStateHandle["showId"]) private val salesTicketTypeId: String = requireNotNull(savedStateHandle["salesTicketId"]) private val ticketCount: Int = savedStateHandle["ticketCount"] ?: 1 @@ -114,13 +116,12 @@ class TicketingViewModel @Inject constructor( } } getRefundPolicyUsecase() - .catch { e -> e.printStackTrace() } .onEach { refundPolicy -> _uiState.update { it.copy(refundPolicy = refundPolicy) } } - .launchIn(viewModelScope) + .launchIn(viewModelScope + recordExceptionHandler) } } From 1db71238a5604e34a05ed352aacf878daf549a59 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 8 Mar 2024 21:28:36 +0900 Subject: [PATCH 053/129] =?UTF-8?q?fix=20:=20singleOrNull=20=EC=B9=9C?= =?UTF-8?q?=EA=B5=AC=EB=93=A4=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/presentation/QrScanViewModel.kt | 12 ++++++------ .../presentation/screen/payment/PaymentViewModel.kt | 8 +++----- .../presentation/screen/qr/HostedShowViewModel.kt | 8 ++++---- .../presentation/screen/ticket/TicketViewModel.kt | 5 +++-- .../screen/ticket/detail/TicketDetailViewModel.kt | 10 ++++------ .../screen/ticketing/TicketingViewModel.kt | 11 +++++------ 6 files changed, 25 insertions(+), 29 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/QrScanViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/QrScanViewModel.kt index e075119d..0b100dc7 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/QrScanViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/QrScanViewModel.kt @@ -7,6 +7,7 @@ import com.nexters.boolti.domain.exception.QrErrorType import com.nexters.boolti.domain.exception.QrScanException import com.nexters.boolti.domain.repository.HostRepository import com.nexters.boolti.domain.request.QrScanRequest +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -23,7 +24,7 @@ import javax.inject.Inject class QrScanViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val hostRepository: HostRepository, -) : ViewModel() { +) : BaseViewModel() { private var lastCode: String? = null // 테스트 코드: wkjai-qoxzaz private val showId: String = requireNotNull(savedStateHandle["showId"]) @@ -56,7 +57,7 @@ class QrScanViewModel @Inject constructor( * 입장 확인 */ private fun requestEntrance(entryCode: String) { - viewModelScope.launch { + viewModelScope.launch(recordExceptionHandler) { hostRepository.requestEntrance( QrScanRequest(showId = showId, entryCode = entryCode) ).catch { e -> @@ -66,6 +67,7 @@ class QrScanViewModel @Inject constructor( event(QrScanEvent.ScanError(type)) } } + else -> throw e } }.singleOrNull()?.let { event(QrScanEvent.ScanSuccess) @@ -74,10 +76,8 @@ class QrScanViewModel @Inject constructor( } private fun getManagerCode() { - viewModelScope.launch { - hostRepository.getManagerCode(showId).catch { e -> - e.printStackTrace() - }.singleOrNull()?.let { code -> + viewModelScope.launch(recordExceptionHandler) { + hostRepository.getManagerCode(showId).singleOrNull()?.let { code -> _uiState.update { it.copy(managerCode = code) } } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentViewModel.kt index cef6549e..474cfce7 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.TicketingRepository +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -16,7 +17,7 @@ import javax.inject.Inject class PaymentViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val repository: TicketingRepository, -) : ViewModel() { +) : BaseViewModel() { private val reservationId: String = requireNotNull(savedStateHandle["reservationId"]) { "TicketingCompleteViewModel 에 reservationId 가 전달되지 않았습니다." } @@ -29,11 +30,8 @@ class PaymentViewModel @Inject constructor( } private fun load() { - viewModelScope.launch { + viewModelScope.launch(recordExceptionHandler) { repository.getPaymentInfo(reservationId) - .catch { e -> - e.printStackTrace() - } .singleOrNull()?.let { _uiState.value = PaymentState.Success(reservationDetail = it) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowViewModel.kt index c7c6fd28..e831ca23 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowViewModel.kt @@ -1,8 +1,8 @@ package com.nexters.boolti.presentation.screen.qr -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.HostRepository +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -16,7 +16,7 @@ import javax.inject.Inject @HiltViewModel class HostedShowViewModel @Inject constructor( private val repository: HostRepository, -) : ViewModel() { +) : BaseViewModel() { private val _uiState = MutableStateFlow(HostedShowState()) val uiState = _uiState.asStateFlow() @@ -25,13 +25,13 @@ class HostedShowViewModel @Inject constructor( } private fun load() { - viewModelScope.launch { + viewModelScope.launch(recordExceptionHandler) { repository.getHostedShows() .onStart { _uiState.update { it.copy(loading = true) } }.catch { e -> - e.printStackTrace() _uiState.update { it.copy(loading = false) } + throw e }.singleOrNull()?.let { shows -> _uiState.update { it.copy(loading = false, shows = shows) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketViewModel.kt index 2e00cff8..e440d9b1 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketViewModel.kt @@ -3,6 +3,7 @@ package com.nexters.boolti.presentation.screen.ticket import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.TicketRepository +import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -16,12 +17,12 @@ import javax.inject.Inject @HiltViewModel class TicketViewModel @Inject constructor( private val ticketRepository: TicketRepository, -) : ViewModel() { +) : BaseViewModel() { private val _uiState = MutableStateFlow(TicketUiState(loading = true)) val uiState = _uiState.asStateFlow() fun load() { - viewModelScope.launch { + viewModelScope.launch(recordExceptionHandler) { _uiState.update { it.copy(loading = true) } ticketRepository.getTicket() .onCompletion { diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailViewModel.kt index 3f323069..49a5bd4c 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailViewModel.kt @@ -47,10 +47,8 @@ class TicketDetailViewModel @Inject constructor( } private fun load(): Job { - return viewModelScope.launch { - repository.getTicket(ticketId).catch { e -> - e.printStackTrace() - }.singleOrNull()?.let { ticket -> + return viewModelScope.launch(recordExceptionHandler) { + repository.getTicket(ticketId).singleOrNull()?.let { ticket -> _uiState.update { it.copy(ticket = ticket) } } @@ -64,15 +62,15 @@ class TicketDetailViewModel @Inject constructor( fun requestEntrance(managerCode: String) { val ticket = uiState.value.ticket - viewModelScope.launch { + viewModelScope.launch(recordExceptionHandler) { repository.requestEntrance( ManagerCodeRequest(showId = ticket.showId, ticketId = ticket.ticketId, managerCode = managerCode) ).catch { e -> - e.printStackTrace() when (e) { is ManagerCodeException -> { _managerCodeState.update { it.copy(error = e.errorType) } } + else -> throw e } }.singleOrNull()?.let { event(TicketDetailEvent.ManagerCodeValid) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt index ea6009a3..ef7d56d7 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt @@ -78,12 +78,12 @@ class TicketingViewModel @Inject constructor( } fun reservation() { - viewModelScope.launch { + viewModelScope.launch(recordExceptionHandler) { repository.requestReservation(reservationRequest) .onStart { _uiState.update { it.copy(loading = true) } } .catch { e -> - e.printStackTrace() _uiState.update { it.copy(loading = false) } + throw e } .singleOrNull()?.let { reservationId -> Timber.tag("MANGBAAM-TicketingViewModel(reservation)").d("예매 성공: $reservationId") @@ -94,9 +94,8 @@ class TicketingViewModel @Inject constructor( } private fun load() { - viewModelScope.launch { + viewModelScope.launch(recordExceptionHandler) { repository.getTicketingInfo(TicketingInfoRequest(showId, salesTicketTypeId, ticketCount)) - .catch { e -> e.printStackTrace() } .onStart { _uiState.update { it.copy(loading = true) } } @@ -132,7 +131,7 @@ class TicketingViewModel @Inject constructor( } fun checkInviteCode() { - viewModelScope.launch { + viewModelScope.launch(recordExceptionHandler) { repository.checkInviteCode( CheckInviteCodeRequest( showId = showId, @@ -142,8 +141,8 @@ class TicketingViewModel @Inject constructor( ).onStart { _uiState.update { it.copy(loading = true) } }.catch { e -> - e.printStackTrace() _uiState.update { it.copy(loading = false) } + throw e }.singleOrNull()?.let { status -> _uiState.update { it.copy(loading = false, inviteCodeStatus = status) From dca697704ab5cf8c72e3c1176d323c42e4264448 Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 9 Mar 2024 01:32:31 +0900 Subject: [PATCH 054/129] Create README.md --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..034df677 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# 핫한 공연 예매의 시작 불티 + +- 플레이 스토어 : https://play.google.com/store/apps/details?id=com.nexters.boolti +- 앱 스토어 : https://apps.apple.com/kr/app/%EB%B6%88%ED%8B%B0/id6476589322 +- 주최자용 웹 : https://boolti.in + +
+ +## Screenshots + + + + + +

+ +## Android developers + +|Android|Android| +|:---:|:---:| +|[박명범](https://github.com/mangbaam)|[송준영](https://github.com/HamBP)| +||| + +
+ +## Other repositories +- iOS Repository : https://github.com/Nexters/Boolti-iOS +- FE Repository : https://github.com/Nexters/boolti-web +- BE Repository : private From c14be085cd14fe315fbe7591529f71627973b639 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sat, 9 Mar 2024 11:10:53 +0900 Subject: [PATCH 055/129] =?UTF-8?q?Boolti-190=20feat:=20=EA=B3=B5=EC=97=B0?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=20=EB=A9=94=EB=89=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/my/MyScreen.kt | 26 +++++++++++++++---- presentation/src/main/res/values/strings.xml | 1 + 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt index 580c66cc..9b31bcfa 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState @@ -25,6 +24,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -52,6 +52,7 @@ fun MyScreen( ) { val user by viewModel.user.collectAsStateWithLifecycle() var openLogoutDialog by remember { mutableStateOf(false) } + val uriHandler = LocalUriHandler.current LaunchedEffect(Unit) { viewModel.fetchMyInfo() @@ -67,17 +68,32 @@ fun MyScreen( text = stringResource(id = R.string.my_ticketing_history), onClick = if (user == null) requireLogin else navigateToReservations, ) - Spacer(modifier = Modifier.height(12.dp)) MyButton( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp), + text = stringResource(R.string.my_register_show), + onClick = { + if (user != null) { + uriHandler.openUri("https://boolti.in/home") // 웹에서 로그인되지 않은 상태라면 login 페이지로 리다이렉션 시킴 + } else { + uriHandler.openUri("https://boolti.in/login") + } + } + ) + MyButton( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp), text = stringResource(id = R.string.my_scan_qr), onClick = onClickQrScan, ) if (user != null) { - Spacer(modifier = Modifier.height(12.dp)) MyButton( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp), text = stringResource(id = R.string.my_logout), onClick = { openLogoutDialog = true }, ) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index fb8b9ccd..6e011c7d 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -175,6 +175,7 @@ 예매 내역 QR 스캔 정말 로그아웃 하시겠어요? + 공연 등록 QR 스캔 From 8af1fc2167989b05d61b7028095b204d8f94571a Mon Sep 17 00:00:00 2001 From: algosketch Date: Tue, 12 Mar 2024 01:06:24 +0900 Subject: [PATCH 056/129] =?UTF-8?q?refactor=20:=20homeScreen=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 4 ++- .../screen/home/HomeNavigation.kt | 30 +++++++++++++++++++ .../presentation/screen/home/HomeScreen.kt | 26 +--------------- 3 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 10e5a300..1968417f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -84,7 +84,7 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: navController = navController, startDestination = Home.route, ) { - HomeScreen(modifier = modifier, navController = navController) + HomeScreen(modifier = modifier, navigateTo = navController::navigateTo) LoginScreen(modifier = modifier, navController = navController) SignoutScreen(navController = navController) ReservationsScreen(navController = navController) @@ -125,3 +125,5 @@ inline fun NavBackStackEntry.sharedViewModel( } return hiltViewModel(parentEntry) } + +private fun NavController.navigateTo(route: String) = navigate(route) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt new file mode 100644 index 00000000..30ece8dd --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt @@ -0,0 +1,30 @@ +package com.nexters.boolti.presentation.screen.home + +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination + +fun NavGraphBuilder.HomeScreen( + navigateTo: (String) -> Unit, + modifier: Modifier = Modifier, +) { + composable( + route = MainDestination.Home.route, + ) { + HomeScreen( + modifier = modifier, + onClickShowItem = { navigateTo("${MainDestination.ShowDetail.route}/$it") }, + onClickTicket = { navigateTo("${MainDestination.TicketDetail.route}/$it") }, + onClickQr = { code, ticketName -> + navigateTo( + "${MainDestination.Qr.route}/${code.filter { c -> c.isLetterOrDigit() }}?ticketName=$ticketName" + ) + }, + onClickQrScan = { navigateTo(MainDestination.HostedShows.route) }, + onClickSignout = { navigateTo(MainDestination.SignOut.route) }, + navigateToReservations = { navigateTo(MainDestination.Reservations.route) }, + requireLogin = { navigateTo(MainDestination.Login.route) } + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt index ec1e537d..e0fbdae1 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt @@ -40,32 +40,8 @@ import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey50 import com.nexters.boolti.presentation.theme.Grey85 -fun NavGraphBuilder.HomeScreen( - navController: NavController, - modifier: Modifier = Modifier, -) { - composable( - route = MainDestination.Home.route, - ) { - HomeScreen( - modifier = modifier, - onClickShowItem = { navController.navigate("${MainDestination.ShowDetail.route}/$it") }, - onClickTicket = { navController.navigate("${MainDestination.TicketDetail.route}/$it") }, - onClickQr = { code, ticketName -> - navController.navigate( - "${MainDestination.Qr.route}/${code.filter { c -> c.isLetterOrDigit() }}?ticketName=$ticketName" - ) - }, - onClickQrScan = { navController.navigate(MainDestination.HostedShows.route) }, - onClickSignout = { navController.navigate(MainDestination.SignOut.route) }, - navigateToReservations = { navController.navigate(MainDestination.Reservations.route) }, - requireLogin = { navController.navigate(MainDestination.Login.route) } - ) - } -} - @Composable -private fun HomeScreen( +fun HomeScreen( onClickShowItem: (showId: String) -> Unit, onClickTicket: (ticketId: String) -> Unit, onClickQr: (data: String, ticketName: String) -> Unit, From ba96b16bac40e92930afb40809f7897199312ebe Mon Sep 17 00:00:00 2001 From: algosketch Date: Tue, 12 Mar 2024 02:00:56 +0900 Subject: [PATCH 057/129] =?UTF-8?q?refactor=20:=20=EB=82=98=EB=A8=B8?= =?UTF-8?q?=EC=A7=80=20=ED=99=94=EB=A9=B4=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EC=B6=9C,=20navCont?= =?UTF-8?q?roller=20=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 73 +++++++++++++++---- .../screen/login/LoginNavigation.kt | 21 ++++++ .../presentation/screen/login/LoginScreen.kt | 16 +--- .../screen/payment/PaymentNavigation.kt | 30 ++++++++ .../screen/payment/PaymentScreen.kt | 25 +------ .../screen/qr/HostedShowNavigation.kt | 22 ++++++ .../screen/qr/HostedShowScreen.kt | 20 +---- .../screen/qr/QrFullNavigation.kt | 20 +++++ .../presentation/screen/qr/QrFullScreen.kt | 16 +--- .../screen/refund/RefundNavigation.kt | 19 +++++ .../screen/refund/RefundScreen.kt | 15 +--- .../screen/report/ReportNavigation.kt | 21 ++++++ .../screen/report/ReportScreen.kt | 17 +---- .../ReservationDetailNavigation.kt | 21 ++++++ .../reservations/ReservationDetailScreen.kt | 16 +--- .../reservations/ReservationsNavigation.kt | 21 ++++++ .../screen/reservations/ReservationsScreen.kt | 16 +--- .../show/ShowDetailContentNavigation.kt | 25 +++++++ .../screen/show/ShowDetailContentScreen.kt | 21 +----- .../screen/show/ShowDetailNavigation.kt | 38 ++++++++++ .../screen/show/ShowDetailScreen.kt | 35 +-------- .../screen/show/ShowImagesNavigation.kt | 27 +++++++ .../screen/show/ShowImagesScreen.kt | 22 +----- .../screen/signout/SignoutNavigation.kt | 19 +++++ .../screen/signout/SignoutScreen.kt | 20 +---- .../ticket/detail/TicketDetailScreen.kt | 9 ++- .../screen/ticketing/TicketingNavigation.kt | 29 ++++++++ .../screen/ticketing/TicketingScreen.kt | 20 +---- 28 files changed, 394 insertions(+), 260 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNavigation.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingNavigation.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 1968417f..e48651ed 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -20,6 +20,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import com.nexters.boolti.presentation.component.ToastSnackbarHost +import com.nexters.boolti.presentation.extension.navigateToHome import com.nexters.boolti.presentation.screen.MainDestination.Home import com.nexters.boolti.presentation.screen.MainDestination.ShowDetail import com.nexters.boolti.presentation.screen.home.HomeScreen @@ -85,33 +86,77 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: startDestination = Home.route, ) { HomeScreen(modifier = modifier, navigateTo = navController::navigateTo) - LoginScreen(modifier = modifier, navController = navController) - SignoutScreen(navController = navController) - ReservationsScreen(navController = navController) - ReservationDetailScreen(navController = navController) - RefundScreen(navController = navController) + LoginScreen(modifier = modifier, popBackStack = navController::popBackStack) + SignoutScreen( + navigateToHome = navController::navigateToHome, + popBackStack = navController::popBackStack + ) + ReservationsScreen( + navigateTo = navController::navigateTo, + popBackStack = navController::popBackStack + ) + ReservationDetailScreen( + navigateTo = navController::navigateTo, + popBackStack = navController::popBackStack + ) + RefundScreen(popBackStack = navController::popBackStack) navigation( route = "${ShowDetail.route}/{$showId}", startDestination = "detail", arguments = ShowDetail.arguments, ) { - ShowDetailScreen(modifier = modifier, navController = navController) - ShowImagesScreen(navController = navController) - ShowDetailContentScreen(modifier = modifier, navController = navController) - ReportScreen(modifier = modifier, navController = navController) + ShowDetailScreen( + modifier = modifier, + navigateTo = navController::navigateTo, + popBackStack = navController::popBackStack, + navigateToHome = navController::navigateToHome, + getSharedViewModel = { entry -> entry.sharedViewModel(navController) } + ) + ShowImagesScreen( + popBackStack = navController::popBackStack, + getSharedViewModel = { entry -> entry.sharedViewModel(navController) } + ) + ShowDetailContentScreen( + modifier = modifier, + popBackStack = navController::popBackStack, + getSharedViewModel = { entry -> entry.sharedViewModel(navController) } + ) + ReportScreen( + modifier = modifier, + navigateToHome = navController::navigateToHome, + popBackStack = navController::popBackStack, + ) } - TicketDetailScreen(modifier = modifier, navController = navController) - TicketingScreen(modifier = modifier, navController = navController) - QrFullScreen(modifier = modifier, navController = navController) + TicketDetailScreen( + modifier = modifier, + navigateTo = navController::navigateTo, + popBackStack = navController::popBackStack + ) + TicketingScreen( + modifier = modifier, + navigateTo = navController::navigateTo, + popBackStack = navController::popBackStack + ) + QrFullScreen(modifier = modifier, popBackStack = navController::popBackStack) HostedShowScreen( modifier = modifier, onClickShow = onClickQrScan, - navController = navController, + popBackStack = navController::popBackStack, ) - PaymentScreen(navController = navController) + PaymentScreen( + navigateTo = navController::navigateTo, + popBackStack = navController::popBackStack, + popInclusiveBackStack = { route -> + navController.popBackStack( + route = route, + inclusive = true, + ) + }, + navigateToHome = navController::navigateToHome + ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginNavigation.kt new file mode 100644 index 00000000..42d4675a --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginNavigation.kt @@ -0,0 +1,21 @@ +package com.nexters.boolti.presentation.screen.login + +import androidx.compose.ui.Modifier +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination + +fun NavGraphBuilder.LoginScreen( + popBackStack: () -> Unit, + modifier: Modifier = Modifier, +) { + composable( + route = MainDestination.Login.route, + ) { + LoginScreen( + modifier = modifier, + onBackPressed = popBackStack + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt index 4dc64440..4b9a128b 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt @@ -48,23 +48,9 @@ import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.subTextPadding -fun NavGraphBuilder.LoginScreen( - navController: NavController, - modifier: Modifier = Modifier, -) { - composable( - route = MainDestination.Login.route, - ) { - LoginScreen( - modifier = modifier, - onBackPressed = { navController.popBackStack() } - ) - } -} - @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun LoginScreen( +fun LoginScreen( onBackPressed: () -> Unit, modifier: Modifier = Modifier, viewModel: LoginViewModel = hiltViewModel(), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentNavigation.kt new file mode 100644 index 00000000..632e7e8d --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentNavigation.kt @@ -0,0 +1,30 @@ +package com.nexters.boolti.presentation.screen.payment + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.reservationId +import com.nexters.boolti.presentation.screen.showId + +fun NavGraphBuilder.PaymentScreen( + navigateTo: (String) -> Unit, + popBackStack: () -> Unit, + popInclusiveBackStack: (String) -> Unit, + navigateToHome: () -> Unit, +) { + composable( + route = "${MainDestination.Payment.route}/{$reservationId}?showId={$showId}", + arguments = MainDestination.Payment.arguments, + ) { + val showId = it.arguments?.getString(showId) + PaymentScreen( + onClickHome = navigateToHome, + onClickClose = { + showId?.let { showId -> + popInclusiveBackStack("${MainDestination.ShowDetail.route}/$showId") + navigateTo("${MainDestination.ShowDetail.route}/$showId") + } ?: popBackStack() + }, + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt index 587f0609..32b5ffb5 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt @@ -30,31 +30,8 @@ import com.nexters.boolti.presentation.screen.reservationId import com.nexters.boolti.presentation.screen.showId import kotlinx.coroutines.launch -fun NavGraphBuilder.PaymentScreen( - navController: NavController, -) { - composable( - route = "${MainDestination.Payment.route}/{$reservationId}?showId={$showId}", - arguments = MainDestination.Payment.arguments, - ) { - val showId = it.arguments?.getString(showId) - PaymentScreen( - onClickHome = { navController.navigateToHome() }, - onClickClose = { - showId?.let { showId -> - navController.popBackStack( - "${MainDestination.ShowDetail.route}/$showId", - inclusive = true - ) - navController.navigate("${MainDestination.ShowDetail.route}/$showId") - } ?: navController.popBackStack() - }, - ) - } -} - @Composable -private fun PaymentScreen( +fun PaymentScreen( onClickHome: () -> Unit, onClickClose: () -> Unit, viewModel: PaymentViewModel = hiltViewModel(), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowNavigation.kt new file mode 100644 index 00000000..4c998285 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowNavigation.kt @@ -0,0 +1,22 @@ +package com.nexters.boolti.presentation.screen.qr + +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination + +fun NavGraphBuilder.HostedShowScreen( + popBackStack: () -> Unit, + onClickShow: (showId: String, showName: String) -> Unit, + modifier: Modifier = Modifier, +) { + composable( + route = MainDestination.HostedShows.route + ) { + HostedShowScreen( + modifier = modifier, + onClickShow = onClickShow, + onClickBack = popBackStack + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt index 8522869b..d4640917 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt @@ -47,26 +47,8 @@ import com.nexters.boolti.presentation.theme.point1 import java.time.LocalDate import java.time.LocalDateTime -fun NavGraphBuilder.HostedShowScreen( - onClickShow: (showId: String, showName: String) -> Unit, - navController: NavController, - modifier: Modifier = Modifier, -) { - composable( - route = MainDestination.HostedShows.route - ) { - HostedShowScreen( - modifier = modifier, - onClickShow = onClickShow, - onClickBack = { - navController.popBackStack() - } - ) - } -} - @Composable -private fun HostedShowScreen( +fun HostedShowScreen( onClickBack: () -> Unit, onClickShow: (showId: String, showName: String) -> Unit, modifier: Modifier = Modifier, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullNavigation.kt new file mode 100644 index 00000000..5ea75cd6 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullNavigation.kt @@ -0,0 +1,20 @@ +package com.nexters.boolti.presentation.screen.qr + +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.data +import com.nexters.boolti.presentation.screen.ticketName + +fun NavGraphBuilder.QrFullScreen( + popBackStack: () -> Unit, + modifier: Modifier = Modifier, +) { + composable( + route = "${MainDestination.Qr.route}/{$data}?ticketName={$ticketName}", + arguments = MainDestination.Qr.arguments, + ) { + QrFullScreen(modifier = modifier) { popBackStack() } + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt index 1172a0f2..61b18782 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt @@ -34,22 +34,8 @@ import com.nexters.boolti.presentation.theme.Grey85 import com.nexters.boolti.presentation.theme.Grey90 import com.nexters.boolti.presentation.util.rememberQrBitmapPainter -fun NavGraphBuilder.QrFullScreen( - navController: NavController, - modifier: Modifier = Modifier, -) { - composable( - route = "${MainDestination.Qr.route}/{$data}?ticketName={$ticketName}", - arguments = MainDestination.Qr.arguments, - ) { - QrFullScreen(modifier = modifier) { - navController.popBackStack() - } - } -} - @Composable -private fun QrFullScreen( +fun QrFullScreen( modifier: Modifier = Modifier, viewModel: QrFullViewModel = hiltViewModel(), onClose: () -> Unit, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundNavigation.kt new file mode 100644 index 00000000..07509aea --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundNavigation.kt @@ -0,0 +1,19 @@ +package com.nexters.boolti.presentation.screen.refund + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.reservationId + +fun NavGraphBuilder.RefundScreen( + popBackStack: () -> Unit, +) { + composable( + route = "${MainDestination.Refund.route}/{$reservationId}", + arguments = MainDestination.Refund.arguments, + ) { + RefundScreen( + onBackPressed = popBackStack, + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt index b3562f30..e4039881 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt @@ -91,22 +91,9 @@ import com.nexters.boolti.presentation.theme.point4 import com.nexters.boolti.presentation.util.PhoneNumberVisualTransformation import kotlinx.coroutines.launch -fun NavGraphBuilder.RefundScreen( - navController: NavController, -) { - composable( - route = "${MainDestination.Refund.route}/{$reservationId}", - arguments = MainDestination.Refund.arguments, - ) { - RefundScreen( - onBackPressed = { navController.popBackStack() }, - ) - } -} - @OptIn(ExperimentalFoundationApi::class) @Composable -private fun RefundScreen( +fun RefundScreen( onBackPressed: () -> Unit, viewModel: RefundViewModel = hiltViewModel(), ) { diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportNavigation.kt new file mode 100644 index 00000000..969e5657 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportNavigation.kt @@ -0,0 +1,21 @@ +package com.nexters.boolti.presentation.screen.report + +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable + +fun NavGraphBuilder.ReportScreen( + navigateToHome: () -> Unit, + popBackStack: () -> Unit, + modifier: Modifier = Modifier, +) { + composable( + route = "report/{showId}", + ) { + ReportScreen( + onBackPressed = popBackStack, + popupToHome = navigateToHome, + modifier = modifier, + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt index 60df5479..816968f3 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt @@ -33,23 +33,8 @@ import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.point4 -fun NavGraphBuilder.ReportScreen( - navController: NavController, - modifier: Modifier = Modifier, -) { - composable( - route = "report/{showId}", - ) { - ReportScreen( - onBackPressed = { navController.popBackStack() }, - popupToHome = { navController.navigateToHome() }, - modifier = modifier, - ) - } -} - @Composable -private fun ReportScreen( +fun ReportScreen( onBackPressed: () -> Unit, popupToHome: () -> Unit, modifier: Modifier = Modifier, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailNavigation.kt new file mode 100644 index 00000000..3ff013a4 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailNavigation.kt @@ -0,0 +1,21 @@ +package com.nexters.boolti.presentation.screen.reservations + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.reservationId + +fun NavGraphBuilder.ReservationDetailScreen( + navigateTo: (String) -> Unit, + popBackStack: () -> Unit, +) { + composable( + route = "${MainDestination.ReservationDetail.route}/{$reservationId}", + arguments = MainDestination.ReservationDetail.arguments, + ) { + ReservationDetailScreen( + onBackPressed = popBackStack, + navigateToRefund = { id -> navigateTo("${MainDestination.Refund.route}/$id") }, + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index 292f8b04..ed264b15 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -71,22 +71,8 @@ import com.nexters.boolti.presentation.theme.Grey90 import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.point2 -fun NavGraphBuilder.ReservationDetailScreen( - navController: NavController, -) { - composable( - route = "${MainDestination.ReservationDetail.route}/{$reservationId}", - arguments = MainDestination.ReservationDetail.arguments, - ) { - ReservationDetailScreen( - onBackPressed = { navController.popBackStack() }, - navigateToRefund = { id -> navController.navigate("${MainDestination.Refund.route}/$id") }, - ) - } -} - @Composable -private fun ReservationDetailScreen( +fun ReservationDetailScreen( onBackPressed: () -> Unit, navigateToRefund: (id: String) -> Unit, modifier: Modifier = Modifier, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsNavigation.kt new file mode 100644 index 00000000..86a64157 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsNavigation.kt @@ -0,0 +1,21 @@ +package com.nexters.boolti.presentation.screen.reservations + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.reservationId + +fun NavGraphBuilder.ReservationsScreen( + navigateTo: (String) -> Unit, + popBackStack: () -> Unit, +) { + composable( + route = MainDestination.Reservations.route, + ) { + ReservationsScreen( + onBackPressed = popBackStack, + navigateToDetail = { reservationId -> + navigateTo("${MainDestination.Reservations.route}/$reservationId") + }) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt index bfa0d6fd..28c06855 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt @@ -52,22 +52,8 @@ import com.nexters.boolti.presentation.theme.point1 import com.nexters.boolti.presentation.theme.subTextPadding import java.time.format.DateTimeFormatter -fun NavGraphBuilder.ReservationsScreen( - navController: NavController, -) { - composable( - route = MainDestination.Reservations.route, - ) { - ReservationsScreen( - onBackPressed = { navController.popBackStack() }, - navigateToDetail = { reservationId -> - navController.navigate("${MainDestination.Reservations.route}/$reservationId") - }) - } -} - @Composable -private fun ReservationsScreen( +fun ReservationsScreen( onBackPressed: () -> Unit, navigateToDetail: (reservationId: String) -> Unit, viewModel: ReservationsViewModel = hiltViewModel(), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentNavigation.kt new file mode 100644 index 00000000..8a3f347b --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentNavigation.kt @@ -0,0 +1,25 @@ +package com.nexters.boolti.presentation.screen.show + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable + +fun NavGraphBuilder.ShowDetailContentScreen( + popBackStack: () -> Unit, + getSharedViewModel: @Composable (NavBackStackEntry) -> ShowDetailViewModel, + modifier: Modifier = Modifier, +) { + composable( + route = "content", + ) { entry -> + val showViewModel: ShowDetailViewModel = getSharedViewModel(entry) + + ShowDetailContentScreen( + modifier = modifier, + viewModel = showViewModel, + onBackPressed = popBackStack + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt index d6d6d426..949e6acb 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable @@ -31,26 +32,8 @@ import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.marginHorizontal -fun NavGraphBuilder.ShowDetailContentScreen( - navController: NavController, - modifier: Modifier = Modifier, -) { - composable( - route = "content", - ) { entry -> - val showViewModel: ShowDetailViewModel = - entry.sharedViewModel(navController = navController) - - ShowDetailContentScreen( - modifier = modifier, - viewModel = showViewModel, - onBackPressed = { navController.popBackStack() } - ) - } -} - @Composable -private fun ShowDetailContentScreen( +fun ShowDetailContentScreen( onBackPressed: () -> Unit, modifier: Modifier = Modifier, viewModel: ShowDetailViewModel = hiltViewModel(), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailNavigation.kt new file mode 100644 index 00000000..f07e20d2 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailNavigation.kt @@ -0,0 +1,38 @@ +package com.nexters.boolti.presentation.screen.show + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable + +fun NavGraphBuilder.ShowDetailScreen( + navigateTo: (String) -> Unit, + popBackStack: () -> Unit, + navigateToHome: () -> Unit, + getSharedViewModel: @Composable (NavBackStackEntry) -> ShowDetailViewModel, + modifier: Modifier = Modifier, +) { + composable( + route = "detail", + ) { entry -> + val showViewModel: ShowDetailViewModel = getSharedViewModel(entry) + + ShowDetailScreen( + modifier = modifier, + onBack = popBackStack, + onClickHome = navigateToHome, + onClickContent = { navigateTo("content") }, + onTicketSelected = { showId, ticketId, ticketCount, isInviteTicket -> + navigateTo("ticketing/$showId?salesTicketId=$ticketId&ticketCount=$ticketCount&inviteTicket=$isInviteTicket") + }, + viewModel = showViewModel, + navigateToLogin = { navigateTo("login") }, + navigateToImages = { index -> navigateTo("images/$index") }, + navigateToReport = { + val showId = entry.arguments?.getString("showId") + navigateTo("report/$showId") + } + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index 3da59d2a..5faa3b47 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -51,7 +51,9 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.ViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable @@ -76,39 +78,8 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.time.format.DateTimeFormatter -fun NavGraphBuilder.ShowDetailScreen( - modifier: Modifier = Modifier, - navController: NavController, -) { - composable( - route = "detail", - ) { entry -> - val showViewModel: ShowDetailViewModel = - entry.sharedViewModel(navController = navController) - - ShowDetailScreen( - modifier = modifier, - onBack = { navController.popBackStack() }, - onClickHome = { navController.navigateToHome() }, - onClickContent = { - navController.navigate("content") - }, - onTicketSelected = { showId, ticketId, ticketCount, isInviteTicket -> - navController.navigate("ticketing/$showId?salesTicketId=$ticketId&ticketCount=$ticketCount&inviteTicket=$isInviteTicket") - }, - viewModel = showViewModel, - navigateToLogin = { navController.navigate("login") }, - navigateToImages = { index -> navController.navigate("images/$index") }, - navigateToReport = { - val showId = entry.arguments?.getString("showId") - navController.navigate("report/$showId") - } - ) - } -} - @Composable -private fun ShowDetailScreen( +fun ShowDetailScreen( onBack: () -> Unit, onClickHome: () -> Unit, onClickContent: () -> Unit, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesNavigation.kt new file mode 100644 index 00000000..b3252c49 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesNavigation.kt @@ -0,0 +1,27 @@ +package com.nexters.boolti.presentation.screen.show + +import androidx.compose.runtime.Composable +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument + +fun NavGraphBuilder.ShowImagesScreen( + popBackStack: () -> Unit, + getSharedViewModel: @Composable (NavBackStackEntry) -> ShowDetailViewModel, +) { + composable( + route = "images/{index}", + arguments = listOf(navArgument("index") { type = NavType.IntType }), + ) { entry -> + val showViewModel: ShowDetailViewModel = getSharedViewModel(entry) + val index = entry.arguments!!.getInt("index") + + ShowImagesScreen( + index = index, + viewModel = showViewModel, + onBackPressed = popBackStack, + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt index 8fe80c24..7e02d201 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType @@ -36,28 +37,9 @@ import com.nexters.boolti.presentation.screen.sharedViewModel import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.zoomable -fun NavGraphBuilder.ShowImagesScreen( - navController: NavController, -) { - composable( - route = "images/{index}", - arguments = listOf(navArgument("index") { type = NavType.IntType }), - ) { entry -> - val showViewModel: ShowDetailViewModel = - entry.sharedViewModel(navController = navController) - val index = entry.arguments!!.getInt("index") - - ShowImagesScreen( - index = index, - viewModel = showViewModel, - onBackPressed = { navController.popBackStack() }, - ) - } -} - @OptIn(ExperimentalFoundationApi::class) @Composable -private fun ShowImagesScreen( +fun ShowImagesScreen( index: Int, onBackPressed: () -> Unit, modifier: Modifier = Modifier, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNavigation.kt new file mode 100644 index 00000000..011753c3 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNavigation.kt @@ -0,0 +1,19 @@ +package com.nexters.boolti.presentation.screen.signout + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination + +fun NavGraphBuilder.SignoutScreen( + navigateToHome: () -> Unit, + popBackStack: () -> Unit, +) { + composable( + route = MainDestination.SignOut.route, + ) { + SignoutScreen( + navigateToHome = navigateToHome, + navigateBack = popBackStack, + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt index 3deb9785..3861a6f2 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt @@ -15,31 +15,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BtAppBar import com.nexters.boolti.presentation.component.MainButton -import com.nexters.boolti.presentation.extension.navigateToHome -import com.nexters.boolti.presentation.screen.MainDestination import com.nexters.boolti.presentation.theme.marginHorizontal -fun NavGraphBuilder.SignoutScreen( - navController: NavController, -) { - composable( - route = MainDestination.SignOut.route, - ) { - SignoutScreen( - navigateToHome = { navController.navigateToHome() }, - navigateBack = { navController.popBackStack() }, - ) - } -} - @Composable -private fun SignoutScreen( +fun SignoutScreen( navigateToHome: () -> Unit, navigateBack: () -> Unit, viewModel: SignoutViewModel = hiltViewModel(), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt index 3cc401c9..d50d3579 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt @@ -110,7 +110,8 @@ import java.time.LocalDate import java.time.LocalDateTime fun NavGraphBuilder.TicketDetailScreen( - navController: NavController, + navigateTo: (String) -> Unit, + popBackStack: () -> Unit, modifier: Modifier = Modifier, ) { composable( @@ -119,13 +120,13 @@ fun NavGraphBuilder.TicketDetailScreen( ) { TicketDetailScreen( modifier = modifier, - onBackClicked = { navController.popBackStack() }, + onBackClicked = popBackStack, onClickQr = { code, ticketName -> - navController.navigate( + navigateTo( "${MainDestination.Qr.route}/${code.filter { c -> c.isLetterOrDigit() }}?ticketName=$ticketName" ) }, - navigateToShowDetail = { navController.navigate("${MainDestination.ShowDetail.route}/$it") } + navigateToShowDetail = { navigateTo("${MainDestination.ShowDetail.route}/$it") } ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingNavigation.kt new file mode 100644 index 00000000..6920d12e --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingNavigation.kt @@ -0,0 +1,29 @@ +package com.nexters.boolti.presentation.screen.ticketing + +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.isInviteTicket +import com.nexters.boolti.presentation.screen.salesTicketId +import com.nexters.boolti.presentation.screen.showId +import com.nexters.boolti.presentation.screen.ticketCount + +fun NavGraphBuilder.TicketingScreen( + navigateTo: (String) -> Unit, + popBackStack: () -> Unit, + modifier: Modifier = Modifier, +) { + composable( + route = "${MainDestination.Ticketing.route}/{$showId}?salesTicketId={$salesTicketId}&ticketCount={$ticketCount}&inviteTicket={$isInviteTicket}", + arguments = MainDestination.Ticketing.arguments, + ) { + TicketingScreen( + modifier = modifier, + onBackClicked = popBackStack, + onReserved = { reservationId, showId -> + navigateTo("${MainDestination.Payment.route}/$reservationId?showId=$showId") + } + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index 2a7b77d3..4d59a577 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -96,27 +96,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.time.LocalDateTime -fun NavGraphBuilder.TicketingScreen( - navController: NavController, - modifier: Modifier = Modifier, -) { - composable( - route = "${MainDestination.Ticketing.route}/{$showId}?salesTicketId={$salesTicketId}&ticketCount={$ticketCount}&inviteTicket={$isInviteTicket}", - arguments = MainDestination.Ticketing.arguments, - ) { - TicketingScreen( - modifier = modifier, - onBackClicked = { navController.popBackStack() }, - onReserved = { reservationId, showId -> - navController.navigate("${MainDestination.Payment.route}/$reservationId?showId=$showId") - } - ) - } -} - @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun TicketingScreen( +fun TicketingScreen( modifier: Modifier = Modifier, viewModel: TicketingViewModel = hiltViewModel(), onBackClicked: () -> Unit = {}, From 1cd7b10ec951a01d8774094c3ebfbbefe009d1d5 Mon Sep 17 00:00:00 2001 From: algosketch Date: Wed, 13 Mar 2024 03:01:55 +0900 Subject: [PATCH 058/129] =?UTF-8?q?fix=20:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=EB=A1=9C=20=EB=82=B4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=8B=9C=20=EB=B0=9C=EC=83=9D=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=81=AC=EB=9E=98=EC=8B=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/show/ShowDetailEvent.kt | 6 +++++ .../screen/show/ShowDetailScreen.kt | 27 ++++++++++++++++--- .../screen/show/ShowDetailViewModel.kt | 19 ++++++++++--- .../presentation/screen/show/ShowScreen.kt | 2 +- 4 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailEvent.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailEvent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailEvent.kt new file mode 100644 index 00000000..e6abfb2f --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailEvent.kt @@ -0,0 +1,6 @@ +package com.nexters.boolti.presentation.screen.show + +sealed interface ShowDetailEvent { + data object PopBackStack : ShowDetailEvent + data class NavigateToImages(val index: Int) : ShowDetailEvent +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index d54e279b..25e31712 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -2,6 +2,7 @@ package com.nexters.boolti.presentation.screen.show import android.content.Intent import android.os.Build +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -28,9 +29,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState 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 @@ -58,7 +59,6 @@ import com.nexters.boolti.domain.model.ShowState import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.CopyButton import com.nexters.boolti.presentation.component.MainButton -import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.requireActivity import com.nexters.boolti.presentation.screen.LocalSnackbarController import com.nexters.boolti.presentation.screen.ticketing.ChooseTicketBottomSheet @@ -99,11 +99,30 @@ fun ShowDetailScreen( val window = LocalContext.current.requireActivity().window window.statusBarColor = MaterialTheme.colorScheme.surface.toArgb() + LaunchedEffect(Unit) { + viewModel.events.collect { event -> + when (event) { + is ShowDetailEvent.NavigateToImages -> { + navigateToImages(event.index) + } + + ShowDetailEvent.PopBackStack -> { + onBack() + viewModel.preventEvents() + } + } + } + } + + BackHandler { + viewModel.sendEvent(ShowDetailEvent.PopBackStack) + } + Scaffold( modifier = modifier, topBar = { ShowDetailAppBar( - onBack = onBack, + onBack = { viewModel.sendEvent(ShowDetailEvent.PopBackStack) }, onClickHome = onClickHome, navigateToReport = navigateToReport, ) @@ -121,7 +140,7 @@ fun ShowDetailScreen( ) { Poster( modifier = modifier.fillMaxWidth(), - navigateToImages = navigateToImages, + navigateToImages = { viewModel.sendEvent(ShowDetailEvent.NavigateToImages(it)) }, title = uiState.showDetail.name, images = uiState.showDetail.images.map { it.originImage } ) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailViewModel.kt index 726acd08..a39f9906 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailViewModel.kt @@ -3,19 +3,19 @@ package com.nexters.boolti.presentation.screen.show import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.nexters.boolti.domain.model.ShowDetail -import com.nexters.boolti.domain.model.TicketingTicket import com.nexters.boolti.domain.repository.AuthRepository import com.nexters.boolti.domain.repository.ShowRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -29,6 +29,9 @@ class ShowDetailViewModel @Inject constructor( private val _uiState = MutableStateFlow(ShowDetailUiState()) val uiState: StateFlow = _uiState.asStateFlow() + private val _events = Channel() + val events: Flow = _events.receiveAsFlow() + val loggedIn = authRepository.loggedIn.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), @@ -39,6 +42,12 @@ class ShowDetailViewModel @Inject constructor( fetchShowDetail() } + fun sendEvent(event: ShowDetailEvent) { + viewModelScope.launch { + _events.send(event) + } + } + private fun fetchShowDetail() { viewModelScope.launch { showRepository.searchById(id = showId) @@ -51,4 +60,8 @@ class ShowDetailViewModel @Inject constructor( } } } + + fun preventEvents() { + _events.cancel() + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt index fd177ad5..7aac527d 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt @@ -269,4 +269,4 @@ private fun Banner( ) Icon(painter = painterResource(id = R.drawable.ic_arrow_right), contentDescription = null) } -} \ No newline at end of file +} From a9eba3f663af68a0547853a518a5a3f56693a189 Mon Sep 17 00:00:00 2001 From: algosketch Date: Wed, 13 Mar 2024 03:06:24 +0900 Subject: [PATCH 059/129] =?UTF-8?q?refactor=20:=20{}=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20=EC=BD=94=EB=93=9C=20=ED=95=9C=20=EC=A4=84?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=94=EA=BE=B8=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/show/ShowDetailViewModel.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailViewModel.kt index a39f9906..cd75e8b8 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailViewModel.kt @@ -42,11 +42,7 @@ class ShowDetailViewModel @Inject constructor( fetchShowDetail() } - fun sendEvent(event: ShowDetailEvent) { - viewModelScope.launch { - _events.send(event) - } - } + fun sendEvent(event: ShowDetailEvent) = viewModelScope.launch { _events.send(event) } private fun fetchShowDetail() { viewModelScope.launch { From f3011fb1e8eec142e151ae423849778c217ee4b7 Mon Sep 17 00:00:00 2001 From: algosketch Date: Mon, 18 Mar 2024 11:45:20 +0900 Subject: [PATCH 060/129] =?UTF-8?q?fix=20:=20=EB=B9=88=EA=B3=B5=EA=B0=84?= =?UTF-8?q?=20=EC=83=9D=EA=B8=B0=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/nexters/boolti/presentation/screen/show/ShowScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt index fd177ad5..eed10227 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt @@ -163,6 +163,7 @@ fun ShowAppBar( Column( modifier = modifier .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) .padding(horizontal = marginHorizontal) ) { Spacer(modifier = Modifier.height(20.dp)) From b846f056fa101cab472e0813466594de209782ba Mon Sep 17 00:00:00 2001 From: algosketch Date: Mon, 18 Mar 2024 12:38:40 +0900 Subject: [PATCH 061/129] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=97=85=EC=9E=90?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/component/BtAppBar.kt | 7 +- .../screen/business/BusinessScreen.kt | 129 ++++++++++++++++++ presentation/src/main/res/values/strings.xml | 18 +++ 3 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt index a6241e08..abb03680 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt @@ -39,11 +39,12 @@ fun BtAppBar( modifier = Modifier.size(width = 48.dp, height = 44.dp), onClick = onBackPressed ) { Icon( + modifier = modifier + .padding(start = marginHorizontal) + .size(width = 24.dp, height = 24.dp), + tint = Grey10, painter = painterResource(navIconRes), contentDescription = stringResource(id = R.string.description_navigate_back), - modifier - .padding(start = marginHorizontal) - .size(width = 24.dp, height = 24.dp) ) } Text( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt new file mode 100644 index 00000000..b742d892 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt @@ -0,0 +1,129 @@ +package com.nexters.boolti.presentation.screen.business + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +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.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BtAppBar +import com.nexters.boolti.presentation.theme.Grey30 +import com.nexters.boolti.presentation.theme.Grey50 +import com.nexters.boolti.presentation.theme.Grey85 +import com.nexters.boolti.presentation.theme.marginHorizontal + +@Composable +fun BusinessScreen( + onBackPressed: () -> Unit, +) { + Scaffold( + contentColor = MaterialTheme.colorScheme.background, + topBar = { + BtAppBar( + title = stringResource(id = R.string.business_title), + onBackPressed = onBackPressed, + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .padding(horizontal = marginHorizontal) + .verticalScroll(rememberScrollState()) + ) { + Text( + modifier = Modifier.padding(bottom = 20.dp), + text = stringResource(id = R.string.business_name), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + + val information = stringArrayResource(id = R.array.business_information) + information.forEachIndexed { index, text -> + val modifier = if (index == 0) Modifier else Modifier.padding(top = 4.dp) + + Text( + modifier = modifier, + text = text, + style = MaterialTheme.typography.bodyLarge, + color = Grey30, + ) + } + + BusinessMenu( + modifier = Modifier.padding(top = 48.dp, bottom = 12.dp), + title = stringResource(id = R.string.business_service_terms), + url = "https://www.notion.so/boolti/b4c5beac61c2480886da75a1f3afb982" + ) + BusinessMenu( + modifier = Modifier.padding(bottom = 12.dp), + title = stringResource(id = R.string.business_privacy_policy), + url = "https://www.notion.so/boolti/5f73661efdcd4507a1e5b6827aa0da70" + ) + BusinessMenu( + title = stringResource(id = R.string.business_refund_policy), + url = "https://www.notion.so/boolti/d2a89e2c19824c60bb1e928370d16989" + ) + } + } +} + +@Composable +private fun BusinessMenu( + title: String, + url: String, + modifier: Modifier = Modifier, +) { + val uriHandler = LocalUriHandler.current + + Row( + modifier = modifier + .fillMaxWidth() + .height(48.dp) + .clip(RoundedCornerShape(4.dp)) + .background(Grey85) + .clickable { uriHandler.openUri(url) } + .padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = Grey30, + ) + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(id = R.drawable.ic_arrow_right), + tint = Grey50, + contentDescription = null, + ) + } +} + +@Preview(backgroundColor = 0xFF090A0B) +@Composable +private fun BusinessScreenPreview() { + BusinessScreen(onBackPressed = {}) +} \ 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 6e011c7d..beaff5a3 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -236,4 +236,22 @@ fcm 불티 알림 티켓 발권 알림을 받습니다. + + + 사업자 정보 + 스튜디오 불티 사업자 정보 + ⓒ Boolti. All Rights Reserved + 스튜디오 불티 + 서비스 이용약관 + 개인정보 처리방침 + 환불 정책 + + 대표 : 김혜선 + 사업자 등록번호 : 202–43–63442 + 주소 : 경기도 남양주시 화도읍 묵현로 25번길 12–5 + 호스팅 서비스 : 스튜디오 불티 + 통신판매업 신고번호 : 2024-화도수동-0518 + 문의전화 : 0507–1363–5690 + studio.boolti@gmail.com + \ No newline at end of file From 70723961f1b900660ddaa521937c172ef4b9b71c Mon Sep 17 00:00:00 2001 From: algosketch Date: Mon, 18 Mar 2024 16:57:06 +0900 Subject: [PATCH 062/129] =?UTF-8?q?feat=20:=20=ED=99=88,=20=EC=98=88?= =?UTF-8?q?=EB=A7=A4=ED=95=98=EA=B8=B0=20flow=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=97=85=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/BusinessInformation.kt | 54 +++++++++++++++++++ .../presentation/screen/show/ShowScreen.kt | 11 ++++ .../screen/ticketing/TicketingScreen.kt | 4 ++ presentation/src/main/res/values/strings.xml | 1 + 4 files changed, 70 insertions(+) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/component/BusinessInformation.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/BusinessInformation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/BusinessInformation.kt new file mode 100644 index 00000000..8738488e --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/BusinessInformation.kt @@ -0,0 +1,54 @@ +package com.nexters.boolti.presentation.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.theme.Grey70 + +@Composable +fun BusinessInformation( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .padding(vertical = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = Modifier.padding(bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.padding(start = 8.dp), + text = stringResource(id = R.string.business_information), + color = Grey70, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + ) + Icon( + modifier = Modifier.size(18.dp), + painter = painterResource(id = R.drawable.ic_arrow_right), + tint = Grey70, + contentDescription = null, + ) + } + Text( + text = stringResource(id = R.string.business_copyright), + color = Grey70, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt index 5d38afea..36290c7a 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.RoundedCornerShape @@ -48,11 +49,13 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BusinessInformation import com.nexters.boolti.presentation.component.ShowFeed import com.nexters.boolti.presentation.extension.toPx import com.nexters.boolti.presentation.theme.Grey15 @@ -131,6 +134,14 @@ fun ShowScreen( .clickable { onClickShowItem(uiState.shows[index].id) }, ) } + + item( + span = { GridItemSpan(2) }, + ) { + BusinessInformation( + modifier = Modifier.padding(bottom = 12.dp) + ) + } } ShowAppBar( modifier = Modifier.offset { diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index 4d59a577..fdfd5f51 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -71,6 +71,7 @@ import coil.compose.AsyncImage import com.nexters.boolti.domain.model.InviteCodeStatus import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTTextField +import com.nexters.boolti.presentation.component.BusinessInformation import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.dayOfWeekString @@ -186,6 +187,9 @@ fun TicketingScreen( ) // 초청 코드 if (!uiState.isInviteTicket) PaymentSection(scope, snackbarHostState) // 결제 수단 if (!uiState.isInviteTicket) RefundPolicySection(uiState.refundPolicy) // 취소/환불 규정 + BusinessInformation( + modifier = Modifier.fillMaxWidth() + ) Spacer(modifier = Modifier.height(120.dp)) } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index beaff5a3..57ec6fa5 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -239,6 +239,7 @@ 사업자 정보 + 스튜디오 불티는 통신판매중개자로 통신판매의 당사자가 아닙니다. 불티에서 판매되는 상품에 대한 광고, 상품주문, 배송, 환불의 의무와 책임은 각 판매자에게 있습니다. 스튜디오 불티 사업자 정보 ⓒ Boolti. All Rights Reserved 스튜디오 불티 From 91c297272af05c876c1efc149224219b2ae05ddf Mon Sep 17 00:00:00 2001 From: algosketch Date: Mon, 18 Mar 2024 17:17:23 +0900 Subject: [PATCH 063/129] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=97=85=EC=9E=90?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EB=88=84=EB=A5=B4=EB=A9=B4=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/BusinessInformation.kt | 6 +++++- .../nexters/boolti/presentation/screen/Main.kt | 2 ++ .../presentation/screen/MainDestination.kt | 1 + .../screen/business/BusinessNavigation.kt | 17 +++++++++++++++++ .../presentation/screen/home/HomeNavigation.kt | 1 + .../presentation/screen/home/HomeScreen.kt | 2 ++ .../presentation/screen/show/ShowScreen.kt | 4 +++- .../screen/ticketing/TicketingNavigation.kt | 3 ++- .../screen/ticketing/TicketingScreen.kt | 9 +++++++-- 9 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessNavigation.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/BusinessInformation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/BusinessInformation.kt index 8738488e..88dd4169 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/BusinessInformation.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/BusinessInformation.kt @@ -1,5 +1,6 @@ package com.nexters.boolti.presentation.component +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -19,6 +20,7 @@ import com.nexters.boolti.presentation.theme.Grey70 @Composable fun BusinessInformation( + onClick: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -27,7 +29,9 @@ fun BusinessInformation( horizontalAlignment = Alignment.CenterHorizontally, ) { Row( - modifier = Modifier.padding(bottom = 2.dp), + modifier = Modifier + .padding(bottom = 2.dp) + .clickable(onClick = onClick), verticalAlignment = Alignment.CenterVertically, ) { Text( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index e48651ed..c8f571ef 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -23,6 +23,7 @@ import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.navigateToHome import com.nexters.boolti.presentation.screen.MainDestination.Home import com.nexters.boolti.presentation.screen.MainDestination.ShowDetail +import com.nexters.boolti.presentation.screen.business.BusinessScreen import com.nexters.boolti.presentation.screen.home.HomeScreen import com.nexters.boolti.presentation.screen.login.LoginScreen import com.nexters.boolti.presentation.screen.payment.PaymentScreen @@ -157,6 +158,7 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: }, navigateToHome = navController::navigateToHome ) + BusinessScreen(popBackStack = navController::popBackStack) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt index 705bcd17..26a8ae24 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt @@ -49,6 +49,7 @@ sealed class MainDestination(val route: String) { data object SignOut : MainDestination(route = "signout") data object Login : MainDestination(route = "login") + data object Business : MainDestination(route = "business") } /** diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessNavigation.kt new file mode 100644 index 00000000..8d8d6e9e --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessNavigation.kt @@ -0,0 +1,17 @@ +package com.nexters.boolti.presentation.screen.business + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.nexters.boolti.presentation.screen.MainDestination + +fun NavGraphBuilder.BusinessScreen( + popBackStack: () -> Unit, +) { + composable( + route = MainDestination.Business.route, + ) { + BusinessScreen( + onBackPressed = popBackStack + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt index 30ece8dd..186dfce7 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt @@ -24,6 +24,7 @@ fun NavGraphBuilder.HomeScreen( onClickQrScan = { navigateTo(MainDestination.HostedShows.route) }, onClickSignout = { navigateTo(MainDestination.SignOut.route) }, navigateToReservations = { navigateTo(MainDestination.Reservations.route) }, + navigateToBusiness = { navigateTo(MainDestination.Business.route) }, requireLogin = { navigateTo(MainDestination.Login.route) } ) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt index e0fbdae1..16f63271 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt @@ -48,6 +48,7 @@ fun HomeScreen( onClickQrScan: () -> Unit, onClickSignout: () -> Unit, navigateToReservations: () -> Unit, + navigateToBusiness: () -> Unit, requireLogin: () -> Unit, modifier: Modifier, viewModel: HomeViewModel = hiltViewModel(), @@ -84,6 +85,7 @@ fun HomeScreen( modifier = modifier.padding(innerPadding), onClickShowItem = onClickShowItem, navigateToReservations = navigateToReservations, + navigateToBusiness = navigateToBusiness, ) } composable( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt index 36290c7a..5660e6ce 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowScreen.kt @@ -68,6 +68,7 @@ import com.nexters.boolti.presentation.theme.point4 @Composable fun ShowScreen( navigateToReservations: () -> Unit, + navigateToBusiness: () -> Unit, onClickShowItem: (showId: String) -> Unit, modifier: Modifier = Modifier, viewModel: ShowViewModel = hiltViewModel() @@ -139,7 +140,8 @@ fun ShowScreen( span = { GridItemSpan(2) }, ) { BusinessInformation( - modifier = Modifier.padding(bottom = 12.dp) + modifier = Modifier.padding(bottom = 12.dp), + onClick = navigateToBusiness ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingNavigation.kt index 6920d12e..6acf1033 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingNavigation.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingNavigation.kt @@ -23,7 +23,8 @@ fun NavGraphBuilder.TicketingScreen( onBackClicked = popBackStack, onReserved = { reservationId, showId -> navigateTo("${MainDestination.Payment.route}/$reservationId?showId=$showId") - } + }, + navigateToBusiness = { navigateTo(MainDestination.Business.route) }, ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index fdfd5f51..ade9f52f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -104,6 +104,7 @@ fun TicketingScreen( viewModel: TicketingViewModel = hiltViewModel(), onBackClicked: () -> Unit = {}, onReserved: (reservationId: String, showId: String) -> Unit, + navigateToBusiness: () -> Unit, ) { val scrollState = rememberScrollState() val snackbarHostState = remember { SnackbarHostState() } @@ -188,7 +189,8 @@ fun TicketingScreen( if (!uiState.isInviteTicket) PaymentSection(scope, snackbarHostState) // 결제 수단 if (!uiState.isInviteTicket) RefundPolicySection(uiState.refundPolicy) // 취소/환불 규정 BusinessInformation( - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + onClick = navigateToBusiness ) Spacer(modifier = Modifier.height(120.dp)) } @@ -668,7 +670,10 @@ private fun SectionTicketInfo(label: String, value: String, marginTop: Dp = 16.d private fun TicketingDetailScreenPreview() { BooltiTheme { Surface { - TicketingScreen { _, _ -> } + TicketingScreen( + onReserved = { _, _ -> }, + navigateToBusiness = {} + ) } } } From c428cf2ba08cbcba9da1255e9fdacbe1450924c2 Mon Sep 17 00:00:00 2001 From: algosketch Date: Thu, 21 Mar 2024 04:04:03 +0900 Subject: [PATCH 064/129] =?UTF-8?q?feat=20:=20ShowDetail=20=EB=94=A5=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 14 +++++++++++++- gradle/libs.versions.toml | 3 ++- .../com/nexters/boolti/presentation/screen/Main.kt | 8 ++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6607f1c9..fc58a45e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,7 +26,19 @@ + android:theme="@style/Theme.Boolti" > + + + + + + + + + + + + Date: Thu, 21 Mar 2024 04:23:30 +0900 Subject: [PATCH 065/129] =?UTF-8?q?feat=20:=20=EA=B3=B5=EC=9C=A0=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EB=8B=A4=EC=9D=B4=EB=82=B4?= =?UTF-8?q?=EB=AF=B9=20=EB=A7=81=ED=81=AC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/show/ShowDetailScreen.kt | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index 25e31712..22898436 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -1,6 +1,7 @@ package com.nexters.boolti.presentation.screen.show import android.content.Intent +import android.net.Uri import android.os.Build import androidx.activity.compose.BackHandler import androidx.compose.foundation.background @@ -54,6 +55,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.firebase.Firebase +import com.google.firebase.dynamiclinks.androidParameters +import com.google.firebase.dynamiclinks.dynamicLink +import com.google.firebase.dynamiclinks.dynamicLinks +import com.google.firebase.dynamiclinks.shortLinkAsync import com.nexters.boolti.domain.model.ShowDetail import com.nexters.boolti.domain.model.ShowState import com.nexters.boolti.presentation.R @@ -122,6 +128,7 @@ fun ShowDetailScreen( modifier = modifier, topBar = { ShowDetailAppBar( + showId = uiState.showDetail.id, onBack = { viewModel.sendEvent(ShowDetailEvent.PopBackStack) }, onClickHome = onClickHome, navigateToReport = navigateToReport, @@ -212,6 +219,7 @@ fun ShowDetailScreen( @Composable private fun ShowDetailAppBar( + showId: String, onBack: () -> Unit, onClickHome: () -> Unit, navigateToReport: () -> Unit, @@ -257,17 +265,29 @@ private fun ShowDetailAppBar( .padding(end = 10.dp) .size(44.dp), onClick = { - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - putExtra( - Intent.EXTRA_TEXT, - "https://play.google.com/store/apps/details?id=${context.applicationContext.packageName}" - ) - type = "text/plain" - } + Firebase.dynamicLinks.shortLinkAsync { + link = Uri.parse("https://boolti.in/show?showId=$showId") + domainUriPrefix = "https://boolti.page.link" - val shareIntent = Intent.createChooser(sendIntent, null) - context.startActivity(shareIntent) + androidParameters { } + // iosParameters("com.example.ios") { } TODO : iOS 패키지 네임 넣기 + }.addOnSuccessListener { + it.shortLink?.let { link -> + println(link) + + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra( + Intent.EXTRA_TEXT, + link.toString() + ) + type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, null) + context.startActivity(shareIntent) + } + } }, ) { Icon( From 393078849c4f43b55feff83498b09c407e9a6be5 Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 23 Mar 2024 21:38:02 +0900 Subject: [PATCH 066/129] =?UTF-8?q?refactor=20:=20=EA=B0=81=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8,=20=EB=B3=84=EB=8F=84=EC=9D=98=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/refund/BankSelection.kt | 140 +++++ .../presentation/screen/refund/ReasonPage.kt | 70 +++ .../screen/refund/RefundInfoPage.kt | 341 +++++++++++++ .../screen/refund/RefundScreen.kt | 479 ------------------ presentation/src/main/res/values/strings.xml | 1 + 5 files changed, 552 insertions(+), 479 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/BankSelection.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/ReasonPage.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/BankSelection.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/BankSelection.kt new file mode 100644 index 00000000..14c63134 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/BankSelection.kt @@ -0,0 +1,140 @@ +package com.nexters.boolti.presentation.screen.refund + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.MainButton +import com.nexters.boolti.presentation.theme.Grey10 +import com.nexters.boolti.presentation.theme.Grey80 +import com.nexters.boolti.presentation.theme.Grey85 +import com.nexters.boolti.presentation.theme.marginHorizontal + +@Composable +fun BankSelection( + onDismiss: () -> Unit, + selectedBank: BankInfo?, + onClick: (bankInfo: BankInfo) -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.padding(bottom = 48.dp), + contentAlignment = Alignment.BottomCenter, + ) { + Column( + modifier = Modifier + .padding(horizontal = marginHorizontal) + .padding(bottom = 48.dp), + ) { + Text( + modifier = Modifier.padding(bottom = 12.dp), + text = stringResource(id = R.string.refund_bank_selection), + style = MaterialTheme.typography.titleLarge, + ) + LazyVerticalGrid( + contentPadding = PaddingValues(vertical = 12.dp), + columns = GridCells.Adaptive(minSize = 100.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + BankInfo.entries.forEach { bankInfo -> + item { + BackItem( + bankInfo = bankInfo, + onClick = onClick, + selected = if (selectedBank == null) null else selectedBank == bankInfo, + ) + } + } + } + } + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .height(16.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + Grey85, + ) + ) + ) + ) + MainButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = marginHorizontal), + label = stringResource(id = R.string.refund_select_bank), + onClick = onDismiss, + enabled = selectedBank != null, + ) + } + } +} + +@Composable +fun BackItem( + onClick: (bankInfo: BankInfo) -> Unit, + bankInfo: BankInfo, + modifier: Modifier = Modifier, + selected: Boolean? = null, +) { + Box( + modifier = modifier + .height(74.dp) + .border( + shape = RoundedCornerShape(4.dp), + color = if (selected == true) Grey10 else Color.Transparent, + width = 1.dp, + ) + .clip(RoundedCornerShape(4.dp)) + .background(Grey80) + .clickable { + onClick(bankInfo) + }, + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier.alpha(alpha = if (selected == false) 0.4f else 1.0f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = modifier.size(32.dp), + painter = painterResource(bankInfo.icon), + contentDescription = null, + ) + Text( + modifier = Modifier.padding(top = 4.dp), + text = bankInfo.bankName, + style = MaterialTheme.typography.bodySmall, + ) + } + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/ReasonPage.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/ReasonPage.kt new file mode 100644 index 00000000..d0f0a866 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/ReasonPage.kt @@ -0,0 +1,70 @@ +package com.nexters.boolti.presentation.screen.refund + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +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.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BTTextField +import com.nexters.boolti.presentation.component.MainButton +import com.nexters.boolti.presentation.theme.marginHorizontal +import com.nexters.boolti.presentation.theme.point4 + +@Composable +fun ReasonPage( + reason: String, + onReasonChanged: (String) -> Unit, + onNextClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val keyboardController = LocalSoftwareKeyboardController.current + val interactionSource = remember { MutableInteractionSource() } + + Column( + modifier = modifier.clickable( + interactionSource = interactionSource, + indication = null + ) { + keyboardController?.hide() + } + ) { + Text( + modifier = Modifier + .padding(top = 20.dp) + .padding(horizontal = marginHorizontal), + text = stringResource(id = R.string.refund_reason_label), + style = point4, + ) + BTTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = marginHorizontal) + .height(160.dp) + .padding(top = 20.dp), + text = reason, + onValueChanged = onReasonChanged, + placeholder = stringResource(id = R.string.refund_reason_hint), + ) + + Spacer(modifier = Modifier.weight(1.0f)) + MainButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = marginHorizontal) + .padding(bottom = 8.dp), + onClick = onNextClick, + enabled = reason.isNotBlank(), + label = stringResource(id = R.string.next) + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt new file mode 100644 index 00000000..31299181 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt @@ -0,0 +1,341 @@ +package com.nexters.boolti.presentation.screen.refund + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +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.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.nexters.boolti.domain.model.ReservationDetail +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BTTextField +import com.nexters.boolti.presentation.component.MainButton +import com.nexters.boolti.presentation.extension.filterToPhoneNumber +import com.nexters.boolti.presentation.theme.Error +import com.nexters.boolti.presentation.theme.Grey10 +import com.nexters.boolti.presentation.theme.Grey15 +import com.nexters.boolti.presentation.theme.Grey30 +import com.nexters.boolti.presentation.theme.Grey50 +import com.nexters.boolti.presentation.theme.Grey70 +import com.nexters.boolti.presentation.theme.Grey80 +import com.nexters.boolti.presentation.theme.Grey85 +import com.nexters.boolti.presentation.theme.marginHorizontal +import com.nexters.boolti.presentation.theme.point2 +import com.nexters.boolti.presentation.util.PhoneNumberVisualTransformation + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RefundInfoPage( + uiState: RefundUiState, + reservation: ReservationDetail, + onRequest: () -> Unit, + onNameChanged: (String) -> Unit, + onContactNumberChanged: (String) -> Unit, + onBankInfoChanged: (BankInfo) -> Unit, + onAccountNumberChanged: (String) -> Unit, + modifier: Modifier = Modifier, +) { + var isSheetOpen by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + var showAccountError by remember { mutableStateOf(false) } + + Column( + modifier = modifier.verticalScroll(rememberScrollState()), + ) { + Header( + reservation = reservation + ) + Section( + title = stringResource(id = R.string.refund_account_holder_info) + ) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.width(56.dp), + text = stringResource(id = R.string.name_label), + style = MaterialTheme.typography.bodySmall.copy(color = Grey30), + ) + BTTextField( + modifier = Modifier + .weight(1.0f), + text = uiState.name, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + placeholder = stringResource(id = R.string.refund_account_name_hint), + onValueChanged = onNameChanged + ) + } + + Row( + modifier = Modifier.padding(top = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.width(56.dp), + text = stringResource(id = R.string.contact_label), + style = MaterialTheme.typography.bodySmall.copy(color = Grey30), + ) + BTTextField( + modifier = Modifier + .weight(1.0f), + text = uiState.contact.filterToPhoneNumber(), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Phone, + imeAction = ImeAction.Next + ), + placeholder = stringResource(id = R.string.ticketing_contact_placeholder), + onValueChanged = onContactNumberChanged, + visualTransformation = PhoneNumberVisualTransformation('-'), + ) + } + } + } + Section( + modifier = Modifier.padding(top = 12.dp), + title = stringResource(id = R.string.refund_account_info), + expandable = false, + ) { + Column { + Button( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + onClick = { isSheetOpen = true }, + shape = RoundedCornerShape(4.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceTint, + ), + contentPadding = PaddingValues(horizontal = 12.dp), + ) { + val bankSelection = stringResource(id = R.string.refund_bank_selection) + Text( + text = if (uiState.bankInfo == null) bankSelection else uiState.bankInfo.bankName, + style = MaterialTheme.typography.bodyLarge.copy(color = Grey15), + ) + Spacer(modifier = Modifier.weight(1.0f)) + Icon( + painter = painterResource(id = R.drawable.ic_arrow_down), + contentDescription = stringResource(id = R.string.refund_bank_selection), + tint = Grey50, + ) + } + BTTextField( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + .onFocusChanged { focusState -> + showAccountError = uiState.accountNumber.isNotEmpty() && + !uiState.isValidAccountNumber && + !focusState.isFocused + }, + text = uiState.accountNumber, + isError = showAccountError, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Decimal, + imeAction = ImeAction.Done + ), + placeholder = stringResource(id = R.string.refund_account_number_hint), + onValueChanged = onAccountNumberChanged, + ) + if (showAccountError) { + Text( + modifier = Modifier.padding(top = 12.dp), + text = stringResource(id = R.string.validation_account), + style = MaterialTheme.typography.bodySmall.copy(color = Error), + ) + } + } + } + + Spacer(modifier = Modifier.weight(1.0f)) + MainButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = marginHorizontal) + .padding(bottom = 8.dp), + onClick = onRequest, + enabled = uiState.isAbleToRequest, + label = stringResource(id = R.string.refund_button) + ) + } + + if (isSheetOpen) { + ModalBottomSheet( + modifier = Modifier.heightIn(max = 646.dp), + sheetState = sheetState, + onDismissRequest = { + isSheetOpen = false + }, + dragHandle = { + Box( + modifier = Modifier + .padding(top = 12.dp, bottom = 20.dp) + .size(45.dp, 4.dp) + .background(Grey70) + .clip(RoundedCornerShape(100.dp)), + ) + }, + containerColor = Grey85, + ) { + BankSelection( + selectedBank = uiState.bankInfo, + onClick = onBankInfoChanged, + onDismiss = { isSheetOpen = false }) + } + } +} + + +@Composable +private fun Header( + reservation: ReservationDetail, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.padding(horizontal = marginHorizontal, vertical = 20.dp), + ) { + AsyncImage( + modifier = Modifier + .width(70.dp) + .height(98.dp) + .border(color = Grey80, width = 1.dp, shape = RoundedCornerShape(4.dp)) + .clip(shape = RoundedCornerShape(4.dp)), + model = reservation.showImage, + contentDescription = stringResource(id = R.string.description_poster), + contentScale = ContentScale.Crop, + ) + Column( + modifier = Modifier.padding(start = 16.dp), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = reservation.showName, + style = point2, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + modifier = Modifier.padding(top = 4.dp), + text = stringResource( + id = R.string.reservation_ticket_info_format, + reservation.ticketName, + reservation.ticketCount + ), + style = MaterialTheme.typography.bodySmall.copy(color = Grey30), + ) + } + } +} + +@Composable +private fun Section( + title: String, + modifier: Modifier = Modifier, + defaultExpanded: Boolean = true, + expandable: Boolean = true, + content: @Composable () -> Unit, +) { + var expanded by remember { + mutableStateOf(defaultExpanded) + } + val rotation by animateFloatAsState( + targetValue = if (expanded) 0f else 180f, label = "rotationX" + ) + + Column( + modifier = modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.surface), + ) { + val touchAreaModifier = if (expandable) { + Modifier + .fillMaxWidth() + .clickable { expanded = !expanded } + } else { + Modifier.fillMaxWidth() + } + Row( + modifier = touchAreaModifier + .padding(horizontal = marginHorizontal) + .padding(vertical = 20.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy(color = Grey10), + ) + if (expandable) { + Icon( + modifier = Modifier.graphicsLayer { + rotationX = rotation + }, + painter = painterResource(id = R.drawable.ic_expand_24), + contentDescription = stringResource(R.string.description_expand), + tint = Grey50, + ) + } + } + AnimatedVisibility( + modifier = Modifier + .padding(horizontal = marginHorizontal) + .padding(bottom = 20.dp), + visible = expanded, + ) { + content() + } + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt index e4039881..b24142dc 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt @@ -1,43 +1,19 @@ package com.nexters.boolti.presentation.screen.refund -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -47,48 +23,18 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import coil.compose.AsyncImage -import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog -import com.nexters.boolti.presentation.component.BTTextField import com.nexters.boolti.presentation.component.BtAppBar -import com.nexters.boolti.presentation.component.MainButton -import com.nexters.boolti.presentation.extension.filterToPhoneNumber import com.nexters.boolti.presentation.screen.LocalSnackbarController -import com.nexters.boolti.presentation.screen.MainDestination -import com.nexters.boolti.presentation.screen.reservationId -import com.nexters.boolti.presentation.theme.Error -import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey15 import com.nexters.boolti.presentation.theme.Grey30 -import com.nexters.boolti.presentation.theme.Grey50 -import com.nexters.boolti.presentation.theme.Grey70 import com.nexters.boolti.presentation.theme.Grey80 -import com.nexters.boolti.presentation.theme.Grey85 -import com.nexters.boolti.presentation.theme.marginHorizontal -import com.nexters.boolti.presentation.theme.point2 -import com.nexters.boolti.presentation.theme.point4 -import com.nexters.boolti.presentation.util.PhoneNumberVisualTransformation import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -210,431 +156,6 @@ fun RefundScreen( } } -@Composable -fun ReasonPage( - reason: String, - onReasonChanged: (String) -> Unit, - onNextClick: () -> Unit, - modifier: Modifier = Modifier, -) { - val keyboardController = LocalSoftwareKeyboardController.current - val interactionSource = remember { MutableInteractionSource() } - - Column( - modifier = modifier.clickable( - interactionSource = interactionSource, - indication = null - ) { - keyboardController?.hide() - } - ) { - Text( - modifier = Modifier - .padding(top = 20.dp) - .padding(horizontal = marginHorizontal), - text = stringResource(id = R.string.refund_reason_label), - style = point4, - ) - BTTextField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = marginHorizontal) - .height(160.dp) - .padding(top = 20.dp), - text = reason, - onValueChanged = onReasonChanged, - placeholder = stringResource(id = R.string.refund_reason_hint), - ) - - Spacer(modifier = Modifier.weight(1.0f)) - MainButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = marginHorizontal) - .padding(bottom = 8.dp), - onClick = onNextClick, - enabled = reason.isNotBlank(), - label = stringResource(id = R.string.next) - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun RefundInfoPage( - uiState: RefundUiState, - reservation: ReservationDetail, - onRequest: () -> Unit, - onNameChanged: (String) -> Unit, - onContactNumberChanged: (String) -> Unit, - onBankInfoChanged: (BankInfo) -> Unit, - onAccountNumberChanged: (String) -> Unit, - modifier: Modifier = Modifier, -) { - var isSheetOpen by remember { mutableStateOf(false) } - val sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ) - var showAccountError by remember { mutableStateOf(false) } - - Column( - modifier = modifier.verticalScroll(rememberScrollState()), - ) { - Header( - reservation = reservation - ) - Section( - title = stringResource(id = R.string.refund_account_holder_info) - ) { - Column { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.width(56.dp), - text = stringResource(id = R.string.name_label), - style = MaterialTheme.typography.bodySmall.copy(color = Grey30), - ) - BTTextField( - modifier = Modifier - .weight(1.0f), - text = uiState.name, - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next - ), - placeholder = stringResource(id = R.string.refund_account_name_hint), - onValueChanged = onNameChanged - ) - } - - Row( - modifier = Modifier.padding(top = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.width(56.dp), - text = stringResource(id = R.string.contact_label), - style = MaterialTheme.typography.bodySmall.copy(color = Grey30), - ) - BTTextField( - modifier = Modifier - .weight(1.0f), - text = uiState.contact.filterToPhoneNumber(), - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Phone, - imeAction = ImeAction.Next - ), - placeholder = stringResource(id = R.string.ticketing_contact_placeholder), - onValueChanged = onContactNumberChanged, - visualTransformation = PhoneNumberVisualTransformation('-'), - ) - } - } - } - Section( - modifier = Modifier.padding(top = 12.dp), - title = stringResource(id = R.string.refund_account_info), - expandable = false, - ) { - Column { - Button( - modifier = Modifier - .fillMaxWidth() - .height(48.dp), - onClick = { isSheetOpen = true }, - shape = RoundedCornerShape(4.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.surfaceTint, - ), - contentPadding = PaddingValues(horizontal = 12.dp), - ) { - val bankSelection = stringResource(id = R.string.refund_bank_selection) - Text( - text = if (uiState.bankInfo == null) bankSelection else uiState.bankInfo.bankName, - style = MaterialTheme.typography.bodyLarge.copy(color = Grey15), - ) - Spacer(modifier = Modifier.weight(1.0f)) - Icon( - painter = painterResource(id = R.drawable.ic_arrow_down), - contentDescription = stringResource(id = R.string.refund_bank_selection), - tint = Grey50, - ) - } - BTTextField( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - .onFocusChanged { focusState -> - showAccountError = uiState.accountNumber.isNotEmpty() && - !uiState.isValidAccountNumber && - !focusState.isFocused - }, - text = uiState.accountNumber, - isError = showAccountError, - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Decimal, - imeAction = ImeAction.Done - ), - placeholder = stringResource(id = R.string.refund_account_number_hint), - onValueChanged = onAccountNumberChanged, - ) - if (showAccountError) { - Text( - modifier = Modifier.padding(top = 12.dp), - text = stringResource(id = R.string.validation_account), - style = MaterialTheme.typography.bodySmall.copy(color = Error), - ) - } - } - } - - Spacer(modifier = Modifier.weight(1.0f)) - MainButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = marginHorizontal) - .padding(bottom = 8.dp), - onClick = onRequest, - enabled = uiState.isAbleToRequest, - label = stringResource(id = R.string.refund_button) - ) - } - - if (isSheetOpen) { - ModalBottomSheet( - modifier = Modifier.heightIn(max = 646.dp), - sheetState = sheetState, - onDismissRequest = { - isSheetOpen = false - }, - dragHandle = { - Box( - modifier = Modifier - .padding(top = 12.dp, bottom = 20.dp) - .size(45.dp, 4.dp) - .background(Grey70) - .clip(RoundedCornerShape(100.dp)), - ) - }, - containerColor = Grey85, - ) { - BankSelection( - selectedBank = uiState.bankInfo, - onClick = onBankInfoChanged, - onDismiss = { isSheetOpen = false }) - } - } -} - -@Composable -private fun Header( - reservation: ReservationDetail, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier.padding(horizontal = marginHorizontal, vertical = 20.dp), - ) { - AsyncImage( - modifier = Modifier - .width(70.dp) - .height(98.dp) - .border(color = Grey80, width = 1.dp, shape = RoundedCornerShape(4.dp)) - .clip(shape = RoundedCornerShape(4.dp)), - model = reservation.showImage, - contentDescription = stringResource(id = R.string.description_poster), - contentScale = ContentScale.Crop, - ) - Column( - modifier = Modifier.padding(start = 16.dp), - verticalArrangement = Arrangement.Center, - ) { - Text( - text = reservation.showName, - style = point2, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - Text( - modifier = Modifier.padding(top = 4.dp), - text = stringResource( - id = R.string.reservation_ticket_info_format, - reservation.ticketName, - reservation.ticketCount - ), - style = MaterialTheme.typography.bodySmall.copy(color = Grey30), - ) - } - } -} - -@Composable -private fun Section( - title: String, - modifier: Modifier = Modifier, - defaultExpanded: Boolean = true, - expandable: Boolean = true, - content: @Composable () -> Unit, -) { - var expanded by remember { - mutableStateOf(defaultExpanded) - } - val rotation by animateFloatAsState( - targetValue = if (expanded) 0f else 180f, label = "rotationX" - ) - - Column( - modifier = modifier - .fillMaxWidth() - .background(color = MaterialTheme.colorScheme.surface), - ) { - val touchAreaModifier = if (expandable) { - Modifier - .fillMaxWidth() - .clickable { expanded = !expanded } - } else { - Modifier.fillMaxWidth() - } - Row( - modifier = touchAreaModifier - .padding(horizontal = marginHorizontal) - .padding(vertical = 20.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = title, - style = MaterialTheme.typography.titleLarge.copy(color = Grey10), - ) - if (expandable) { - Icon( - modifier = Modifier.graphicsLayer { - rotationX = rotation - }, - painter = painterResource(id = R.drawable.ic_expand_24), - contentDescription = stringResource(R.string.description_expand), - tint = Grey50, - ) - } - } - AnimatedVisibility( - modifier = Modifier - .padding(horizontal = marginHorizontal) - .padding(bottom = 20.dp), - visible = expanded, - ) { - content() - } - } -} - -@Composable -fun BankSelection( - onDismiss: () -> Unit, - selectedBank: BankInfo?, - onClick: (bankInfo: BankInfo) -> Unit, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier.padding(bottom = 48.dp), - contentAlignment = Alignment.BottomCenter, - ) { - Column( - modifier = Modifier - .padding(horizontal = marginHorizontal) - .padding(bottom = 48.dp), - ) { - Text( - modifier = Modifier.padding(bottom = 12.dp), - text = stringResource(id = R.string.refund_bank_selection), - style = MaterialTheme.typography.titleLarge, - ) - LazyVerticalGrid( - contentPadding = PaddingValues(vertical = 12.dp), - columns = GridCells.Adaptive(minSize = 100.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - BankInfo.entries.forEach { bankInfo -> - item { - BackItem( - bankInfo = bankInfo, - onClick = onClick, - selected = if (selectedBank == null) null else selectedBank == bankInfo, - ) - } - } - } - } - Column { - Box( - modifier = Modifier - .fillMaxWidth() - .height(16.dp) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - Grey85, - ) - ) - ) - ) - MainButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = marginHorizontal), - label = stringResource(id = R.string.refund_select_bank), - onClick = onDismiss, - enabled = selectedBank != null, - ) - } - } -} - -@Composable -fun BackItem( - onClick: (bankInfo: BankInfo) -> Unit, - bankInfo: BankInfo, - modifier: Modifier = Modifier, - selected: Boolean? = null, -) { - Box( - modifier = modifier - .height(74.dp) - .border( - shape = RoundedCornerShape(4.dp), - color = if (selected == true) Grey10 else Color.Transparent, - width = 1.dp, - ) - .clip(RoundedCornerShape(4.dp)) - .background(Grey80) - .clickable { - onClick(bankInfo) - }, - contentAlignment = Alignment.Center, - ) { - Column( - modifier = Modifier.alpha(alpha = if (selected == false) 0.4f else 1.0f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - modifier = modifier.size(32.dp), - painter = painterResource(bankInfo.icon), - contentDescription = null, - ) - Text( - modifier = Modifier.padding(top = 4.dp), - text = bankInfo.bankName, - style = MaterialTheme.typography.bodySmall, - ) - } - } -} - @Composable fun InfoRow( type: String, diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 57ec6fa5..098635d4 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -219,6 +219,7 @@ 선택 완료하기 환불 정보를 확인해 주세요 환불 요청이 완료되었어요 + 환불 정보 신고하기 From bc2814c270e8135c43f304fe103577de5f04ccb8 Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 23 Mar 2024 21:58:21 +0900 Subject: [PATCH 067/129] =?UTF-8?q?feat=20:=20=ED=99=98=EB=B6=88=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/refund/RefundInfoPage.kt | 48 +++++++++++++++++++ presentation/src/main/res/values/strings.xml | 4 +- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt index 31299181..16697bbb 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTTextField @@ -201,6 +202,30 @@ fun RefundInfoPage( } } + Section( + modifier = Modifier.padding(top = 12.dp), + title = stringResource(id = R.string.refund_account_info), + expandable = false, + ) { + Column { + val paymentType = when (reservation.paymentType) { + PaymentType.ACCOUNT_TRANSFER -> stringResource(id = R.string.payment_account_transfer) + PaymentType.CARD -> stringResource(id = R.string.payment_card) + else -> stringResource(id = R.string.reservations_unknown) + } + + NormalRow( + key = stringResource(id = R.string.refund_price), + value = stringResource(id = R.string.unit_won, reservation.totalAmountPrice) + ) + NormalRow( + modifier = Modifier.padding(top = 16.dp), + key = stringResource(id = R.string.refund_method), + value = paymentType + ) + } + } + Spacer(modifier = Modifier.weight(1.0f)) MainButton( modifier = Modifier @@ -339,3 +364,26 @@ private fun Section( } } } + +@Composable +private fun NormalRow( + key: String, + value: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + modifier = Modifier, + text = key, + style = MaterialTheme.typography.bodyLarge.copy(color = Grey30), + ) + Text( + text = value, + style = MaterialTheme.typography.bodyLarge.copy(color = Grey15), + ) + } +} diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 098635d4..56e15fd5 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -220,6 +220,8 @@ 환불 정보를 확인해 주세요 환불 요청이 완료되었어요 환불 정보 + 환불 예정 금액 + 환불 수단 신고하기 @@ -256,4 +258,4 @@ 문의전화 : 0507–1363–5690 studio.boolti@gmail.com
- \ No newline at end of file + From 0ede3cfd73fd3d55a8ea212cd950de8960025e35 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sat, 23 Mar 2024 22:03:20 +0900 Subject: [PATCH 068/129] =?UTF-8?q?Boolti-193=20feat:=20=ED=8B=B0=EC=BC=93?= =?UTF-8?q?=20=EC=84=A0=ED=83=9D=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD,=20=EC=9D=BC?= =?UTF-8?q?=EB=B0=98=20=ED=8B=B0=EC=BC=93=20=EA=B0=9C=EC=88=98=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/HorizontalStepper.kt | 125 +++++++++++++ .../ticketing/ChooseTicketBottomSheet.kt | 165 +++++++++++++----- .../main/res/drawable/ic_stepper_minus.xml | 17 ++ .../src/main/res/drawable/ic_stepper_plus.xml | 24 +++ presentation/src/main/res/values/strings.xml | 5 +- 5 files changed, 294 insertions(+), 42 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/component/HorizontalStepper.kt create mode 100644 presentation/src/main/res/drawable/ic_stepper_minus.xml create mode 100644 presentation/src/main/res/drawable/ic_stepper_plus.xml diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/HorizontalStepper.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/HorizontalStepper.kt new file mode 100644 index 00000000..03af893d --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/HorizontalStepper.kt @@ -0,0 +1,125 @@ +package com.nexters.boolti.presentation.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.theme.BooltiTheme +import com.nexters.boolti.presentation.theme.Grey15 +import com.nexters.boolti.presentation.theme.Grey70 + +@Composable +fun HorizontalStepper( + currentItem: T, + modifier: Modifier = Modifier, + minusEnabled: Boolean = true, + plusEnabled: Boolean = true, + onClickMinus: (current: T) -> Unit, + onClickPlus: (current: T) -> Unit, +) { + Card( + shape = RoundedCornerShape(4.dp), + ) { + Row( + modifier = modifier + .defaultMinSize(minWidth = 100.dp, minHeight = 32.dp) + .background(color = MaterialTheme.colorScheme.secondaryContainer), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Image( + modifier = Modifier.clickable( + enabled = minusEnabled, + role = Role.Button, + onClickLabel = stringResource(R.string.stepper_minus_description), + onClick = { onClickMinus(currentItem) }, + ), + painter = painterResource(id = R.drawable.ic_stepper_minus), + colorFilter = ColorFilter.tint(if (minusEnabled) Grey15 else Grey70), + contentDescription = stringResource(R.string.stepper_minus_description), + ) + Box( + modifier = Modifier + .defaultMinSize(minWidth = 32.dp, minHeight = 32.dp) + .weight(1f), + contentAlignment = Alignment.Center, + ) { + Text( + text = currentItem.toString(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + maxLines = 1, + ) + } + Image( + modifier = Modifier.clickable( + enabled = plusEnabled, + role = Role.Button, + onClickLabel = stringResource(R.string.stepper_plus_description), + onClick = { onClickPlus(currentItem) }, + ), + painter = painterResource(id = R.drawable.ic_stepper_plus), + colorFilter = ColorFilter.tint(if (plusEnabled) Grey15 else Grey70), + contentDescription = stringResource(R.string.stepper_plus_description), + ) + } + } +} + +@Composable +fun HorizontalCountStepper( + modifier: Modifier = Modifier, + minCount: Int = 1, + maxCount: Int = Int.MAX_VALUE, + currentCount: Int = 1, + onClickMinus: (current: Int) -> Unit = {}, + onClickPlus: (current: Int) -> Unit = {}, +) { + HorizontalStepper( + modifier = modifier, + currentItem = currentCount, + minusEnabled = currentCount != minCount, + plusEnabled = currentCount != maxCount, + onClickMinus = onClickMinus, + onClickPlus = onClickPlus, + ) +} + +@Preview +@Composable +private fun HorizontalCountStepperPreview() { + var current by remember { mutableIntStateOf(1) } + BooltiTheme { + HorizontalCountStepper( + modifier = Modifier.width(100.dp), + currentCount = current, + maxCount = 10, + onClickPlus = { current++ }, + onClickMinus = { current-- } + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt index 22a98873..a0b59a03 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt @@ -14,16 +14,16 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.IconButton -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text @@ -31,6 +31,9 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -46,8 +49,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.nexters.boolti.domain.model.SalesTicket import com.nexters.boolti.domain.model.TicketWithQuantity import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.component.Badge +import com.nexters.boolti.presentation.component.HorizontalCountStepper +import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.extension.sliceAtMost import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey15 @@ -65,7 +69,6 @@ fun ChooseTicketBottomSheet( onDismissRequest: () -> Unit, ) { val uiState by viewModel.uiState.collectAsState() - val listState = rememberLazyListState() val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet( @@ -89,12 +92,16 @@ fun ChooseTicketBottomSheet( ) { Column( modifier = Modifier - .padding(bottom = 20.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) + .padding( + bottom = 20.dp + WindowInsets.navigationBars + .asPaddingValues() + .calculateBottomPadding() + ) .heightIn(max = 564.dp) ) { Text( text = stringResource(id = R.string.choose_ticket_bottomsheet_title), - style = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onSurface), + style = MaterialTheme.typography.titleLarge.copy(color = Grey30), modifier = Modifier .padding(top = 20.dp, start = 24.dp, end = 24.dp, bottom = 12.dp) ) @@ -102,14 +109,13 @@ fun ChooseTicketBottomSheet( ChooseTicketBottomSheetContent2( ticket = it, onCloseClicked = viewModel::unSelectTicket, - onTicketingClicked = { - onTicketingClicked(it.ticket, 1) // TODO 추후 개수 선택 기획 들어오면 변경 + onTicketingClicked = { ticket, count -> + onTicketingClicked(ticket.ticket, count) viewModel.unSelectTicket() }, ) } ?: run { ChooseTicketBottomSheetContent1( - listState = listState, tickets = uiState.tickets, onSelectItem = viewModel::selectTicket, ) @@ -121,10 +127,10 @@ fun ChooseTicketBottomSheet( @Composable private fun ChooseTicketBottomSheetContent1( modifier: Modifier = Modifier, - listState: LazyListState, tickets: List, onSelectItem: (TicketWithQuantity) -> Unit, ) { + val listState = rememberLazyListState() LazyColumn( modifier = modifier.nestedScroll(rememberNestedScrollInteropConnection()), state = listState @@ -143,8 +149,10 @@ private fun ChooseTicketBottomSheetContent2( modifier: Modifier = Modifier, ticket: TicketWithQuantity, onCloseClicked: () -> Unit, - onTicketingClicked: (TicketWithQuantity) -> Unit, + onTicketingClicked: (ticket: TicketWithQuantity, count: Int) -> Unit, ) { + var ticketCount by remember { mutableIntStateOf(1) } + Column(modifier) { Row( modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), @@ -161,41 +169,61 @@ private fun ChooseTicketBottomSheetContent2( if (!ticket.ticket.isInviteTicket) { Badge( stringResource(R.string.badge_left_ticket_amount, ticket.quantity), + color = MaterialTheme.colorScheme.onSecondaryContainer, + containerColor = MaterialTheme.colorScheme.surface, modifier = Modifier.padding(start = 8.dp), ) } + Spacer(modifier = Modifier.weight(1F)) + IconButton(onClick = onCloseClicked) { + Icon( + painter = painterResource(R.drawable.ic_close), + contentDescription = stringResource(id = R.string.description_close_button), + tint = Grey50, + ) + } + } + Row( + modifier = Modifier.padding(top = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (ticket.ticket.isInviteTicket) { + Text( + text = stringResource(R.string.ticketing_limit_per_person, 1), + style = MaterialTheme.typography.bodyLarge.copy(color = Grey15), + ) + } else { + HorizontalCountStepper( + modifier = Modifier.width(100.dp), + currentCount = ticketCount, + minCount = 1, + maxCount = ticket.quantity, + onClickMinus = { ticketCount-- }, + onClickPlus = { ticketCount++ }, + ) + } + Spacer(modifier = Modifier.weight(1f)) + Text( + text = stringResource(R.string.format_price, ticket.ticket.price), + style = MaterialTheme.typography.bodyLarge.copy(color = Grey15), + ) } - Text( - text = stringResource(R.string.format_price, ticket.ticket.price), - style = MaterialTheme.typography.bodyLarge.copy( - color = Grey15, - ), - modifier = Modifier.padding(top = 12.dp), - ) - } - Spacer(modifier = Modifier.weight(1F)) - IconButton(onClick = onCloseClicked) { - Icon( - painter = painterResource(R.drawable.ic_close), - contentDescription = stringResource(id = R.string.description_close_button), - tint = Grey50, - ) } } - Divider(color = Grey80, thickness = 1.dp, modifier = Modifier.fillMaxWidth()) + HorizontalDivider(modifier = Modifier.fillMaxWidth(), thickness = 1.dp, color = Grey80) Row( modifier = Modifier.padding(vertical = 16.dp, horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically ) { Text( - text = stringResource(R.string.ticketing_limit_per_person, 1), + text = stringResource(R.string.total_payment_amount_label), style = MaterialTheme.typography.bodyLarge.copy(color = Grey30), ) Spacer(modifier = Modifier.weight(1F)) Text( - text = stringResource(R.string.format_total_price, ticket.ticket.price), + text = stringResource(R.string.format_total_price, ticket.ticket.price * ticketCount), style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary), ) } @@ -206,7 +234,7 @@ private fun ChooseTicketBottomSheetContent2( .fillMaxWidth() .padding(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 24.dp) .height(48.dp), - onClick = { onTicketingClicked(ticket) }, + onClick = { onTicketingClicked(ticket, ticketCount) }, ) } } @@ -217,6 +245,7 @@ private fun TicketingTicketItem( onClick: (TicketWithQuantity) -> Unit, ) { val enabled = ticket.ticket.isInviteTicket || ticket.quantity > 0 + val textColor = if (enabled) MaterialTheme.colorScheme.onPrimary else Grey70 Row( modifier = Modifier @@ -227,13 +256,15 @@ private fun TicketingTicketItem( ) { Text( text = ticket.ticket.ticketName.sliceAtMost(12), - style = MaterialTheme.typography.bodyLarge.copy(color = if (enabled) Grey30 else Grey70), + style = MaterialTheme.typography.bodyLarge.copy(color = textColor), overflow = TextOverflow.Ellipsis, ) if (!ticket.ticket.isInviteTicket && ticket.quantity > 0) { Badge( stringResource(R.string.badge_left_ticket_amount, ticket.quantity), - Modifier.padding(start = 8.dp), + color = MaterialTheme.colorScheme.onSecondaryContainer, + containerColor = MaterialTheme.colorScheme.surface, + modifier = Modifier.padding(start = 8.dp), ) } Text( @@ -245,7 +276,7 @@ private fun TicketingTicketItem( } else { stringResource(R.string.sold_out_label) }, - style = MaterialTheme.typography.bodyLarge.copy(color = if (enabled) Grey15 else Grey70), + style = MaterialTheme.typography.bodyLarge.copy(color = textColor), textAlign = TextAlign.End, maxLines = 1, ) @@ -254,23 +285,75 @@ private fun TicketingTicketItem( @Preview @Composable -fun TicketingTicketItemPreview() { - val ticket = SalesTicket("", "", "상운이쇼상운이쇼상운이쇼상운이쇼", 1000, false) +private fun TicketingTicket1Preview() { + BooltiTheme { + ChooseTicketBottomSheetContent1( + tickets = listOf( + TicketWithQuantity( + ticket = SalesTicket( + id = "legimus", + showId = "repudiandae", + ticketName = "Nadine Faulkner", + price = 4358, + isInviteTicket = false, + ), + quantity = 100, + ), + TicketWithQuantity( + ticket = SalesTicket( + id = "putent", + showId = "qui", + ticketName = "Beth Small", + price = 6979, + isInviteTicket = false, + ), + quantity = 7168, + ), + TicketWithQuantity( + ticket = SalesTicket( + id = "verear", + showId = "non", + ticketName = "Dante Keith", + price = 9397, + isInviteTicket = true, + ), quantity = 4817 + ) + ) + ) {} + } +} +@Preview +@Composable +private fun TicketingTicket2Preview() { BooltiTheme { - TicketingTicketItem( + ChooseTicketBottomSheetContent2( ticket = TicketWithQuantity( - ticket = ticket, + ticket = SalesTicket( + id = "legimus", + showId = "repudiandae", + ticketName = "Nadine Faulkner", + price = 4358, + isInviteTicket = false, + ), quantity = 100, ), - ) {} + onCloseClicked = {}, + ) { _, _ -> } } } @Preview @Composable -fun BadgePreview() { +private fun TicketingTicketItemPreview() { + val ticket = SalesTicket("", "", "상운이쇼상운이쇼상운이쇼상운이쇼", 1000, false) + BooltiTheme { - Badge("3개 남음") + TicketingTicketItem( + ticket = TicketWithQuantity( + ticket = ticket, + quantity = 100, + ), + ) {} } } diff --git a/presentation/src/main/res/drawable/ic_stepper_minus.xml b/presentation/src/main/res/drawable/ic_stepper_minus.xml new file mode 100644 index 00000000..89b12fc9 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_stepper_minus.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/presentation/src/main/res/drawable/ic_stepper_plus.xml b/presentation/src/main/res/drawable/ic_stepper_plus.xml new file mode 100644 index 00000000..dac41b0d --- /dev/null +++ b/presentation/src/main/res/drawable/ic_stepper_plus.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 57ec6fa5..be1bc06c 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -56,6 +56,9 @@ 티켓 초청 티켓 + 감소 + 증가 + 불티나게 팔리는 티켓, 불티 지금 티켓을 예매하고 공연을 즐겨보세요! @@ -98,7 +101,7 @@ 공연 정보 보기 - 티켓 선택 + 옵션 선택 공유하기 티켓 예매 기간 일시 From a46270beb22c024f961b58274ef028557639b7b7 Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 23 Mar 2024 23:03:29 +0900 Subject: [PATCH 069/129] =?UTF-8?q?feat=20:=20BtCheckBox=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/component/BtCheckBox.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/component/BtCheckBox.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtCheckBox.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtCheckBox.kt new file mode 100644 index 00000000..f090db77 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtCheckBox.kt @@ -0,0 +1,39 @@ +package com.nexters.boolti.presentation.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.theme.Grey05 +import com.nexters.boolti.presentation.theme.Grey50 + +@Composable +fun BtCheckBox( + isSelected: Boolean, + modifier: Modifier = Modifier, +) { + if (isSelected) { + Icon( + modifier = modifier + .padding(3.dp) + .background(MaterialTheme.colorScheme.primary, shape = CircleShape), + painter = painterResource(R.drawable.ic_checkbox_selected), + tint = Grey05, + contentDescription = null, + ) + } else { + Icon( + modifier = modifier, + painter = painterResource(R.drawable.ic_checkbox_18), + tint = Grey50, + contentDescription = null, + ) + } +} From 00f54678b7e7a4c99cf82a985a7c471582f59dd3 Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 23 Mar 2024 23:04:19 +0900 Subject: [PATCH 070/129] =?UTF-8?q?feat=20:=20=EC=B7=A8=EC=86=8C/=ED=99=98?= =?UTF-8?q?=EB=B6=88=20=EA=B7=9C=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/refund/RefundInfoPage.kt | 65 +++++++++++++++++-- .../screen/refund/RefundScreen.kt | 3 + .../screen/refund/RefundUiState.kt | 14 ++-- .../screen/refund/RefundViewModel.kt | 18 +++++ presentation/src/main/res/values/strings.xml | 1 + 5 files changed, 92 insertions(+), 9 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt index 16697bbb..8a2da248 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt @@ -17,8 +17,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions @@ -40,8 +38,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource @@ -55,6 +51,7 @@ import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTTextField +import com.nexters.boolti.presentation.component.BtCheckBox import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.extension.filterToPhoneNumber import com.nexters.boolti.presentation.theme.Error @@ -73,12 +70,14 @@ import com.nexters.boolti.presentation.util.PhoneNumberVisualTransformation @Composable fun RefundInfoPage( uiState: RefundUiState, + refundPolicy: List, reservation: ReservationDetail, onRequest: () -> Unit, onNameChanged: (String) -> Unit, onContactNumberChanged: (String) -> Unit, onBankInfoChanged: (BankInfo) -> Unit, onAccountNumberChanged: (String) -> Unit, + onRefundPolicyChecked: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { var isSheetOpen by remember { mutableStateOf(false) } @@ -226,12 +225,48 @@ fun RefundInfoPage( } } + Section( + modifier = Modifier.padding(vertical = 12.dp), + title = stringResource(id = R.string.refund_policy_label), + expandable = false, + ) { + Column { + refundPolicy.forEach { + PolicyLine(modifier = Modifier.padding(bottom = 4.dp), text = it) + } + Row( + modifier = Modifier + .padding(vertical = 20.dp) + .height(48.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(4.dp)) + .clickable { onRefundPolicyChecked(!uiState.refundPolicyChecked) } + .background(Grey85) + .border(width = 1.dp, color = Grey80, shape = RoundedCornerShape(4.dp)) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + BtCheckBox( + modifier = Modifier.size(24.dp), + isSelected = uiState.refundPolicyChecked, + ) + Text( + modifier = Modifier.padding(start = 8.dp), + text = stringResource(id = R.string.refund_confirm_policy), + color = Grey10, + style = MaterialTheme.typography.bodyLarge, + ) + } + } + } + Spacer(modifier = Modifier.weight(1.0f)) MainButton( modifier = Modifier .fillMaxWidth() .padding(horizontal = marginHorizontal) - .padding(bottom = 8.dp), + .padding(top = 16.dp) + .padding(vertical = 8.dp), onClick = onRequest, enabled = uiState.isAbleToRequest, label = stringResource(id = R.string.refund_button) @@ -387,3 +422,23 @@ private fun NormalRow( ) } } + +@Composable +private fun PolicyLine( + text: String, + modifier: Modifier = Modifier, +) { + Row(modifier = modifier) { + Box( + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 7.dp) + .size(4.dp) + .clip(shape = RoundedCornerShape(2.dp)) + .background(color = Grey50), + ) + Text( + text = text, + style = MaterialTheme.typography.bodySmall.copy(color = Grey50), + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt index b24142dc..5cd1417f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt @@ -44,6 +44,7 @@ fun RefundScreen( viewModel: RefundViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val refundPolicy by viewModel.refundPolicy.collectAsStateWithLifecycle() val events = viewModel.events val scope = rememberCoroutineScope() val pagerState = rememberPagerState { 2 } @@ -91,6 +92,7 @@ fun RefundScreen( } else { RefundInfoPage( uiState = uiState, + refundPolicy = refundPolicy, modifier = Modifier .fillMaxSize() .padding(innerPadding), @@ -100,6 +102,7 @@ fun RefundScreen( onBankInfoChanged = viewModel::updateBankInfo, onAccountNumberChanged = viewModel::updateAccountNumber, onRequest = { openDialog = true }, + onRefundPolicyChecked = viewModel::toggleRefundPolicyCheck, ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundUiState.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundUiState.kt index cc4af476..6e7fdc5c 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundUiState.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundUiState.kt @@ -9,6 +9,7 @@ data class RefundUiState( val bankInfo: BankInfo? = null, val accountNumber: String = "", val reservation: ReservationDetail? = null, + val refundPolicyChecked: Boolean = false ) { val isValidName: Boolean get() = name.isNotBlank() @@ -19,7 +20,12 @@ data class RefundUiState( return regex.matches(accountNumber) } - val isAbleToRequest: Boolean get() { - return reason.isNotBlank() && isValidName && isValidContact && (bankInfo != null) && isValidAccountNumber - } -} \ No newline at end of file + val isAbleToRequest: Boolean get() = + reason.isNotBlank() && + isValidName && + isValidContact && + (bankInfo != null) && + isValidAccountNumber && + refundPolicyChecked + +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundViewModel.kt index a1120693..a5bb87a9 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.ReservationRepository import com.nexters.boolti.domain.request.RefundRequest +import com.nexters.boolti.domain.usecase.GetRefundPolicyUsecase import com.nexters.boolti.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow @@ -23,6 +24,7 @@ import javax.inject.Inject class RefundViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val reservationRepository: ReservationRepository, + private val getRefundPolicyUsecase: GetRefundPolicyUsecase, ) : BaseViewModel() { private val reservationId: String = checkNotNull(savedStateHandle["reservationId"]) { "reservationId가 전달되어야 합니다." @@ -31,11 +33,15 @@ class RefundViewModel @Inject constructor( private val _uiState: MutableStateFlow = MutableStateFlow(RefundUiState()) val uiState: StateFlow = _uiState.asStateFlow() + private val _refundPolicy = MutableStateFlow>(emptyList()) + val refundPolicy = _refundPolicy.asStateFlow() + private val _events = MutableSharedFlow() val events: SharedFlow = _events.asSharedFlow() init { fetchReservation() + fetchRefundPolicy() } private fun sendEvent(event: RefundEvent) { @@ -85,4 +91,16 @@ class RefundViewModel @Inject constructor( fun updateAccountNumber(newAccountNumber: String) { _uiState.update { it.copy(accountNumber = newAccountNumber) } } + + fun toggleRefundPolicyCheck(selected: Boolean) { + _uiState.update { it.copy(refundPolicyChecked = selected) } + } + + private fun fetchRefundPolicy() { + getRefundPolicyUsecase() + .onEach { refundPolicy -> + _refundPolicy.value = refundPolicy + } + .launchIn(viewModelScope + recordExceptionHandler) + } } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 56e15fd5..4ee43e10 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -222,6 +222,7 @@ 환불 정보 환불 예정 금액 환불 수단 + 취소/환불 규정을 확인했습니다 신고하기 From 28f94f3d60010b88e65a51a974dddec1dde1899d Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 23 Mar 2024 23:07:39 +0900 Subject: [PATCH 071/129] =?UTF-8?q?feat=20:=20=ED=99=98=EB=B6=88=20->=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20=EC=9A=A9=EC=96=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/repository/ReservationRepositoryImpl.kt | 2 +- .../presentation/screen/refund/RefundInfoPage.kt | 2 +- presentation/src/main/res/values/strings.xml | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/data/src/main/java/com/nexters/boolti/data/repository/ReservationRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/ReservationRepositoryImpl.kt index 371a2d7a..19673df2 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/ReservationRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/ReservationRepositoryImpl.kt @@ -26,7 +26,7 @@ internal class ReservationRepositoryImpl @Inject constructor( if (isSuccessful) { emit(Unit) } else { - throw RuntimeException("환불 실패") + throw RuntimeException("취소 실패") } } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt index 8a2da248..6f225f5f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt @@ -236,7 +236,7 @@ fun RefundInfoPage( } Row( modifier = Modifier - .padding(vertical = 20.dp) + .padding(top = 20.dp) .height(48.dp) .fillMaxWidth() .clip(RoundedCornerShape(4.dp)) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 4ee43e10..dc836c14 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -187,10 +187,10 @@ 예매 내역이 없어요 티켓을 예매하고 공연을 즐겨보세요! 입금 확인 중 - 환불 진행 중 + 취소 진행 중 예매 취소 티켓 발권 완료 - 환불 완료 + 취소 완료 상세 보기 알 수 없음 "%s / %d매 / %,d원" @@ -208,8 +208,8 @@ "%s / %d매" - 환불 요청하기 - 환불 이유를 입력해 주세요 + 취소 요청하기 + 취소 사유를 입력해 주세요 예) 티켓 종류 재 선택 후 다시 예매할게요 예금주 정보 실명을 입력해 주세요 @@ -218,7 +218,7 @@ 계좌번호를 입력해 주세요 선택 완료하기 환불 정보를 확인해 주세요 - 환불 요청이 완료되었어요 + 취소 요청이 완료되었어요 환불 정보 환불 예정 금액 환불 수단 From 7af0ce8aa71b29d5dde726f5883d8a067b34c463 Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 23 Mar 2024 23:27:49 +0900 Subject: [PATCH 072/129] =?UTF-8?q?feat=20:=20=ED=99=98=EB=B6=88=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=99=95=EC=9D=B8=20=ED=8C=9D=EC=97=85=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/refund/RefundScreen.kt | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt index 5cd1417f..172d8779 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt @@ -2,6 +2,7 @@ package com.nexters.boolti.presentation.screen.refund import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -34,6 +36,7 @@ import com.nexters.boolti.presentation.component.BtAppBar import com.nexters.boolti.presentation.screen.LocalSnackbarController import com.nexters.boolti.presentation.theme.Grey15 import com.nexters.boolti.presentation.theme.Grey30 +import com.nexters.boolti.presentation.theme.Grey70 import com.nexters.boolti.presentation.theme.Grey80 import kotlinx.coroutines.launch @@ -134,7 +137,7 @@ fun RefundScreen( value = uiState.name ) InfoRow( - modifier = Modifier.padding(top = 16.dp), + modifier = Modifier.padding(top = 12.dp), type = stringResource(id = R.string.contact_label), value = StringBuilder(uiState.contact).apply { if (uiState.contact.length > 7) { @@ -144,15 +147,28 @@ fun RefundScreen( }.toString() ) InfoRow( - modifier = Modifier.padding(top = 16.dp), + modifier = Modifier.padding(top = 12.dp), type = stringResource(id = R.string.bank_name), value = uiState.bankInfo?.bankName ?: "" ) InfoRow( - modifier = Modifier.padding(top = 16.dp), + modifier = Modifier.padding(top = 12.dp), type = stringResource(id = R.string.account_number), value = uiState.accountNumber ) + HorizontalDivider( + modifier = Modifier.padding(top = 12.dp), + thickness = 1.dp, + color = Grey70, + ) + InfoRow( + modifier = Modifier.padding(top = 12.dp), + type = stringResource(id = R.string.refund_price), + value = stringResource( + id = R.string.unit_won, + uiState.reservation!!.totalAmountPrice, + ) + ) } } } @@ -167,9 +183,9 @@ fun InfoRow( ) { Row( modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - modifier = Modifier.width(70.dp), text = type, style = MaterialTheme.typography.bodySmall.copy(color = Grey30), ) From 3877e1f2dfdb2d6cc7f49c7e4f036b915e336919 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 24 Mar 2024 14:32:06 +0900 Subject: [PATCH 073/129] =?UTF-8?q?Boolti-193=20feat:=200=EC=9B=90=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=9C=20=EC=8B=9C=20=EA=B2=B0=EC=A0=9C=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/payment/PaymentScreen.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt index 32b5ffb5..b1f14dde 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt @@ -16,18 +16,11 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.domain.model.ReservationState import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.ToastSnackbarHost -import com.nexters.boolti.presentation.extension.navigateToHome -import com.nexters.boolti.presentation.screen.MainDestination -import com.nexters.boolti.presentation.screen.reservationId -import com.nexters.boolti.presentation.screen.showId import kotlinx.coroutines.launch @Composable @@ -59,7 +52,7 @@ fun PaymentScreen( is PaymentState.Success -> { val reservation = (uiState as PaymentState.Success).reservationDetail when { - reservation.reservationState == ReservationState.RESERVED || reservation.isInviteTicket -> + reservation.totalAmountPrice == 0 || reservation.reservationState == ReservationState.RESERVED || reservation.isInviteTicket -> PaymentCompleteScreen( modifier = Modifier.padding(innerPadding), reservation = reservation From ce29db0571c95f563802f1e97075790d7ae3f1da Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 24 Mar 2024 14:39:04 +0900 Subject: [PATCH 074/129] =?UTF-8?q?Boolti-193=20style:=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=88=EC=95=A1=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/ChooseTicketBottomSheet.kt | 2 +- .../screen/ticketing/TicketingScreen.kt | 29 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt index a0b59a03..2e8ed4ad 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt @@ -224,7 +224,7 @@ private fun ChooseTicketBottomSheetContent2( Spacer(modifier = Modifier.weight(1F)) Text( text = stringResource(R.string.format_total_price, ticket.ticket.price * ticketCount), - style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary), + style = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.primary), ) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index ade9f52f..38744fee 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -64,9 +64,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import coil.compose.AsyncImage import com.nexters.boolti.domain.model.InviteCodeStatus import com.nexters.boolti.presentation.R @@ -77,11 +74,6 @@ import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.dayOfWeekString import com.nexters.boolti.presentation.extension.filterToPhoneNumber import com.nexters.boolti.presentation.extension.format -import com.nexters.boolti.presentation.screen.MainDestination -import com.nexters.boolti.presentation.screen.isInviteTicket -import com.nexters.boolti.presentation.screen.salesTicketId -import com.nexters.boolti.presentation.screen.showId -import com.nexters.boolti.presentation.screen.ticketCount import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Error import com.nexters.boolti.presentation.theme.Grey05 @@ -462,10 +454,23 @@ private fun TicketInfoSection(ticketName: String, ticketCount: Int, totalPrice: label = stringResource(R.string.ticket_count_label), value = stringResource(R.string.ticket_count, ticketCount), ) - SectionTicketInfo( - label = stringResource(R.string.total_payment_amount_label), - value = stringResource(R.string.unit_won, totalPrice), - ) + Row( + modifier = Modifier + .padding(top = 16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.total_payment_amount_label), + style = MaterialTheme.typography.bodyLarge, + color = Grey30 + ) + Text( + text = stringResource(R.string.unit_won, totalPrice), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary, + ) + } Spacer(modifier = Modifier.padding(bottom = 8.dp)) } } From e701e94a4e7aebd41532f5b32b432b24b85b5d39 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 24 Mar 2024 15:32:06 +0900 Subject: [PATCH 075/129] =?UTF-8?q?Boolti-193=20style:=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=ED=99=95=EC=9D=B8=20=EB=8B=A4=EC=9D=B4=EC=96=BC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/TicketingConfirmDialog.kt | 194 +++++++----------- 1 file changed, 70 insertions(+), 124 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt index e8046cec..a7774225 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt @@ -1,21 +1,28 @@ package com.nexters.boolti.presentation.screen.ticketing import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.AbsoluteAlignment +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog import com.nexters.boolti.presentation.extension.toContactFormat +import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey15 import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.marginHorizontal @@ -39,158 +46,97 @@ fun TicketingConfirmDialog( onClickPositiveButton = onClick, onDismiss = onDismiss, ) { - Text(text = stringResource(R.string.ticketing_confirm_dialog_title)) - ConstraintLayout( + Text( + text = stringResource(R.string.ticketing_confirm_dialog_title), + style = MaterialTheme.typography.headlineSmall + ) + Column( modifier = Modifier .padding(top = 24.dp) + .clip(RoundedCornerShape(4.dp)) .fillMaxWidth() .background(MaterialTheme.colorScheme.secondaryContainer) - .clip(RoundedCornerShape(4.dp)) - .padding(horizontal = marginHorizontal, vertical = 16.dp) + .padding(horizontal = marginHorizontal, vertical = 16.dp), ) { - val ( - reservationLabelRef, - depositorRef, - ticketRef, - paymentTypeRef, - ) = createRefs() - - val barrier = createEndBarrier( - reservationLabelRef, - depositorRef, - ticketRef, - paymentTypeRef, - margin = 20.dp, - ) - // 예매자 - Label( - modifier = Modifier.constrainAs(reservationLabelRef) { - top.linkTo(parent.top) - start.linkTo(parent.start) - }, - label = stringResource(R.string.ticket_holder) + InfoRow( + label = stringResource(R.string.ticket_holder), + value1 = reservationName, + value2 = reservationContact.toContactFormat(), ) - val (reservationNameRef, reservationContactRef) = createRefs() - InfoText( - modifier = Modifier.constrainAs(reservationNameRef) { - start.linkTo(barrier) - baseline.linkTo(reservationLabelRef.baseline) - }, - value = reservationName, - ) - InfoText( - modifier = Modifier.constrainAs(reservationContactRef) { - start.linkTo(reservationNameRef.start) - top.linkTo(reservationNameRef.bottom, 4.dp) - }, - value = reservationContact.toContactFormat(), - ) - // 입금자 - val (depositorNameRef, depositorContactRef) = createRefs() if (!isInviteTicket) { - Label( - modifier = Modifier.constrainAs(depositorRef) { - top.linkTo(reservationContactRef.bottom, 16.dp) - start.linkTo(parent.start) - }, + InfoRow( + modifier = Modifier.padding(top = 16.dp), label = stringResource(R.string.depositor), - ) - InfoText( - modifier = Modifier.constrainAs(depositorNameRef) { - start.linkTo(barrier) - baseline.linkTo(depositorRef.baseline) - }, - value = depositor - ) - InfoText( - modifier = Modifier.constrainAs(depositorContactRef) { - start.linkTo(depositorNameRef.start) - top.linkTo(depositorNameRef.bottom, 4.dp) - }, - value = depositorContact.toContactFormat(), + value1 = depositor, + value2 = depositorContact.toContactFormat(), ) } - // 티켓 - Label( - modifier = Modifier.constrainAs(ticketRef) { - if (isInviteTicket) { - top.linkTo(reservationContactRef.bottom, 16.dp) - } else { - top.linkTo(depositorContactRef.bottom, 16.dp) - } - start.linkTo(parent.start) - }, + InfoRow( + modifier = Modifier.padding(top = 16.dp), label = stringResource(R.string.ticket), + value1 = ticketName, + value2 = stringResource(R.string.reservations_ticket_count_price_format_short, ticketCount, totalPrice), ) - val (ticketNameRef, ticketInfoRef) = createRefs() - InfoText( - modifier = Modifier.constrainAs(ticketNameRef) { - start.linkTo(barrier) - baseline.linkTo(ticketRef.baseline) - }, - value = ticketName, - ) - InfoText( - modifier = Modifier.constrainAs(ticketInfoRef) { - start.linkTo(ticketNameRef.start) - top.linkTo(ticketNameRef.bottom, 4.dp) - }, - value = stringResource(R.string.reservations_ticket_count_price_format_short, ticketCount, totalPrice), - ) - // 결제 수단 - Label( - modifier = Modifier.constrainAs(paymentTypeRef) { - top.linkTo(ticketInfoRef.bottom, 16.dp) - start.linkTo(parent.start) - }, - label = stringResource(R.string.ticket_type_label), - ) - val paymentTypeDataRef = createRef() - InfoText( - modifier = Modifier.constrainAs(paymentTypeDataRef) { - start.linkTo(barrier) - baseline.linkTo(paymentTypeRef.baseline) - }, - value = if (isInviteTicket) { - stringResource(R.string.invite_ticket) - } else { + if (!isInviteTicket) { + InfoRow( + modifier = Modifier.padding(top = 16.dp), + label = stringResource(R.string.ticket_type_label), + value1 = when (paymentType) { PaymentType.ACCOUNT_TRANSFER -> stringResource(R.string.payment_account_transfer) PaymentType.CARD -> stringResource(R.string.payment_card) PaymentType.UNDEFINED -> "" - } - } - ) + }, + ) + } } } } @Composable -private fun Label( +private fun InfoRow( modifier: Modifier = Modifier, label: String, + value1: String, + value2: String? = null, ) { - Text( - modifier = modifier, - text = label, - style = MaterialTheme.typography.bodySmall, - color = Grey30, - ) + Column(modifier = modifier) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(text = label, style = MaterialTheme.typography.bodySmall, color = Grey30) + Text(text = value1, style = MaterialTheme.typography.bodySmall, color = Grey15) + } + value2?.let { + Text( + modifier = Modifier + .padding(top = 4.dp) + .align(AbsoluteAlignment.Right), + text = value2, + style = MaterialTheme.typography.bodySmall, + color = Grey15, + ) + } + } } +@Preview @Composable -private fun InfoText( - modifier: Modifier = Modifier, - value: String, -) { - Text( - modifier = modifier, - text = value, - style = MaterialTheme.typography.bodySmall, - color = Grey15, - ) +private fun InfoRowPreview() { + Surface { + BooltiTheme { + InfoRow( + modifier = Modifier.fillMaxWidth(), + label = "예매자", + value1 = "박명범", + value2 = "01020302030" + ) + } + } } From 73a2f504c49594e331ecf31d6c2d34930485d822 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 24 Mar 2024 16:58:54 +0900 Subject: [PATCH 076/129] =?UTF-8?q?Boolti-193=20feat:=20=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EC=96=BC=EB=A1=9C=EA=B7=B8=EC=97=90=EC=84=9C=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EB=8B=A8=20=EC=97=B4=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=20=EC=A1=B0=EA=B1=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/ticketing/TicketingConfirmDialog.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt index a7774225..e466c662 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt @@ -81,15 +81,18 @@ fun TicketingConfirmDialog( value2 = stringResource(R.string.reservations_ticket_count_price_format_short, ticketCount, totalPrice), ) // 결제 수단 - if (!isInviteTicket) { + if (isInviteTicket || totalPrice > 0) { InfoRow( modifier = Modifier.padding(top = 16.dp), label = stringResource(R.string.ticket_type_label), - value1 = - when (paymentType) { - PaymentType.ACCOUNT_TRANSFER -> stringResource(R.string.payment_account_transfer) - PaymentType.CARD -> stringResource(R.string.payment_card) - PaymentType.UNDEFINED -> "" + value1 = if (isInviteTicket) { + stringResource(R.string.invite_ticket) + } else { + when (paymentType) { + PaymentType.ACCOUNT_TRANSFER -> stringResource(R.string.payment_account_transfer) + PaymentType.CARD -> stringResource(R.string.payment_card) + PaymentType.UNDEFINED -> "" + } }, ) } From bdf4a7c9e0ac5b0ca8f217be10fedd412482f677 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 24 Mar 2024 17:22:01 +0900 Subject: [PATCH 077/129] =?UTF-8?q?Boolti-193=20feat:=200=EC=9B=90=20?= =?UTF-8?q?=ED=8B=B0=EC=BC=93=EC=9D=BC=20=EA=B2=BD=EC=9A=B0=20UI=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/ticketing/TicketingScreen.kt | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index 38744fee..84abc0aa 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -152,34 +152,48 @@ fun TicketingScreen( showName = uiState.showName, showDate = uiState.showDate, ) + // 예매자 정보 TicketHolderSection( name = uiState.reservationName, phoneNumber = uiState.reservationContact, isSameContactInfo = uiState.isSameContactInfo, onNameChanged = viewModel::setReservationName, onPhoneNumberChanged = viewModel::setReservationPhoneNumber, - ) // 예매자 정보 - if (!uiState.isInviteTicket) DeposorSection( - name = uiState.depositorName, - phoneNumber = uiState.depositorContact, - isSameContactInfo = uiState.isSameContactInfo, - onClickSameContact = viewModel::toggleIsSameContactInfo, - onNameChanged = viewModel::setDepositorName, - onPhoneNumberChanged = viewModel::setDepositorPhoneNumber, - ) // 입금자 정보 + ) + + // 입금자 정보 + if (!uiState.isInviteTicket && uiState.totalPrice > 0) { + DepositorSection( + name = uiState.depositorName, + phoneNumber = uiState.depositorContact, + isSameContactInfo = uiState.isSameContactInfo, + onClickSameContact = viewModel::toggleIsSameContactInfo, + onNameChanged = viewModel::setDepositorName, + onPhoneNumberChanged = viewModel::setDepositorPhoneNumber, + ) + } + + // 티켓 정보 TicketInfoSection( ticketName = uiState.ticketName, ticketCount = uiState.ticketCount, totalPrice = uiState.totalPrice, - ) // 티켓 정보 - if (uiState.isInviteTicket) InviteCodeSection( - uiState.inviteCode, - uiState.inviteCodeStatus, - onClickCheckInviteCode = viewModel::checkInviteCode, - onInviteCodeChanged = viewModel::setInviteCode, - ) // 초청 코드 - if (!uiState.isInviteTicket) PaymentSection(scope, snackbarHostState) // 결제 수단 + ) + + // 초청 코드 + if (uiState.isInviteTicket) { + InviteCodeSection( + uiState.inviteCode, + uiState.inviteCodeStatus, + onClickCheckInviteCode = viewModel::checkInviteCode, + onInviteCodeChanged = viewModel::setInviteCode, + ) + } + + if (!uiState.isInviteTicket && uiState.totalPrice > 0) PaymentSection(scope, snackbarHostState) // 결제 수단 if (!uiState.isInviteTicket) RefundPolicySection(uiState.refundPolicy) // 취소/환불 규정 + + // 사업자 정보 BusinessInformation( modifier = Modifier.fillMaxWidth(), onClick = navigateToBusiness @@ -476,7 +490,7 @@ private fun TicketInfoSection(ticketName: String, ticketCount: Int, totalPrice: } @Composable -private fun DeposorSection( +private fun DepositorSection( name: String = "", phoneNumber: String = "", isSameContactInfo: Boolean, From e3e03efcb67b7c0c07a06722a0e085b569f10638 Mon Sep 17 00:00:00 2001 From: algosketch Date: Mon, 25 Mar 2024 21:32:51 +0900 Subject: [PATCH 078/129] =?UTF-8?q?feat=20:=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=94=A5=EB=A7=81=ED=81=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=ED=95=98=EB=82=98=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/DeepLinkEvent.kt | 17 +++++++++++ .../presentation/screen/HomeViewModel.kt | 17 +++++++++++ .../presentation/screen/home/HomeEvent.kt | 7 +++++ .../presentation/screen/home/HomeScreen.kt | 24 +++++++++++++-- .../screen/splash/SplashActivity.kt | 28 +++++------------ .../screen/splash/SplashViewModel.kt | 30 +++++++++++++++++++ 6 files changed, 100 insertions(+), 23 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/DeepLinkEvent.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeEvent.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashViewModel.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/DeepLinkEvent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/DeepLinkEvent.kt new file mode 100644 index 00000000..539a9a34 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/DeepLinkEvent.kt @@ -0,0 +1,17 @@ +package com.nexters.boolti.presentation.screen + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DeepLinkEvent @Inject constructor() { + private val _events = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val events = _events.receiveAsFlow() + + suspend fun sendEvent(deepLink: String) { + _events.send(deepLink) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt index 83be0f8d..77bb4de4 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt @@ -3,10 +3,15 @@ package com.nexters.boolti.presentation.screen import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.AuthRepository import com.nexters.boolti.presentation.base.BaseViewModel +import com.nexters.boolti.presentation.screen.home.HomeEvent import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.plus @@ -15,6 +20,7 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val authRepository: AuthRepository, + private val deepLinkEvent: DeepLinkEvent, ) : BaseViewModel() { val loggedIn = authRepository.loggedIn.stateIn( viewModelScope, @@ -22,9 +28,13 @@ class HomeViewModel @Inject constructor( null, ) + private val _events = Channel() + val events: ReceiveChannel = _events + init { initUserInfo() sendFcmToken() + collectDeepLinkEvents() } private fun initUserInfo() { @@ -39,4 +49,11 @@ class HomeViewModel @Inject constructor( } } } + + private fun collectDeepLinkEvents() { + deepLinkEvent.events + .filter { it.startsWith("https://boolti.in/home") } + .onEach { _events.send(HomeEvent.NavigateToDeepLink(it)) } + .launchIn(viewModelScope + recordExceptionHandler) + } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeEvent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeEvent.kt new file mode 100644 index 00000000..40172ae1 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeEvent.kt @@ -0,0 +1,7 @@ +package com.nexters.boolti.presentation.screen.home + +sealed interface HomeEvent { + data class NavigateToDeepLink( + val deeplink: String + ) : HomeEvent +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt index 16f63271..e78b4ab6 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt @@ -1,5 +1,7 @@ package com.nexters.boolti.presentation.screen.home +import android.content.Intent +import android.net.Uri import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.layout.Column @@ -14,6 +16,7 @@ import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -22,16 +25,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.navigation.navDeepLink import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.screen.HomeViewModel -import com.nexters.boolti.presentation.screen.MainDestination import com.nexters.boolti.presentation.screen.my.MyScreen import com.nexters.boolti.presentation.screen.show.ShowScreen import com.nexters.boolti.presentation.screen.ticket.TicketLoginScreen @@ -39,6 +40,7 @@ import com.nexters.boolti.presentation.screen.ticket.TicketScreen import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey50 import com.nexters.boolti.presentation.theme.Grey85 +import kotlinx.coroutines.channels.consumeEach @Composable fun HomeScreen( @@ -59,6 +61,16 @@ fun HomeScreen( val loggedIn by viewModel.loggedIn.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + viewModel.events.consumeEach { event -> + when (event) { + is HomeEvent.NavigateToDeepLink -> { + navController.navigate(Uri.parse(event.deeplink)) + } + } + } + } + Scaffold( bottomBar = { HomeNavigationBar( @@ -90,6 +102,12 @@ fun HomeScreen( } composable( route = Destination.Ticket.route, + deepLinks = listOf( + navDeepLink { + uriPattern = "https://boolti.in/home/tickets" + action = Intent.ACTION_VIEW + } + ) ) { when (loggedIn) { true -> TicketScreen( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt index 5b4719f8..ce8dfef8 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt @@ -22,10 +22,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.lifecycle.ViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewModelScope -import com.nexters.boolti.domain.repository.ConfigRepository import com.nexters.boolti.presentation.BuildConfig import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog @@ -33,9 +30,6 @@ import com.nexters.boolti.presentation.screen.MainActivity import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey50 import dagger.hilt.android.AndroidEntryPoint -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject @AndroidEntryPoint class SplashActivity : ComponentActivity() { @@ -52,10 +46,17 @@ class SplashActivity : ComponentActivity() { shouldUpdate = shouldUpdate, onSuccessVersionCheck = { startActivity(Intent(this, MainActivity::class.java)) + intent.extras?.getString("합의된 key").let { + // TODO : 받은 값에 따라 딥링크 생성하기 + val deeplink = "https://boolti.in/home/tickets" + viewModel.sendDeepLinkEvent(deeplink) + } + finish() }, onClickUpdate = { - val playStoreUrl = "http://play.google.com/store/apps/details?id=${BuildConfig.PACKAGE_NAME}" + val playStoreUrl = + "http://play.google.com/store/apps/details?id=${BuildConfig.PACKAGE_NAME}" startActivity( Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(playStoreUrl) @@ -121,16 +122,3 @@ fun UpdateDialogPreview() { } } } - -@HiltViewModel -class SplashViewModel @Inject constructor( - configRepository: ConfigRepository, -) : ViewModel() { - init { - viewModelScope.launch { - configRepository.cacheRefundPolicy() - } - } - - val shouldUpdate = configRepository.shouldUpdate() -} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashViewModel.kt new file mode 100644 index 00000000..60a60a72 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashViewModel.kt @@ -0,0 +1,30 @@ +package com.nexters.boolti.presentation.screen.splash + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nexters.boolti.domain.repository.ConfigRepository +import com.nexters.boolti.presentation.screen.DeepLinkEvent +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SplashViewModel @Inject constructor( + configRepository: ConfigRepository, + private val deepLinkEvent: DeepLinkEvent, +) : ViewModel() { + init { + viewModelScope.launch { + configRepository.cacheRefundPolicy() + } + } + + val shouldUpdate = configRepository.shouldUpdate() + + fun sendDeepLinkEvent(deeplink: String) { + viewModelScope.launch { + deepLinkEvent.sendEvent(deeplink) + } + } +} + From 45a0e943215ef1b26aa21e9919e7adf9dc693115 Mon Sep 17 00:00:00 2001 From: algosketch Date: Tue, 26 Mar 2024 21:08:24 +0900 Subject: [PATCH 079/129] =?UTF-8?q?fix=20:=20=EB=8B=A4=EC=96=91=ED=95=9C?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EB=94=A5=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EB=A5=BC=20=EA=B5=AC?= =?UTF-8?q?=EB=8F=85=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/DeepLinkEvent.kt | 13 ++++++---- .../presentation/screen/HomeViewModel.kt | 24 +++++++------------ .../presentation/screen/MainActivity.kt | 2 +- .../presentation/screen/home/HomeEvent.kt | 7 ------ .../presentation/screen/home/HomeScreen.kt | 8 ++----- .../screen/splash/SplashActivity.kt | 10 ++++---- 6 files changed, 26 insertions(+), 38 deletions(-) delete mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeEvent.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/DeepLinkEvent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/DeepLinkEvent.kt index 539a9a34..98ea4ca1 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/DeepLinkEvent.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/DeepLinkEvent.kt @@ -1,17 +1,20 @@ package com.nexters.boolti.presentation.screen import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import javax.inject.Inject import javax.inject.Singleton @Singleton class DeepLinkEvent @Inject constructor() { - private val _events = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - val events = _events.receiveAsFlow() + private val _events = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + val events = _events.asSharedFlow() suspend fun sendEvent(deepLink: String) { - _events.send(deepLink) + _events.emit(deepLink) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt index 77bb4de4..c7b01b85 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt @@ -3,15 +3,13 @@ package com.nexters.boolti.presentation.screen import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.AuthRepository import com.nexters.boolti.presentation.base.BaseViewModel -import com.nexters.boolti.presentation.screen.home.HomeEvent import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.plus @@ -20,7 +18,7 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val authRepository: AuthRepository, - private val deepLinkEvent: DeepLinkEvent, + deepLinkEvent: DeepLinkEvent, ) : BaseViewModel() { val loggedIn = authRepository.loggedIn.stateIn( viewModelScope, @@ -28,13 +26,16 @@ class HomeViewModel @Inject constructor( null, ) - private val _events = Channel() - val events: ReceiveChannel = _events + val event: SharedFlow = + deepLinkEvent.events.filter { it.startsWith("https://boolti.in/home") } + .shareIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + ) init { initUserInfo() sendFcmToken() - collectDeepLinkEvents() } private fun initUserInfo() { @@ -49,11 +50,4 @@ class HomeViewModel @Inject constructor( } } } - - private fun collectDeepLinkEvents() { - deepLinkEvent.events - .filter { it.startsWith("https://boolti.in/home") } - .onEach { _events.send(HomeEvent.NavigateToDeepLink(it)) } - .launchIn(viewModelScope + recordExceptionHandler) - } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt index 29acdf4f..d1ee937d 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt @@ -73,4 +73,4 @@ class MainActivity : ComponentActivity() { } } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeEvent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeEvent.kt deleted file mode 100644 index 40172ae1..00000000 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.nexters.boolti.presentation.screen.home - -sealed interface HomeEvent { - data class NavigateToDeepLink( - val deeplink: String - ) : HomeEvent -} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt index e78b4ab6..1035c953 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt @@ -62,12 +62,8 @@ fun HomeScreen( val loggedIn by viewModel.loggedIn.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - viewModel.events.consumeEach { event -> - when (event) { - is HomeEvent.NavigateToDeepLink -> { - navController.navigate(Uri.parse(event.deeplink)) - } - } + viewModel.event.collect { deepLink -> + navController.navigate(Uri.parse(deepLink)) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt index ce8dfef8..696bfa31 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt @@ -46,10 +46,12 @@ class SplashActivity : ComponentActivity() { shouldUpdate = shouldUpdate, onSuccessVersionCheck = { startActivity(Intent(this, MainActivity::class.java)) - intent.extras?.getString("합의된 key").let { - // TODO : 받은 값에 따라 딥링크 생성하기 - val deeplink = "https://boolti.in/home/tickets" - viewModel.sendDeepLinkEvent(deeplink) + intent.extras?.getString("합의된 key").let { // TODO : 서버에서 푸시 알림 타입을 확정하면 변경하기 + val deepLink = when (it) { + else -> "https://boolti.in/home/tickets" + } + + viewModel.sendDeepLinkEvent(deepLink) } finish() From 7f114cac33a6d85a7f93ec928449ee0ebd4d8090 Mon Sep 17 00:00:00 2001 From: algosketch Date: Tue, 26 Mar 2024 21:11:54 +0900 Subject: [PATCH 080/129] =?UTF-8?q?fix=20:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/nexters/boolti/presentation/screen/HomeViewModel.kt | 2 +- .../main/java/com/nexters/boolti/presentation/screen/Main.kt | 2 +- .../com/nexters/boolti/presentation/screen/home/HomeScreen.kt | 2 +- .../nexters/boolti/presentation/screen/splash/SplashActivity.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt index c7b01b85..18db2497 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/HomeViewModel.kt @@ -27,7 +27,7 @@ class HomeViewModel @Inject constructor( ) val event: SharedFlow = - deepLinkEvent.events.filter { it.startsWith("https://boolti.in/home") } + deepLinkEvent.events.filter { it.startsWith("https://app.boolti.in/home") } .shareIn( scope = viewModelScope, started = SharingStarted.Lazily, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 0a84a8a2..69b81c9f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -110,7 +110,7 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: arguments = ShowDetail.arguments, deepLinks = listOf( navDeepLink { - uriPattern = "https://boolti.in/show?showId={$showId}" + uriPattern = "https://app.boolti.in/show?showId={$showId}" action = Intent.ACTION_VIEW } ), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt index 1035c953..9c20a35c 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt @@ -100,7 +100,7 @@ fun HomeScreen( route = Destination.Ticket.route, deepLinks = listOf( navDeepLink { - uriPattern = "https://boolti.in/home/tickets" + uriPattern = "https://app.boolti.in/home/tickets" action = Intent.ACTION_VIEW } ) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt index 696bfa31..9d238d34 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt @@ -48,7 +48,7 @@ class SplashActivity : ComponentActivity() { startActivity(Intent(this, MainActivity::class.java)) intent.extras?.getString("합의된 key").let { // TODO : 서버에서 푸시 알림 타입을 확정하면 변경하기 val deepLink = when (it) { - else -> "https://boolti.in/home/tickets" + else -> "https://app.boolti.in/home/tickets" } viewModel.sendDeepLinkEvent(deepLink) From aebcf101b1b22da3dfbb45502d31e3b9ae0d2641 Mon Sep 17 00:00:00 2001 From: algosketch Date: Tue, 26 Mar 2024 21:16:03 +0900 Subject: [PATCH 081/129] =?UTF-8?q?feat=20:=20=ED=8F=AC=EA=B7=B8=EB=9D=BC?= =?UTF-8?q?=EC=9A=B4=EB=93=9C=20=EB=94=A5=EB=A7=81=ED=81=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/BtFirebaseMessagingService.kt | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt index ed3c5958..06cf7dfb 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt @@ -8,12 +8,14 @@ import com.google.firebase.messaging.RemoteMessage import com.nexters.boolti.domain.repository.AuthRepository import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.extension.checkGrantedPermission +import com.nexters.boolti.presentation.screen.DeepLinkEvent import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint @@ -23,6 +25,9 @@ class BtFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var authRepository: AuthRepository + @Inject + lateinit var deepLinkEvent: DeepLinkEvent + override fun onNewToken(token: String) { scope.launch { authRepository.sendFcmToken() @@ -32,6 +37,19 @@ class BtFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage) { if(!checkGrantedPermission(Manifest.permission.POST_NOTIFICATIONS)) return + if (remoteMessage.data.isNotEmpty()) { + Timber.d("Message data payload: ${remoteMessage.data}") + + // TODO : 서버에서 푸시 알림 타입을 확정하면 변경하기 + val deepLink = when (remoteMessage.data["합의된 Key"]) { + else -> "https://app.boolti.in/home/tickets" + } + + scope.launch { + deepLinkEvent.sendEvent(deepLink) + } + } + remoteMessage.notification?.let { notification -> val defaultChannelId = getString(R.string.default_notification_channel_id) val builder = @@ -48,4 +66,4 @@ class BtFirebaseMessagingService : FirebaseMessagingService() { super.onDestroy() scope.cancel() } -} \ No newline at end of file +} From 9ccec2704a834984538e2cf20b5c29536fcff065 Mon Sep 17 00:00:00 2001 From: algosketch Date: Tue, 26 Mar 2024 21:40:38 +0900 Subject: [PATCH 082/129] =?UTF-8?q?feat=20:=20=EC=9E=90=EB=8F=99=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=A4=ED=8C=A8=EC=8B=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/nexters/boolti/data/datasource/AuthDataSource.kt | 2 +- .../java/com/nexters/boolti/data/network/AuthAuthenticator.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt index 4de3e1ac..aaa135a3 100644 --- a/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt +++ b/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt @@ -48,8 +48,8 @@ internal class AuthDataSource @Inject constructor( } suspend fun logout(): Result = runCatching { - loginService.logout() localLogout() + loginService.logout() } suspend fun localLogout() { diff --git a/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt b/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt index fe291b9c..8ae8e232 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt @@ -28,6 +28,9 @@ internal class AuthAuthenticator @Inject constructor( refreshToken = it.refreshToken, ) it.accessToken + } ?: run { + authDataSource.logout() + null } } } From 4ed80d777fd7ad448aae079bd9779df135f89686 Mon Sep 17 00:00:00 2001 From: algosketch Date: Wed, 27 Mar 2024 04:01:30 +0900 Subject: [PATCH 083/129] =?UTF-8?q?feat=20:=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=ED=81=B4=EB=A6=AD=EC=8B=9C=20=ED=8B=B0?= =?UTF-8?q?=EC=BC=93=ED=83=AD=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/splash/SplashActivity.kt | 4 ++- .../service/BtFirebaseMessagingService.kt | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt index 9d238d34..495cb149 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.app.NotificationManagerCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nexters.boolti.presentation.BuildConfig @@ -46,12 +47,13 @@ class SplashActivity : ComponentActivity() { shouldUpdate = shouldUpdate, onSuccessVersionCheck = { startActivity(Intent(this, MainActivity::class.java)) - intent.extras?.getString("합의된 key").let { // TODO : 서버에서 푸시 알림 타입을 확정하면 변경하기 + intent.extras?.getString("합의된 key")?.let { // TODO : 서버에서 푸시 알림 타입을 확정하면 변경하기 val deepLink = when (it) { else -> "https://app.boolti.in/home/tickets" } viewModel.sendDeepLinkEvent(deepLink) + NotificationManagerCompat.from(this).cancel(0) // TODO : 서버에서 푸시 알림 타입을 확정하면 변경하기 } finish() diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt index 06cf7dfb..aa56b99c 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt @@ -1,6 +1,8 @@ package com.nexters.boolti.presentation.service import android.Manifest +import android.app.PendingIntent +import android.content.Intent import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.google.firebase.messaging.FirebaseMessagingService @@ -9,13 +11,14 @@ import com.nexters.boolti.domain.repository.AuthRepository import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.extension.checkGrantedPermission import com.nexters.boolti.presentation.screen.DeepLinkEvent +import com.nexters.boolti.presentation.screen.splash.SplashActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import timber.log.Timber +import java.util.UUID import javax.inject.Inject @AndroidEntryPoint @@ -35,29 +38,24 @@ class BtFirebaseMessagingService : FirebaseMessagingService() { } override fun onMessageReceived(remoteMessage: RemoteMessage) { - if(!checkGrantedPermission(Manifest.permission.POST_NOTIFICATIONS)) return - - if (remoteMessage.data.isNotEmpty()) { - Timber.d("Message data payload: ${remoteMessage.data}") - - // TODO : 서버에서 푸시 알림 타입을 확정하면 변경하기 - val deepLink = when (remoteMessage.data["합의된 Key"]) { - else -> "https://app.boolti.in/home/tickets" - } - - scope.launch { - deepLinkEvent.sendEvent(deepLink) - } - } + if (!checkGrantedPermission(Manifest.permission.POST_NOTIFICATIONS)) return remoteMessage.notification?.let { notification -> val defaultChannelId = getString(R.string.default_notification_channel_id) + val pendingIntent = PendingIntent.getActivity( + applicationContext, + UUID.randomUUID().hashCode(), + Intent(this, SplashActivity::class.java).putExtra("합의된 key", ""), + PendingIntent.FLAG_IMMUTABLE + ) val builder = NotificationCompat.Builder(this, notification.channelId ?: defaultChannelId) .setContentTitle(notification.title) .setContentText(notification.body) .setSmallIcon(R.drawable.ic_logo) + .setContentIntent(pendingIntent) + // TODO : 서버에서 푸시 알림 타입을 확정하면 변경하기 NotificationManagerCompat.from(this).notify(0, builder.build()) } } From 00db9ec53939aef7b3974a6abb9227d1ff14ac12 Mon Sep 17 00:00:00 2001 From: algosketch Date: Wed, 27 Mar 2024 21:34:41 +0900 Subject: [PATCH 084/129] =?UTF-8?q?feat=20:=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/splash/SplashActivity.kt | 12 +++--- .../service/BtFirebaseMessagingService.kt | 37 ++++++++++--------- .../presentation/service/BtNotification.kt | 15 ++++++++ 3 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/service/BtNotification.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt index 495cb149..b819d605 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/splash/SplashActivity.kt @@ -28,6 +28,7 @@ import com.nexters.boolti.presentation.BuildConfig import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog import com.nexters.boolti.presentation.screen.MainActivity +import com.nexters.boolti.presentation.service.BtNotification import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey50 import dagger.hilt.android.AndroidEntryPoint @@ -47,13 +48,14 @@ class SplashActivity : ComponentActivity() { shouldUpdate = shouldUpdate, onSuccessVersionCheck = { startActivity(Intent(this, MainActivity::class.java)) - intent.extras?.getString("합의된 key")?.let { // TODO : 서버에서 푸시 알림 타입을 확정하면 변경하기 - val deepLink = when (it) { - else -> "https://app.boolti.in/home/tickets" + intent.extras?.getString("type")?.let { type -> + val notification = BtNotification(type) + + notification.deepLink?.let { + viewModel.sendDeepLinkEvent(it) } - viewModel.sendDeepLinkEvent(deepLink) - NotificationManagerCompat.from(this).cancel(0) // TODO : 서버에서 푸시 알림 타입을 확정하면 변경하기 + NotificationManagerCompat.from(this).cancel(notification.id) } finish() diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt index aa56b99c..a7888ddc 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt @@ -40,24 +40,27 @@ class BtFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage) { if (!checkGrantedPermission(Manifest.permission.POST_NOTIFICATIONS)) return - remoteMessage.notification?.let { notification -> - val defaultChannelId = getString(R.string.default_notification_channel_id) - val pendingIntent = PendingIntent.getActivity( - applicationContext, - UUID.randomUUID().hashCode(), - Intent(this, SplashActivity::class.java).putExtra("합의된 key", ""), - PendingIntent.FLAG_IMMUTABLE - ) - val builder = - NotificationCompat.Builder(this, notification.channelId ?: defaultChannelId) - .setContentTitle(notification.title) - .setContentText(notification.body) - .setSmallIcon(R.drawable.ic_logo) - .setContentIntent(pendingIntent) + val notification = remoteMessage.notification ?: return + val btNotification = BtNotification(remoteMessage.data["type"]) - // TODO : 서버에서 푸시 알림 타입을 확정하면 변경하기 - NotificationManagerCompat.from(this).notify(0, builder.build()) - } + val defaultChannelId = getString(R.string.default_notification_channel_id) + val pendingIntent = PendingIntent.getActivity( + applicationContext, + UUID.randomUUID().hashCode(), + Intent(this, SplashActivity::class.java).putExtra( + "type", + btNotification.type + ), + PendingIntent.FLAG_IMMUTABLE + ) + val builder = + NotificationCompat.Builder(this, notification.channelId ?: defaultChannelId) + .setContentTitle(notification.title) + .setContentText(notification.body) + .setSmallIcon(R.drawable.ic_logo) + .setContentIntent(pendingIntent) + + NotificationManagerCompat.from(this).notify(btNotification.id, builder.build()) } override fun onDestroy() { diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtNotification.kt b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtNotification.kt new file mode 100644 index 00000000..73bc7819 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtNotification.kt @@ -0,0 +1,15 @@ +package com.nexters.boolti.presentation.service + +enum class BtNotification(val id: Int, val type: String, val deepLink: String?) { + RESERVATION_COMPLETED(id = 0, type = "RESERVATION_COMPLETED", deepLink = "https://app.boolti.in/home/tickets"), + ENTER_NOTIFICATION(id = 3, type = "ENTER_NOTIFICATION", deepLink = "https://app.boolti.in/home/tickets"), + UNDEFINED(id = -1, type = "UNDEFINED", deepLink = null), +} + +fun BtNotification(type: String?): BtNotification { + return when(type) { + BtNotification.RESERVATION_COMPLETED.type -> BtNotification.RESERVATION_COMPLETED + BtNotification.ENTER_NOTIFICATION.type -> BtNotification.ENTER_NOTIFICATION + else -> BtNotification.UNDEFINED + } +} From ec7bb9acab506b453d56a772e9465c8d766feaa7 Mon Sep 17 00:00:00 2001 From: algosketch Date: Wed, 27 Mar 2024 21:48:42 +0900 Subject: [PATCH 085/129] =?UTF-8?q?feat=20:=20ic=5Flogo=20=EC=95=B1=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=B9=BC=EB=9D=BC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/ticket/TicketContent.kt | 3 ++- .../presentation/screen/ticket/detail/TicketDetailScreen.kt | 2 +- .../presentation/service/BtFirebaseMessagingService.kt | 1 + presentation/src/main/res/drawable/ic_logo.xml | 6 +++--- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt index f41356be..da92d666 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt @@ -53,6 +53,7 @@ import com.nexters.boolti.presentation.extension.toPx import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey40 import com.nexters.boolti.presentation.theme.Grey50 +import com.nexters.boolti.presentation.theme.Grey70 import com.nexters.boolti.presentation.theme.Grey80 import com.nexters.boolti.presentation.theme.Grey95 import com.nexters.boolti.presentation.theme.marginHorizontal @@ -193,7 +194,7 @@ private fun Title( ) Image( painter = painterResource(R.drawable.ic_logo), - colorFilter = ColorFilter.tint(Grey80), + colorFilter = ColorFilter.tint(Grey70.copy(alpha = 0.5f)), contentDescription = null, ) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt index d50d3579..8ad77ab6 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt @@ -416,7 +416,7 @@ private fun Title( ) Image( painter = painterResource(R.drawable.ic_logo), - colorFilter = ColorFilter.tint(Grey80), + colorFilter = ColorFilter.tint(Grey70.copy(alpha = 0.5f)), contentDescription = null, ) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt index a7888ddc..0ce2ba05 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt @@ -57,6 +57,7 @@ class BtFirebaseMessagingService : FirebaseMessagingService() { NotificationCompat.Builder(this, notification.channelId ?: defaultChannelId) .setContentTitle(notification.title) .setContentText(notification.body) + .setColor(0xFF6827) .setSmallIcon(R.drawable.ic_logo) .setContentIntent(pendingIntent) diff --git a/presentation/src/main/res/drawable/ic_logo.xml b/presentation/src/main/res/drawable/ic_logo.xml index fa642826..f572dc9f 100644 --- a/presentation/src/main/res/drawable/ic_logo.xml +++ b/presentation/src/main/res/drawable/ic_logo.xml @@ -8,8 +8,8 @@ android:pathData="M0,0h18v18h-18z"/> + android:strokeAlpha="1" + android:fillColor="#FFFFFF" + android:fillAlpha="1"/> From 1379d430d1bff60adde85e98ac9990af25bcce8e Mon Sep 17 00:00:00 2001 From: algosketch Date: Wed, 27 Mar 2024 21:52:54 +0900 Subject: [PATCH 086/129] =?UTF-8?q?fix=20:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/presentation/screen/show/ShowDetailScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index 22898436..b8d66dd6 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -266,7 +266,7 @@ private fun ShowDetailAppBar( .size(44.dp), onClick = { Firebase.dynamicLinks.shortLinkAsync { - link = Uri.parse("https://boolti.in/show?showId=$showId") + link = Uri.parse("https://app.boolti.in/show?showId=$showId") domainUriPrefix = "https://boolti.page.link" androidParameters { } From d434fe7c14abc1ff39a4221968bacfc577f94d2c Mon Sep 17 00:00:00 2001 From: algosketch Date: Wed, 27 Mar 2024 21:53:54 +0900 Subject: [PATCH 087/129] =?UTF-8?q?feat=20:=20iOS=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/show/ShowDetailScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index b8d66dd6..fd95beec 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -59,6 +59,7 @@ import com.google.firebase.Firebase import com.google.firebase.dynamiclinks.androidParameters import com.google.firebase.dynamiclinks.dynamicLink import com.google.firebase.dynamiclinks.dynamicLinks +import com.google.firebase.dynamiclinks.iosParameters import com.google.firebase.dynamiclinks.shortLinkAsync import com.nexters.boolti.domain.model.ShowDetail import com.nexters.boolti.domain.model.ShowState @@ -270,7 +271,7 @@ private fun ShowDetailAppBar( domainUriPrefix = "https://boolti.page.link" androidParameters { } - // iosParameters("com.example.ios") { } TODO : iOS 패키지 네임 넣기 + iosParameters("com.nexters.boolti") { } }.addOnSuccessListener { it.shortLink?.let { link -> println(link) From 29731a8c9d7b7dd3adc226c34203ab67bd63ba07 Mon Sep 17 00:00:00 2001 From: algosketch Date: Wed, 27 Mar 2024 23:09:38 +0900 Subject: [PATCH 088/129] =?UTF-8?q?feat=20:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=88=98=EC=A0=95.=20=EB=82=B4=EA=B0=80?= =?UTF-8?q?=20=EC=9D=B4=EA=B1=B0=EB=95=8C=EB=AC=B8=EC=97=90=20=EC=96=B4?= =?UTF-8?q?=EC=9A=B0...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fc58a45e..56c7ae9f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,7 +35,7 @@ - + @@ -83,4 +83,4 @@ - \ No newline at end of file + From 4de7ce1cbe57ced9c127b2ca9a9cba39ad6a5551 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Thu, 28 Mar 2024 01:59:19 +0900 Subject: [PATCH 089/129] =?UTF-8?q?Boolti-193=20feat:=20=EA=B2=B0=EC=A0=9C?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C=20=ED=99=94=EB=A9=B4=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/component/MainButton.kt | 27 ++- .../boolti/presentation/component/ShowFeed.kt | 9 +- .../presentation/extension/LocalDateTime.kt | 12 ++ .../screen/payment/AccountTransferContent.kt | 4 +- .../screen/payment/PaymentCompleteScreen.kt | 170 ++++++++++++++++-- .../screen/payment/PaymentScreen.kt | 4 + .../screen/payment/TicketSummarySection.kt | 91 ++++++---- .../screen/ticketing/TicketingScreen.kt | 5 +- presentation/src/main/res/values/strings.xml | 13 +- 9 files changed, 264 insertions(+), 71 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/MainButton.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/MainButton.kt index 5829757d..8cecad52 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/MainButton.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/MainButton.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Color.Companion.White import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -49,6 +48,32 @@ fun MainButton( } } +@Composable +fun SecondaryButton( + modifier: Modifier = Modifier, + label: String = stringResource(id = R.string.btn_ok), + enabled: Boolean = true, + disabledContentColor: Color = Grey50, + onClick: () -> Unit, +) { + Button( + modifier = modifier.height(48.dp), + onClick = onClick, + enabled = enabled, + colors = ButtonDefaults.buttonColors( + containerColor = Grey80, + contentColor = MaterialTheme.colorScheme.onPrimary, + disabledContainerColor = Grey80, + disabledContentColor = disabledContentColor, + ), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(horizontal = marginHorizontal), + interactionSource = remember { MutableInteractionSource() }, + ) { + Text(text = label, style = MaterialTheme.typography.titleMedium) + } +} + @Preview @Composable fun MainButtonPreview() { diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt index f61f37e4..6195fb56 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/ShowFeed.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -27,6 +26,7 @@ import com.nexters.boolti.domain.model.Show import com.nexters.boolti.domain.model.ShowState import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.constants.posterRatio +import com.nexters.boolti.presentation.extension.showDateTimeString import com.nexters.boolti.presentation.extension.toPx import com.nexters.boolti.presentation.theme.Grey05 import com.nexters.boolti.presentation.theme.Grey20 @@ -35,7 +35,6 @@ import com.nexters.boolti.presentation.theme.Grey40 import com.nexters.boolti.presentation.theme.Grey80 import com.nexters.boolti.presentation.theme.Grey95 import com.nexters.boolti.presentation.theme.point1 -import java.time.format.DateTimeFormatter @Composable fun ShowFeed( @@ -97,12 +96,8 @@ fun ShowFeed( ) } - val daysOfWeek = stringArrayResource(id = R.array.days_of_week) - val indexOfDay = show.date.dayOfWeek.value - 1 - val formatter = - DateTimeFormatter.ofPattern("yyyy.MM.dd (${daysOfWeek[indexOfDay]}) HH:mm") Text( - text = show.date.format(formatter), + text = show.date.showDateTimeString, modifier = Modifier.padding(top = 12.dp), style = MaterialTheme.typography.bodySmall.copy(color = Grey30) ) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/extension/LocalDateTime.kt b/presentation/src/main/java/com/nexters/boolti/presentation/extension/LocalDateTime.kt index 077073e7..e4b5afd1 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/extension/LocalDateTime.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/extension/LocalDateTime.kt @@ -16,4 +16,16 @@ val LocalDateTime.dayOfWeekString: String dayOfWeekArr[dayOfWeek.value - 1] } +/** + * ## yyyy.MM.dd (A) HH:mm + * + * ex) 2024.01.20 (토) 18:00 + */ +val LocalDateTime.showDateTimeString: String + @Composable + get() = run { + val formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd ($dayOfWeekString) HH:mm") + format(formatter) + } + fun LocalDateTime.format(pattern: String): String = format(DateTimeFormatter.ofPattern(pattern)) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt index 76953c6e..c9ffa889 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt @@ -54,9 +54,7 @@ fun AccountTransferContent( Modifier.padding(top = 24.dp), poster = reservation.showImage, showName = reservation.showName, - ticketName = reservation.ticketName, - ticketCount = reservation.ticketCount, - price = reservation.totalAmountPrice, + showDate = LocalDateTime.now(), // TODO 서버에서 내려주는 값으로 대체해야 하지면 이 화면은 곧 사라질 예정. ) Box( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt index c737b9a3..abf2b0fa 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt @@ -1,44 +1,112 @@ package com.nexters.boolti.presentation.screen.payment +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Divider +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.domain.model.ReservationDetail +import com.nexters.boolti.domain.model.ReservationState import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.MainButton +import com.nexters.boolti.presentation.component.SecondaryButton +import com.nexters.boolti.presentation.theme.BooltiTheme +import com.nexters.boolti.presentation.theme.Grey15 +import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey85 import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.point4 +import java.time.LocalDateTime @Composable fun PaymentCompleteScreen( modifier: Modifier = Modifier, reservation: ReservationDetail, + navigateToReservation: (reservation: ReservationDetail) -> Unit = {}, + navigateToTicketDetail: (reservation: ReservationDetail) -> Unit = {}, ) { - Column( - modifier = modifier.padding(horizontal = marginHorizontal) - ) { - HeaderSection() - Divider( - modifier = Modifier.padding(top = 20.dp), - thickness = 1.dp, - color = Grey85, - ) - TicketSummarySection( - Modifier.padding(top = 24.dp), - poster = reservation.showImage, - showName = reservation.showName, - ticketName = reservation.ticketName, - ticketCount = reservation.ticketCount, - price = reservation.totalAmountPrice, - ) + val scrollState = rememberScrollState() + Box { + Column( + modifier = modifier + .padding(horizontal = marginHorizontal) + .verticalScroll(scrollState), + ) { + HeaderSection() + SectionDivider(modifier = Modifier.padding(top = 20.dp)) + + InfoRow( + modifier = Modifier.padding(top = 24.dp), + label = stringResource(R.string.reservation_number), value = reservation.csReservationId + ) + InfoRow( + label = stringResource(R.string.ticketing_ticket_holder_label), + value = slashFormat(reservation.ticketHolderName, reservation.ticketHolderPhoneNumber), + ) + if (reservation.isInviteTicket || reservation.totalAmountPrice > 0) { + InfoRow( + label = stringResource(R.string.depositor_info_label), + value = slashFormat(reservation.depositorName, reservation.depositorPhoneNumber), + ) + } + SectionDivider(modifier = Modifier.padding(top = 24.dp)) + + InfoRow( + modifier = Modifier.padding(top = 24.dp), + label = stringResource(R.string.payment_amount_label), + value = "${stringResource(R.string.unit_won, reservation.totalAmountPrice)} (카카오뱅크카드 / 일시불)", + ) + InfoRow( + label = stringResource(R.string.reservation_ticket_type), + value = slashFormat( + reservation.ticketName, + stringResource(R.string.ticket_count, reservation.ticketCount) + ), + ) + + TicketSummarySection( + Modifier + .fillMaxWidth() + .padding(top = 24.dp), + poster = reservation.showImage, + showName = reservation.showName, + showDate = LocalDateTime.now(), // TODO 서버에서 내려주는 공연 일시로 대체 필요!! + ) + } + + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = marginHorizontal) + .padding(bottom = 20.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + SecondaryButton(label = stringResource(R.string.show_reservation)) { + navigateToReservation(reservation) + } + MainButton(label = stringResource(R.string.show_ticket)) { + navigateToTicketDetail(reservation) + } + } } } +private fun slashFormat(s1: String, s2: String): String = String.format("%s / %s", s1, s2) + @Composable private fun HeaderSection() { Text( @@ -47,3 +115,69 @@ private fun HeaderSection() { style = point4, ) } + +@Composable +private fun InfoRow( + modifier: Modifier = Modifier, + label: String, + value: String, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.width(100.dp), + text = label, + style = MaterialTheme.typography.bodyLarge, + color = Grey30, + ) + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = value, + style = MaterialTheme.typography.bodyLarge, + color = Grey15, + ) + } +} + +@Composable +private fun SectionDivider( + modifier: Modifier = Modifier, +) = HorizontalDivider( + modifier = modifier, + thickness = 1.dp, + color = Grey85, +) + +@Preview +@Composable +private fun PaymentCompleteScreenPreview() { + BooltiTheme { + PaymentCompleteScreen( + reservation = ReservationDetail( + id = "eius", + showImage = "noster", + showName = "Mara King", + ticketName = "Juliet Greer", + isInviteTicket = false, + ticketCount = 6931, + bankName = "Corinne Leon", + accountNumber = "graece", + accountHolder = "reprimique", + salesEndDateTime = LocalDateTime.now(), + paymentType = PaymentType.UNDEFINED, + totalAmountPrice = 3473, + reservationState = ReservationState.REFUNDING, + completedDateTime = null, + ticketHolderName = "Cedric Butler", + ticketHolderPhoneNumber = "(453) 355-6682", + depositorName = "Dick Haley", + depositorPhoneNumber = "(869) 823-0418", + csReservationId = "mutat" + ), + navigateToReservation = {}, + navigateToTicketDetail = {}, + ) + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt index b1f14dde..7f6bb635 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt @@ -52,6 +52,10 @@ fun PaymentScreen( is PaymentState.Success -> { val reservation = (uiState as PaymentState.Success).reservationDetail when { + true -> PaymentCompleteScreen( + modifier = Modifier.padding(innerPadding), + reservation = reservation + ) reservation.totalAmountPrice == 0 || reservation.reservationState == ReservationState.RESERVED || reservation.isInviteTicket -> PaymentCompleteScreen( modifier = Modifier.padding(innerPadding), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt index 10686416..26d9b65b 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt @@ -6,7 +6,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -14,58 +17,76 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.extension.showDateTimeString +import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey05 import com.nexters.boolti.presentation.theme.Grey30 +import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.point1 +import java.time.LocalDateTime @Composable fun TicketSummarySection( modifier: Modifier = Modifier, poster: String, showName: String, - ticketName: String, - ticketCount: Int, - price: Int, + showDate: LocalDateTime, ) { - Row( + Card( modifier = modifier, - verticalAlignment = Alignment.CenterVertically, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + shape = RoundedCornerShape(4.dp), ) { - AsyncImage( - model = poster, - contentDescription = stringResource(R.string.description_poster), - modifier = Modifier - .size(width = 70.dp, height = 98.dp) - .clip(RoundedCornerShape(4.dp)) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.secondaryContainer, - shape = RoundedCornerShape(4.dp), - ), - contentScale = ContentScale.Crop, - ) - Column( - modifier = Modifier.padding(start = 16.dp), + Row( + modifier = Modifier.padding(vertical = 16.dp, horizontal = marginHorizontal), + verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = showName, - style = point1, - color = Grey05, + AsyncImage( + model = poster, + contentDescription = stringResource(R.string.description_poster), + modifier = Modifier + .size(width = 70.dp, height = 98.dp) + .clip(RoundedCornerShape(4.dp)) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.secondaryContainer, + shape = RoundedCornerShape(4.dp), + ), + contentScale = ContentScale.Crop, ) - Text( - modifier = Modifier.padding(top = 4.dp), - text = "$ticketName / ${stringResource(R.string.ticket_count, ticketCount)}", - style = MaterialTheme.typography.labelMedium, - color = Grey30, - ) - Text( - modifier = Modifier.padding(top = 4.dp), - text = stringResource(R.string.unit_won, price), - style = MaterialTheme.typography.labelMedium, - color = Grey30, + Column( + modifier = Modifier.padding(start = 16.dp), + ) { + Text( + text = showName, + style = point1, + color = Grey05, + ) + Text( + modifier = Modifier.padding(top = 4.dp), + text = showDate.showDateTimeString, + color = Grey30, + style = MaterialTheme.typography.bodySmall, + ) + } + } + } +} + +@Preview +@Composable +private fun TicketSummaryPreview() { + BooltiTheme { + Surface { + TicketSummarySection( + modifier = Modifier.padding(16.dp), + poster = "", + showName = "2024 TOGETHER LUCKY CLUB", + showDate = LocalDateTime.now(), ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index 84abc0aa..8773dfd5 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -71,9 +71,8 @@ import com.nexters.boolti.presentation.component.BTTextField import com.nexters.boolti.presentation.component.BusinessInformation import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.component.ToastSnackbarHost -import com.nexters.boolti.presentation.extension.dayOfWeekString import com.nexters.boolti.presentation.extension.filterToPhoneNumber -import com.nexters.boolti.presentation.extension.format +import com.nexters.boolti.presentation.extension.showDateTimeString import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Error import com.nexters.boolti.presentation.theme.Grey05 @@ -280,7 +279,7 @@ private fun Header( modifier = Modifier.padding(bottom = 4.dp) ) Text( - text = showDate.format("yyyy.MM.dd (${showDate.dayOfWeekString}) HH:mm"), + text = showDate.showDateTimeString, style = MaterialTheme.typography.bodySmall, color = Grey30, ) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index be1bc06c..8bb30334 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -52,7 +52,7 @@ 다음 예매자 - 입금자 + 결제자 티켓 초청 티켓 @@ -121,7 +121,7 @@ 1인 %d매 결제하기 예매자 정보 - 입금자 정보 + 결제자 정보 티켓 정보 초청 코드 결제 수단 @@ -138,10 +138,11 @@ 이미 사용된 초청 코드입니다 초청 코드가 올바르지 않아요 초청 코드를 입력해 주세요 - 예매자와 입금자가 같아요 + 예매자와 결제자가 같아요 티켓 종류 티켓 매수 총 결제 금액 + 결제 금액 다음 페이지에서 계좌 번호를 안내해 드릴게요 지금은 계좌 이체로만 결제할 수 있어요 %,d원 결제하기 @@ -168,7 +169,7 @@ 계좌번호 예금주 입금 마감일 - 결제가 완료되었어요 + 결제를 완료했어요 예매자 정보 확인 후 티켓이 발권됩니다. @@ -209,6 +210,10 @@ 발권 전 %d매 "%s / %d매" + 주문 번호 + 주문 티켓 + 티켓 보기 + 예매 내역보기 환불 요청하기 From 643b626344c1e5c9e65fb40a4afec9e1046e342e Mon Sep 17 00:00:00 2001 From: mangbaam Date: Fri, 29 Mar 2024 10:50:22 +0900 Subject: [PATCH 090/129] =?UTF-8?q?Boolti-193=20feat:=20=EA=B2=B0=EC=A0=9C?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C=20=ED=99=94=EB=A9=B4=20=EA=B3=B5=EC=97=B0?= =?UTF-8?q?=20=EB=82=A0=EC=A7=9C=20=EC=A0=95=EB=B3=B4=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/network/response/ReservationDetailResponse.kt | 2 ++ .../com/nexters/boolti/domain/model/ReservationDetail.kt | 1 + .../presentation/screen/payment/PaymentCompleteScreen.kt | 8 ++++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt b/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt index 3ab1b0be..8ae71aba 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/ReservationDetailResponse.kt @@ -11,6 +11,7 @@ internal data class ReservationDetailResponse( val reservationId: String, val showImg: String, val showName: String, + val showDate: String, val salesTicketName: String, val salesTicketType: String, val ticketCount: Int, @@ -33,6 +34,7 @@ internal data class ReservationDetailResponse( id = reservationId, showImage = showImg, showName = showName, + showDate = showDate.toLocalDateTime(), ticketName = salesTicketName, isInviteTicket = salesTicketType == "INVITE", ticketCount = ticketCount, diff --git a/domain/src/main/java/com/nexters/boolti/domain/model/ReservationDetail.kt b/domain/src/main/java/com/nexters/boolti/domain/model/ReservationDetail.kt index 1ef6b4b1..1325e773 100644 --- a/domain/src/main/java/com/nexters/boolti/domain/model/ReservationDetail.kt +++ b/domain/src/main/java/com/nexters/boolti/domain/model/ReservationDetail.kt @@ -6,6 +6,7 @@ data class ReservationDetail( val id: String, val showImage: String, val showName: String, + val showDate: LocalDateTime, val ticketName: String, val isInviteTicket: Boolean, val ticketCount: Int, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt index abf2b0fa..19f36c11 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt @@ -68,7 +68,10 @@ fun PaymentCompleteScreen( InfoRow( modifier = Modifier.padding(top = 24.dp), label = stringResource(R.string.payment_amount_label), - value = "${stringResource(R.string.unit_won, reservation.totalAmountPrice)} (카카오뱅크카드 / 일시불)", + value = stringResource( + R.string.unit_won, + reservation.totalAmountPrice + ), // TODO (카카오뱅크카드 / 일시불) 형태의 정보 추가 ) InfoRow( label = stringResource(R.string.reservation_ticket_type), @@ -84,7 +87,7 @@ fun PaymentCompleteScreen( .padding(top = 24.dp), poster = reservation.showImage, showName = reservation.showName, - showDate = LocalDateTime.now(), // TODO 서버에서 내려주는 공연 일시로 대체 필요!! + showDate = reservation.showDate, ) } @@ -159,6 +162,7 @@ private fun PaymentCompleteScreenPreview() { id = "eius", showImage = "noster", showName = "Mara King", + showDate = LocalDateTime.now(), ticketName = "Juliet Greer", isInviteTicket = false, ticketCount = 6931, From 50c8b504c087f9e28620776e52616036f6421d5d Mon Sep 17 00:00:00 2001 From: mangbaam Date: Fri, 29 Mar 2024 11:05:58 +0900 Subject: [PATCH 091/129] =?UTF-8?q?Boolti-193=20feat:=20=EA=B2=B0=EC=A0=9C?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C=20=ED=9B=84=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EA=B5=AC=ED=98=84=20(=EA=B3=B5=EC=97=B0?= =?UTF-8?q?=20=EC=83=81=EC=84=B8,=20=EC=98=88=EB=A7=A4=20=EC=83=81?= =?UTF-8?q?=EC=84=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/nexters/boolti/presentation/screen/Main.kt | 2 +- .../screen/payment/PaymentNavigation.kt | 2 ++ .../presentation/screen/payment/PaymentScreen.kt | 13 ++++++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index c8f571ef..f8feb925 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -156,7 +156,7 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: inclusive = true, ) }, - navigateToHome = navController::navigateToHome + navigateToHome = navController::navigateToHome, ) BusinessScreen(popBackStack = navController::popBackStack) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentNavigation.kt index 632e7e8d..30b635f2 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentNavigation.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentNavigation.kt @@ -25,6 +25,8 @@ fun NavGraphBuilder.PaymentScreen( navigateTo("${MainDestination.ShowDetail.route}/$showId") } ?: popBackStack() }, + navigateToReservation = { reservation -> navigateTo("${MainDestination.ReservationDetail.route}/${reservation.id}") }, + navigateToTicketDetail = { reservation -> navigateTo("${MainDestination.TicketDetail.route}/${reservation.id}") }, ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt index 7f6bb635..217a9c0c 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt @@ -27,6 +27,8 @@ import kotlinx.coroutines.launch fun PaymentScreen( onClickHome: () -> Unit, onClickClose: () -> Unit, + navigateToReservation: (reservation: ReservationDetail) -> Unit, + navigateToTicketDetail: (reservation: ReservationDetail) -> Unit, viewModel: PaymentViewModel = hiltViewModel(), ) { val snackbarHostState = remember { SnackbarHostState() } @@ -51,15 +53,20 @@ fun PaymentScreen( is PaymentState.Loading -> Unit is PaymentState.Success -> { val reservation = (uiState as PaymentState.Success).reservationDetail - when { + when { // TODO 조건 정리 true -> PaymentCompleteScreen( modifier = Modifier.padding(innerPadding), - reservation = reservation + reservation = reservation, + navigateToReservation = navigateToReservation, + navigateToTicketDetail = navigateToTicketDetail, ) + reservation.totalAmountPrice == 0 || reservation.reservationState == ReservationState.RESERVED || reservation.isInviteTicket -> PaymentCompleteScreen( modifier = Modifier.padding(innerPadding), - reservation = reservation + reservation = reservation, + navigateToReservation = navigateToReservation, + navigateToTicketDetail = navigateToTicketDetail, ) else -> ProgressPayment( From c0703f50e6bda0805f57fe1c2aeb73f495a53852 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Fri, 29 Mar 2024 11:26:20 +0900 Subject: [PATCH 092/129] =?UTF-8?q?Boolti-193=20feat:=20=ED=8B=B0=EC=BC=93?= =?UTF-8?q?=EC=97=90=20csTicketId=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/data/network/response/TicketDto.kt | 8 ++++++ .../com/nexters/boolti/domain/model/Ticket.kt | 2 ++ .../screen/ticket/TicketContent.kt | 27 ++++++++++++------- .../ticket/detail/TicketDetailScreen.kt | 24 +++++++++++------ 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/data/src/main/java/com/nexters/boolti/data/network/response/TicketDto.kt b/data/src/main/java/com/nexters/boolti/data/network/response/TicketDto.kt index 5fd738ed..05529d50 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/response/TicketDto.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/response/TicketDto.kt @@ -18,6 +18,8 @@ internal data class TicketDto( val ticketName: String, val entryCode: String, val usedAt: String? = null, // 사용되지 않았을 때 null + val ticketCreatedAt: String, + val csTicketId: String, ) { fun toDomain(): Ticket = Ticket( userId = userId, @@ -30,6 +32,8 @@ internal data class TicketDto( placeName = placeName, entryCode = entryCode, usedAt = usedAt?.toLocalDateTime(), + csReservationId = "", // TODO 이거 빈 값으로 둬도 되는지 확인 필요 + csTicketId = csTicketId, ) } @@ -54,6 +58,8 @@ data class TicketDetailDto( val usedAt: String? = null, val hostName: String = "", val hostPhoneNumber: String = "", + val csReservationId: String = "", + val csTicketId: String = "", ) { fun toDomain(): Ticket = Ticket( userId = userId, @@ -75,5 +81,7 @@ data class TicketDetailDto( usedAt = usedAt?.toLocalDateTime(), hostName = hostName, hostPhoneNumber = hostPhoneNumber, + csReservationId = csReservationId, + csTicketId = csTicketId, ) } diff --git a/domain/src/main/java/com/nexters/boolti/domain/model/Ticket.kt b/domain/src/main/java/com/nexters/boolti/domain/model/Ticket.kt index 3f5ce19c..ed87dcd1 100644 --- a/domain/src/main/java/com/nexters/boolti/domain/model/Ticket.kt +++ b/domain/src/main/java/com/nexters/boolti/domain/model/Ticket.kt @@ -43,6 +43,8 @@ data class Ticket( val usedAt: LocalDateTime? = null, val hostName: String = "", val hostPhoneNumber: String = "", + val csReservationId: String = "", + val csTicketId: String = "", ) { val ticketState: TicketState get() = run { diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt index f41356be..14bf45d8 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -39,6 +38,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -130,7 +130,7 @@ fun TicketContent( ), ) Column { - Title(ticket.ticketName, 1) // TODO 개수 정보 생기면 업데이트 + Title(ticket.ticketName, ticket.csTicketId) AsyncImage( modifier = Modifier .fillMaxWidth() @@ -177,25 +177,32 @@ fun TicketContent( @Composable private fun Title( ticketName: String = "", - count: Int = 0, + csTicketId: String = "", ) { Row( modifier = Modifier .background(White.copy(alpha = 0.3f)) + .alpha(0.65f) .padding(horizontal = 20.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { - Text( - modifier = Modifier.weight(1f), - text = if (count > 1) stringResource(R.string.ticket_title, ticketName, count) else ticketName, - style = MaterialTheme.typography.bodySmall, - color = Grey80, - ) Image( + modifier = Modifier.padding(end = 4.dp), painter = painterResource(R.drawable.ic_logo), colorFilter = ColorFilter.tint(Grey80), contentDescription = null, ) + Text( + modifier = Modifier.weight(1f), + text = ticketName, + style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.SemiBold), + color = Grey80, + ) + Text( + text = csTicketId, + style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.SemiBold), + color = Grey80, + ) } } @@ -291,7 +298,7 @@ private fun TicketQr( .clip(RoundedCornerShape(4.dp)) .size(70.dp) .background( - brush = SolidColor(Color.Black), + brush = SolidColor(Black), alpha = 0.8f, ) ) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt index d50d3579..713318a7 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt @@ -69,6 +69,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -77,7 +78,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import coil.compose.AsyncImage @@ -262,7 +262,7 @@ private fun TicketDetailScreen( coordinates.size.height.toFloat() } ) { - Title(ticketName = ticket.ticketName) + Title(ticketName = ticket.ticketName, csTicketId = ticket.csTicketId) AsyncImage( modifier = Modifier @@ -401,23 +401,31 @@ private fun TicketDetailToolbar( @Composable private fun Title( ticketName: String = "", + csTicketId: String = "", ) { Row( modifier = Modifier .background(White.copy(alpha = 0.3f)) + .alpha(0.65f) .padding(horizontal = 20.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { + Image( + modifier = Modifier.padding(end = 4.dp), + painter = painterResource(R.drawable.ic_logo), + colorFilter = ColorFilter.tint(Grey80), + contentDescription = null, + ) Text( modifier = Modifier.weight(1f), text = ticketName, - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.SemiBold), color = Grey80, ) - Image( - painter = painterResource(R.drawable.ic_logo), - colorFilter = ColorFilter.tint(Grey80), - contentDescription = null, + Text( + text = csTicketId, + style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.SemiBold), + color = Grey80, ) } } @@ -502,7 +510,7 @@ private fun TicketQr( modifier = Modifier .padding(vertical = 8.dp) .clip(RoundedCornerShape(4.dp)) - .background(Color.White) + .background(White) .padding(2.dp) .clickable { if (ticketState == TicketState.Ready) onClickQr(entryCode) From abc3a5f7244fa5837a3697aac3618a19d37adbf6 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Fri, 29 Mar 2024 13:56:20 +0900 Subject: [PATCH 093/129] =?UTF-8?q?Boolti-193=20feat:=20=EC=98=88=EB=A7=A4?= =?UTF-8?q?=20-=20=EC=A3=BC=EB=AC=B8=EB=82=B4=EC=9A=A9=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=EA=B2=B0=EC=A0=9C=20=EB=8F=99=EC=9D=98?= =?UTF-8?q?=20=EC=84=B9=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/ticketing/TicketingScreen.kt | 134 ++++++++++++++++++ .../screen/ticketing/TicketingState.kt | 32 ++++- .../screen/ticketing/TicketingViewModel.kt | 9 +- presentation/src/main/res/values/strings.xml | 5 + 4 files changed, 177 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index 8773dfd5..66dc77c1 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -55,10 +55,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -76,6 +78,7 @@ import com.nexters.boolti.presentation.extension.showDateTimeString import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Error import com.nexters.boolti.presentation.theme.Grey05 +import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey20 import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey50 @@ -192,6 +195,16 @@ fun TicketingScreen( if (!uiState.isInviteTicket && uiState.totalPrice > 0) PaymentSection(scope, snackbarHostState) // 결제 수단 if (!uiState.isInviteTicket) RefundPolicySection(uiState.refundPolicy) // 취소/환불 규정 + // 주문내용 확인 및 결제 동의 + OrderAgreementSection( + totalAgreed = uiState.orderAgreed, + agreement = uiState.orderAgreement, + agreementLabels = uiState.orderAgreementInfos, + onClickTotalAgree = viewModel::toggleAgreement, + onClickAgree = viewModel::toggleAgreement, + onClickShow = {}, // TODO 기획 확정되면 구현 + ) + // 사업자 정보 BusinessInformation( modifier = Modifier.fillMaxWidth(), @@ -594,6 +607,110 @@ private fun TicketHolderSection( } } +@Composable +private fun OrderAgreementSection( + totalAgreed: Boolean, + agreementLabels: List, + agreement: List, + onClickTotalAgree: () -> Unit, + onClickAgree: (index: Int) -> Unit, + onClickShow: (index: Int) -> Unit, +) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.surface) + .padding(20.dp), + ) { + Row( + modifier = Modifier.clickable(onClick = onClickTotalAgree), + verticalAlignment = Alignment.CenterVertically, + ) { + if (totalAgreed) { + Icon( + painter = painterResource(R.drawable.ic_checkbox_selected), + tint = Grey05, + contentDescription = null, + modifier = Modifier + .size(24.dp) + .padding(3.dp) + .background(MaterialTheme.colorScheme.primary, shape = CircleShape), + ) + } else { + Icon( + painter = painterResource(R.drawable.ic_checkbox_18), + tint = Grey50, + contentDescription = null, + ) + } + Text( + text = stringResource(R.string.order_agreement_label), + color = Grey10, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = 4.dp) + ) + } + + Spacer(modifier = Modifier.size(16.dp)) + agreementLabels.forEachIndexed { index, labelRes -> + OrderAgreementItem( + modifier = Modifier.padding(top = 4.dp), + index = index, + agreed = agreement[index], + label = stringResource(labelRes), + onClickAgree = onClickAgree, + onClickShow = onClickShow, + ) + } + } +} + +@Composable +private fun OrderAgreementItem( + modifier: Modifier = Modifier, + index: Int, + agreed: Boolean, + label: String, + onClickAgree: (index: Int) -> Unit, + onClickShow: (index: Int) -> Unit, +) { + Row( + modifier = modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.clickable { onClickAgree(index) }, + ) { + Icon( + modifier = Modifier.padding(end = 4.dp), + painter = painterResource(R.drawable.ic_check), contentDescription = label, + tint = if (agreed) MaterialTheme.colorScheme.primary else Grey50, + ) + Text( + text = label, + style = MaterialTheme.typography.bodySmall, + color = Grey50, + ) + } + Spacer(modifier = Modifier.weight(1f)) + ShowButton { onClickShow(index) } + } +} + +@Composable +private fun ShowButton( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Text( + modifier = modifier.clickable(onClick = onClick), + text = stringResource(R.string.show), + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.SemiBold, + textDecoration = TextDecoration.Underline, + ), + color = Grey50, + ) +} + @Composable private fun Section( modifier: Modifier = Modifier, @@ -695,3 +812,20 @@ private fun TicketingDetailScreenPreview() { } } } + +@Preview +@Composable +private fun OrderAgreementItemPreview() { + BooltiTheme { + Surface { + var agreed by remember { mutableStateOf(false) } + OrderAgreementItem( + index = 0, + agreed = agreed, + label = "test", + onClickAgree = { agreed = !agreed }, + onClickShow = {}, + ) + } + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt index 75e79dd6..c4a8b1c9 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt @@ -2,6 +2,7 @@ package com.nexters.boolti.presentation.screen.ticketing import com.nexters.boolti.domain.model.InviteCodeStatus import com.nexters.boolti.domain.model.PaymentType +import com.nexters.boolti.presentation.R import java.time.LocalDateTime data class TicketingState( @@ -22,16 +23,43 @@ data class TicketingState( val depositorContact: String = "", val inviteCode: String = "", val refundPolicy: List = emptyList(), + val orderAgreement: List = listOf(false, false, false), ) { + val orderAgreementInfos = listOf( + R.string.order_agreement_privacy_collection, + R.string.order_agreement_privacy_offer, + R.string.order_agreement_payment_agency, + ) + + val orderAgreed: Boolean + get() = orderAgreement.none { !it } + val reservationButtonEnabled: Boolean get() = if (isInviteTicket) { - reservationName.isNotBlank() && + orderAgreed && + reservationName.isNotBlank() && reservationContact.isNotBlank() && inviteCodeStatus is InviteCodeStatus.Valid } else { - reservationName.isNotBlank() && + orderAgreed && + reservationName.isNotBlank() && reservationContact.isNotBlank() && (isSameContactInfo || depositorName.isNotBlank()) && (isSameContactInfo || depositorContact.isNotBlank()) } + + fun toggleAgreement(index: Int): TicketingState { + val updated = orderAgreement.toMutableList().apply { + set(index, !orderAgreement[index]) + } + return copy(orderAgreement = updated) + } + + fun toggleAgreement(): TicketingState { + return if (orderAgreed) { + copy(orderAgreement = orderAgreement.map { false }) + } else { + copy(orderAgreement = orderAgreement.map { true }) + } + } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt index ef7d56d7..eea72878 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingViewModel.kt @@ -1,7 +1,6 @@ package com.nexters.boolti.presentation.screen.ticketing import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.model.InviteCodeStatus import com.nexters.boolti.domain.repository.TicketingRepository @@ -171,6 +170,14 @@ class TicketingViewModel @Inject constructor( _uiState.update { it.copy(inviteCode = code, inviteCodeStatus = InviteCodeStatus.Default) } } + fun toggleAgreement(index: Int) { + _uiState.update { it.toggleAgreement(index) } + } + + fun toggleAgreement() { + _uiState.update { it.toggleAgreement() } + } + private fun event(event: TicketingEvent) { viewModelScope.launch { _event.send(event) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 8bb30334..1789ac93 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -50,6 +50,7 @@ 알 수 없는 에러가 발생했습니다 복사 다음 + 보기 예매자 결제자 @@ -150,6 +151,10 @@ 결제 정보를 확인해주세요 공연장 주소가 복사되었어요 %s (%s) + 주문내용 확인 및 결제 동의 + [필수] 개인정보 수집・이용 동의 + [필수] 개인정보 제 3자 정보 제공 동의 + [필수] 결제대행 서비스 이용약관 동의 입장 코드 From 8df39ee5b7af16e9ea5ec865ffdbc49c5a3b84cf Mon Sep 17 00:00:00 2001 From: mangbaam Date: Fri, 29 Mar 2024 21:31:43 +0900 Subject: [PATCH 094/129] =?UTF-8?q?Boolti-193=20style:=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=EC=99=84=EB=A3=8C=20=ED=99=94=EB=A9=B4=20UI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/payment/PaymentCompleteScreen.kt | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt index 19f36c11..aa1f8617 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -40,9 +41,11 @@ fun PaymentCompleteScreen( navigateToTicketDetail: (reservation: ReservationDetail) -> Unit = {}, ) { val scrollState = rememberScrollState() - Box { + Box( + modifier = modifier.fillMaxSize(), + ) { Column( - modifier = modifier + modifier = Modifier .padding(horizontal = marginHorizontal) .verticalScroll(scrollState), ) { @@ -54,11 +57,13 @@ fun PaymentCompleteScreen( label = stringResource(R.string.reservation_number), value = reservation.csReservationId ) InfoRow( + modifier = Modifier.padding(top = 16.dp), label = stringResource(R.string.ticketing_ticket_holder_label), value = slashFormat(reservation.ticketHolderName, reservation.ticketHolderPhoneNumber), ) if (reservation.isInviteTicket || reservation.totalAmountPrice > 0) { InfoRow( + modifier = Modifier.padding(top = 16.dp), label = stringResource(R.string.depositor_info_label), value = slashFormat(reservation.depositorName, reservation.depositorPhoneNumber), ) @@ -74,6 +79,7 @@ fun PaymentCompleteScreen( ), // TODO (카카오뱅크카드 / 일시불) 형태의 정보 추가 ) InfoRow( + modifier = Modifier.padding(top = 16.dp), label = stringResource(R.string.reservation_ticket_type), value = slashFormat( reservation.ticketName, @@ -93,15 +99,22 @@ fun PaymentCompleteScreen( Row( modifier = Modifier + .fillMaxWidth() .align(Alignment.BottomCenter) .padding(horizontal = marginHorizontal) .padding(bottom = 20.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - SecondaryButton(label = stringResource(R.string.show_reservation)) { + SecondaryButton( + modifier = Modifier.weight(1f), + label = stringResource(R.string.show_reservation), + ) { navigateToReservation(reservation) } - MainButton(label = stringResource(R.string.show_ticket)) { + MainButton( + modifier = Modifier.weight(1f), + label = stringResource(R.string.show_ticket), + ) { navigateToTicketDetail(reservation) } } From 54522e9e623830b8aa07e330848b916589cbe758 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 31 Mar 2024 00:36:35 +0900 Subject: [PATCH 095/129] =?UTF-8?q?Boolti-193=20feat:=20=EC=8A=A4=ED=8E=99?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=B2=98=EB=A6=AC=20-=20=EC=98=88?= =?UTF-8?q?=EB=A7=A4=20=ED=99=94=EB=A9=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/ticketing/TicketingScreen.kt | 30 ++++++++++++++----- .../screen/ticketing/TicketingState.kt | 4 +-- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index 66dc77c1..1d390346 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -82,9 +82,11 @@ import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey20 import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey50 +import com.nexters.boolti.presentation.theme.Grey70 import com.nexters.boolti.presentation.theme.Grey80 import com.nexters.boolti.presentation.theme.Grey90 import com.nexters.boolti.presentation.theme.Success +import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.point2 import com.nexters.boolti.presentation.util.PhoneNumberVisualTransformation import kotlinx.coroutines.CoroutineScope @@ -105,6 +107,7 @@ fun TicketingScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() var showConfirmDialog by remember { mutableStateOf(false) } + val specOut = false // TODO 다음 버전(페이 들어오는 버전)에서 추가될 기능 마킹 LaunchedEffect(viewModel.event) { viewModel.event.collect { @@ -195,16 +198,27 @@ fun TicketingScreen( if (!uiState.isInviteTicket && uiState.totalPrice > 0) PaymentSection(scope, snackbarHostState) // 결제 수단 if (!uiState.isInviteTicket) RefundPolicySection(uiState.refundPolicy) // 취소/환불 규정 - // 주문내용 확인 및 결제 동의 - OrderAgreementSection( - totalAgreed = uiState.orderAgreed, - agreement = uiState.orderAgreement, - agreementLabels = uiState.orderAgreementInfos, - onClickTotalAgree = viewModel::toggleAgreement, - onClickAgree = viewModel::toggleAgreement, - onClickShow = {}, // TODO 기획 확정되면 구현 + Text( + modifier = Modifier + .padding(top = 24.dp, bottom = 20.dp) + .padding(horizontal = marginHorizontal), + text = stringResource(R.string.business_responsibility), + style = MaterialTheme.typography.labelMedium, + color = Grey70, ) + if (specOut) { + // 주문내용 확인 및 결제 동의 + OrderAgreementSection( + totalAgreed = uiState.orderAgreed, + agreement = uiState.orderAgreement, + agreementLabels = uiState.orderAgreementInfos, + onClickTotalAgree = viewModel::toggleAgreement, + onClickAgree = viewModel::toggleAgreement, + onClickShow = {}, // TODO 기획 확정되면 구현 + ) + } + // 사업자 정보 BusinessInformation( modifier = Modifier.fillMaxWidth(), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt index c4a8b1c9..2066d382 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt @@ -36,12 +36,12 @@ data class TicketingState( val reservationButtonEnabled: Boolean get() = if (isInviteTicket) { - orderAgreed && +// orderAgreed && reservationName.isNotBlank() && reservationContact.isNotBlank() && inviteCodeStatus is InviteCodeStatus.Valid } else { - orderAgreed && +// orderAgreed && reservationName.isNotBlank() && reservationContact.isNotBlank() && (isSameContactInfo || depositorName.isNotBlank()) && From 558530079cabe6888f1ec1b273dd8713b5675f40 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 31 Mar 2024 18:09:44 +0900 Subject: [PATCH 096/129] =?UTF-8?q?Boolti-193=20feat:=20=EA=B3=84=EC=A2=8C?= =?UTF-8?q?=EC=9D=B4=EC=B2=B4=20=ED=99=94=EB=A9=B4=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/payment/AccountTransferContent.kt | 18 +++-- .../screen/payment/PaymentScreen.kt | 13 +--- .../screen/payment/TicketSummarySection.kt | 75 +++++++++++++++++++ presentation/src/main/res/values/strings.xml | 2 + 4 files changed, 93 insertions(+), 15 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt index c9ffa889..92b96b1b 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt @@ -10,7 +10,7 @@ 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.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -21,6 +21,7 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.extension.format @@ -34,6 +35,7 @@ import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.point4 import com.nexters.boolti.presentation.theme.subTextPadding import java.time.LocalDateTime +import com.nexters.boolti.presentation.screen.payment.LegacyTicketSummarySection as LegacyTicketSummarySection1 @Composable fun AccountTransferContent( @@ -45,16 +47,20 @@ fun AccountTransferContent( modifier = modifier.padding(horizontal = marginHorizontal), ) { HeaderSection(reservation.totalAmountPrice, reservation.salesEndDateTime) - Divider( + + HorizontalDivider( modifier = Modifier.padding(top = 20.dp), thickness = 1.dp, - color = Grey85, + color = Grey85 ) - TicketSummarySection( - Modifier.padding(top = 24.dp), + + LegacyTicketSummarySection1( + modifier = Modifier.padding(top = 24.dp), poster = reservation.showImage, showName = reservation.showName, - showDate = LocalDateTime.now(), // TODO 서버에서 내려주는 값으로 대체해야 하지면 이 화면은 곧 사라질 예정. + paymentType = PaymentType.ACCOUNT_TRANSFER, + ticketCount = reservation.ticketCount, + totalPrice = reservation.totalAmountPrice, ) Box( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt index 217a9c0c..a96639f6 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt @@ -53,15 +53,10 @@ fun PaymentScreen( is PaymentState.Loading -> Unit is PaymentState.Success -> { val reservation = (uiState as PaymentState.Success).reservationDetail - when { // TODO 조건 정리 - true -> PaymentCompleteScreen( - modifier = Modifier.padding(innerPadding), - reservation = reservation, - navigateToReservation = navigateToReservation, - navigateToTicketDetail = navigateToTicketDetail, - ) - - reservation.totalAmountPrice == 0 || reservation.reservationState == ReservationState.RESERVED || reservation.isInviteTicket -> + when { + reservation.totalAmountPrice == 0 || + reservation.reservationState == ReservationState.RESERVED || + reservation.isInviteTicket -> PaymentCompleteScreen( modifier = Modifier.padding(innerPadding), reservation = reservation, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt index 26d9b65b..bdb6c598 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.extension.showDateTimeString import com.nexters.boolti.presentation.theme.BooltiTheme @@ -77,6 +78,63 @@ fun TicketSummarySection( } } +@Composable +fun LegacyTicketSummarySection( + modifier: Modifier = Modifier, + poster: String, + showName: String, + paymentType: PaymentType, + ticketCount: Int, + totalPrice: Int, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + AsyncImage( + model = poster, + contentDescription = stringResource(R.string.description_poster), + modifier = Modifier + .size(width = 70.dp, height = 98.dp) + .clip(RoundedCornerShape(4.dp)) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.secondaryContainer, + shape = RoundedCornerShape(4.dp), + ), + contentScale = ContentScale.Crop, + ) + Column( + modifier = Modifier.padding(start = 16.dp) + ) { + Text( + text = showName, + style = point1, + color = Grey05, + ) + val payment = when (paymentType) { + PaymentType.ACCOUNT_TRANSFER -> R.string.payment_account_transfer + PaymentType.CARD -> R.string.payment_card + PaymentType.UNDEFINED -> R.string.blank + }.let { stringResource(id = it) } + + Text( + modifier = Modifier.padding(top = 4.dp), + text = String.format("%s / %s", payment, ticketCount), + color = Grey30, + style = MaterialTheme.typography.bodySmall, + ) + + Text( + modifier = Modifier.padding(top = 4.dp), + text = stringResource(id = R.string.unit_won, totalPrice), + color = Grey30, + style = MaterialTheme.typography.bodySmall, + ) + } + } +} + @Preview @Composable private fun TicketSummaryPreview() { @@ -91,3 +149,20 @@ private fun TicketSummaryPreview() { } } } + +@Preview +@Composable +private fun LegacyTicketSummaryPreview() { + BooltiTheme { + Surface { + LegacyTicketSummarySection( + modifier = Modifier.padding(24.dp), + poster = "", + showName = "2024 TOGETHER LUCKY CLUB", + paymentType = PaymentType.ACCOUNT_TRANSFER, + ticketCount = 10, + totalPrice = 30000, + ) + } + } +} diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 1789ac93..b5d3db38 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -3,6 +3,8 @@ 티켓 마이 + + 뒤로 가기 포스터 사진 닫기 From d409cb0d1f95fc98637ac6d75054ddb2f8668e6e Mon Sep 17 00:00:00 2001 From: mangbaam Date: Mon, 1 Apr 2024 22:30:48 +0900 Subject: [PATCH 097/129] =?UTF-8?q?Boolti-193=20feat:=20=EC=98=88=EB=A7=A4?= =?UTF-8?q?=ED=95=9C=20=EA=B3=B5=EC=97=B0=20=EC=98=88=EB=A7=A4=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/show/ShowDetailScreen.kt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index 25e31712..e38a5e29 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -177,7 +177,6 @@ fun ShowDetailScreen( ) ShowDetailCtaButton( showState = uiState.showDetail.state, - purchased = uiState.showDetail.isReserved, onClick = { scope.launch { if (isLoggedIn == true) { @@ -497,24 +496,16 @@ private fun SectionContent( @Composable fun ShowDetailCtaButton( onClick: () -> Unit, - purchased: Boolean, showState: ShowState, modifier: Modifier = Modifier, ) { - val enabled = showState is ShowState.TicketingInProgress && !purchased + val enabled = showState is ShowState.TicketingInProgress val text = when (showState) { is ShowState.WaitingTicketing -> stringResource( id = R.string.ticketing_button_upcoming_ticket, showState.dDay ) - ShowState.TicketingInProgress -> { - if (purchased) { - stringResource(id = R.string.ticketing_button_purchased_ticket) - } else { - stringResource(id = R.string.ticketing_button_label) - } - } - + ShowState.TicketingInProgress -> stringResource(id = R.string.ticketing_button_label) ShowState.ClosedTicketing -> stringResource(id = R.string.ticketing_button_closed_ticket) ShowState.FinishedShow -> stringResource(id = R.string.ticketing_button_finished_show) } From 470d803a91511b515d89236a7f24e83c89835e0d Mon Sep 17 00:00:00 2001 From: mangbaam Date: Tue, 2 Apr 2024 22:49:57 +0900 Subject: [PATCH 098/129] =?UTF-8?q?Boolti-193=20feat:=200=EC=9B=90=20?= =?UTF-8?q?=ED=8B=B0=EC=BC=93=20=EC=98=88=EB=A7=A4=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20=EB=B0=8F=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/TicketingConfirmDialog.kt | 2 +- .../screen/ticketing/TicketingState.kt | 26 +++++++++++-------- presentation/src/main/res/values/strings.xml | 2 +- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt index e466c662..0042495e 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt @@ -65,7 +65,7 @@ fun TicketingConfirmDialog( value2 = reservationContact.toContactFormat(), ) // 입금자 - if (!isInviteTicket) { + if (!isInviteTicket && totalPrice > 0) { InfoRow( modifier = Modifier.padding(top = 16.dp), label = stringResource(R.string.depositor), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt index 2066d382..05b40266 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingState.kt @@ -35,17 +35,21 @@ data class TicketingState( get() = orderAgreement.none { !it } val reservationButtonEnabled: Boolean - get() = if (isInviteTicket) { -// orderAgreed && - reservationName.isNotBlank() && - reservationContact.isNotBlank() && - inviteCodeStatus is InviteCodeStatus.Valid - } else { -// orderAgreed && - reservationName.isNotBlank() && - reservationContact.isNotBlank() && - (isSameContactInfo || depositorName.isNotBlank()) && - (isSameContactInfo || depositorContact.isNotBlank()) + get() = when { + isInviteTicket -> // orderAgreed && + reservationName.isNotBlank() && + reservationContact.isNotBlank() && + inviteCodeStatus is InviteCodeStatus.Valid + + totalPrice == 0 -> // orderAgreed &7 + reservationName.isNotBlank() && + reservationContact.isNotBlank() + + else -> // orderAgreed && + reservationName.isNotBlank() && + reservationContact.isNotBlank() && + (isSameContactInfo || depositorName.isNotBlank()) && + (isSameContactInfo || depositorContact.isNotBlank()) } fun toggleAgreement(index: Int): TicketingState { diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index b5d3db38..aead0c8b 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -176,7 +176,7 @@ 계좌번호 예금주 입금 마감일 - 결제를 완료했어요 + 결제가 완료되었어요 예매자 정보 확인 후 티켓이 발권됩니다. From e76fc577de22bd6af08edea7d659a83f466ea084 Mon Sep 17 00:00:00 2001 From: algosketch Date: Wed, 3 Apr 2024 09:12:29 +0900 Subject: [PATCH 099/129] =?UTF-8?q?feat=20:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=88=98=EC=A0=95.=20=EB=82=B4=EA=B0=80?= =?UTF-8?q?=20=EC=9D=B4=EA=B1=B0=EB=95=8C=EB=AC=B8=EC=97=90=20=EC=96=B4?= =?UTF-8?q?=EC=9A=B0...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/nexters/boolti/presentation/screen/Main.kt | 6 +++++- .../boolti/presentation/screen/show/ShowDetailScreen.kt | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 69b81c9f..3fc788c0 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -112,7 +112,11 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: navDeepLink { uriPattern = "https://app.boolti.in/show?showId={$showId}" action = Intent.ACTION_VIEW - } + }, + navDeepLink { + uriPattern = "https://preview.boolti.in/show/{$showId}" + action = Intent.ACTION_VIEW + }, ), ) { ShowDetailScreen( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index fd95beec..db9eb965 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -267,7 +267,7 @@ private fun ShowDetailAppBar( .size(44.dp), onClick = { Firebase.dynamicLinks.shortLinkAsync { - link = Uri.parse("https://app.boolti.in/show?showId=$showId") + link = Uri.parse("https://preview.boolti.in/show/$showId") domainUriPrefix = "https://boolti.page.link" androidParameters { } From b12384c75d1969f0eae5cd20bbf79b160235d5c7 Mon Sep 17 00:00:00 2001 From: algosketch Date: Wed, 3 Apr 2024 09:23:32 +0900 Subject: [PATCH 100/129] =?UTF-8?q?fix=20:=20deep=20link=20uri=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + .../java/com/nexters/boolti/presentation/screen/Main.kt | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 56c7ae9f..15837bd0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,6 +36,7 @@ + diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 3fc788c0..cd6f18aa 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -109,10 +109,10 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: startDestination = "detail", arguments = ShowDetail.arguments, deepLinks = listOf( - navDeepLink { - uriPattern = "https://app.boolti.in/show?showId={$showId}" - action = Intent.ACTION_VIEW - }, +// navDeepLink { +// uriPattern = "https://app.boolti.in/show?showId={$showId}" +// action = Intent.ACTION_VIEW +// }, navDeepLink { uriPattern = "https://preview.boolti.in/show/{$showId}" action = Intent.ACTION_VIEW From d5aa90e3465a8c0b88e141462f8942e79284d3f8 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Thu, 4 Apr 2024 00:46:06 +0900 Subject: [PATCH 101/129] =?UTF-8?q?release/1.4.0=20fix:=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=EC=99=84=EB=A3=8C=20=ED=99=94=EB=A9=B4=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=A0=9C=EA=B1=B0=20(=EC=98=88=EB=A7=A4=20?= =?UTF-8?q?=EB=82=B4=EC=97=AD=EB=B3=B4=EA=B8=B0,=20=ED=8B=B0=EC=BC=93=20?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/payment/PaymentCompleteScreen.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt index aa1f8617..d40a8a38 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt @@ -1,6 +1,5 @@ package com.nexters.boolti.presentation.screen.payment -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -23,8 +22,6 @@ import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.domain.model.ReservationState import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.component.MainButton -import com.nexters.boolti.presentation.component.SecondaryButton import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey15 import com.nexters.boolti.presentation.theme.Grey30 @@ -97,7 +94,8 @@ fun PaymentCompleteScreen( ) } - Row( + // TODO 백엔드에 TicketId 요청 필요 <-- 1.5.0 에 주석 제거 + /*Row( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) @@ -117,7 +115,7 @@ fun PaymentCompleteScreen( ) { navigateToTicketDetail(reservation) } - } + }*/ } } From bab6a77851628128598b1047b2f424c0d4ff2285 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Thu, 4 Apr 2024 00:51:49 +0900 Subject: [PATCH 102/129] =?UTF-8?q?release/1.4.0=20fix:=20=EC=B4=88?= =?UTF-8?q?=EC=B2=AD=ED=8B=B0=EC=BC=93=20=EA=B2=B0=EC=A0=9C=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=20=EA=B2=B0=EC=A0=9C=EC=9E=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=97=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/payment/PaymentCompleteScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt index d40a8a38..ff52dc34 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt @@ -58,7 +58,7 @@ fun PaymentCompleteScreen( label = stringResource(R.string.ticketing_ticket_holder_label), value = slashFormat(reservation.ticketHolderName, reservation.ticketHolderPhoneNumber), ) - if (reservation.isInviteTicket || reservation.totalAmountPrice > 0) { + if (!reservation.isInviteTicket || reservation.totalAmountPrice > 0) { InfoRow( modifier = Modifier.padding(top = 16.dp), label = stringResource(R.string.depositor_info_label), From 44e57dfb138b06d33decd4f56f54e192428a472e Mon Sep 17 00:00:00 2001 From: mangbaam Date: Thu, 4 Apr 2024 01:02:15 +0900 Subject: [PATCH 103/129] =?UTF-8?q?release/1.4.0=20fix:=20=ED=8B=B0?= =?UTF-8?q?=EC=BC=93=20=EC=83=81=EC=84=B8=20-=20=EB=8F=84=EB=A1=9C?= =?UTF-8?q?=EB=AA=85=20=EC=A3=BC=EC=86=8C=EA=B9=8C=EC=A7=80=EB=A7=8C=20?= =?UTF-8?q?=EB=B3=B5=EC=82=AC=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/ticket/detail/TicketDetailScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt index a5da64c5..4cf66328 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt @@ -315,7 +315,7 @@ private fun TicketDetailScreen( hostName = ticket.hostName, hostPhoneNumber = ticket.hostPhoneNumber, onClickCopyPlace = { - clipboardManager.setText(AnnotatedString(ticket.streetAddress + ticket.detailAddress)) + clipboardManager.setText(AnnotatedString(ticket.streetAddress)) if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { scope.launch { snackbarHostState.showSnackbar(copiedMessage) From 70ef62b35c09c766731c05fff7e8824da3b90c46 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Thu, 4 Apr 2024 01:08:03 +0900 Subject: [PATCH 104/129] =?UTF-8?q?release/1.4.0=20fix:=20=EC=98=88?= =?UTF-8?q?=EB=A7=A4=20=ED=99=95=EC=9D=B8=20=EB=8B=A4=EC=9D=B4=EC=96=BC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/ticketing/TicketingConfirmDialog.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt index 0042495e..08a0adf8 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt @@ -84,9 +84,9 @@ fun TicketingConfirmDialog( if (isInviteTicket || totalPrice > 0) { InfoRow( modifier = Modifier.padding(top = 16.dp), - label = stringResource(R.string.ticket_type_label), + label = stringResource(R.string.payment_type_label), value1 = if (isInviteTicket) { - stringResource(R.string.invite_ticket) + stringResource(R.string.invite_code_label) } else { when (paymentType) { PaymentType.ACCOUNT_TRANSFER -> stringResource(R.string.payment_account_transfer) From a977efb3a158d5bed30e26926477059f217d8a4d Mon Sep 17 00:00:00 2001 From: mangbaam Date: Thu, 4 Apr 2024 01:27:52 +0900 Subject: [PATCH 105/129] =?UTF-8?q?release/1.4.0=20style:=20=ED=8B=B0?= =?UTF-8?q?=EC=BC=93=20=EC=84=A0=ED=83=9D=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/ChooseTicketBottomSheet.kt | 101 +++++++++--------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt index 2e8ed4ad..3be3360e 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt @@ -155,60 +155,57 @@ private fun ChooseTicketBottomSheetContent2( Column(modifier) { Row( - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically + modifier = Modifier + .padding(top = 16.dp, end = 8.dp, start = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = ticket.ticket.ticketName.sliceAtMost(12), + style = MaterialTheme.typography.headlineSmall.copy( + color = MaterialTheme.colorScheme.onPrimary, + ), + ) + if (!ticket.ticket.isInviteTicket) { + Badge( + stringResource(R.string.badge_left_ticket_amount, ticket.quantity), + color = MaterialTheme.colorScheme.onSecondaryContainer, + containerColor = MaterialTheme.colorScheme.surface, + modifier = Modifier.padding(start = 8.dp), + ) + } + Spacer(modifier = Modifier.weight(1F)) + IconButton(onClick = onCloseClicked) { + Icon( + painter = painterResource(R.drawable.ic_close), + contentDescription = stringResource(id = R.string.description_close_button), + tint = Grey50, + ) + } + } + Row( + modifier = Modifier.padding(top = 8.dp, bottom = 20.dp, start = 24.dp, end = 24.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Column { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = ticket.ticket.ticketName.sliceAtMost(12), - style = MaterialTheme.typography.headlineSmall.copy( - color = MaterialTheme.colorScheme.onPrimary, - ), - ) - if (!ticket.ticket.isInviteTicket) { - Badge( - stringResource(R.string.badge_left_ticket_amount, ticket.quantity), - color = MaterialTheme.colorScheme.onSecondaryContainer, - containerColor = MaterialTheme.colorScheme.surface, - modifier = Modifier.padding(start = 8.dp), - ) - } - Spacer(modifier = Modifier.weight(1F)) - IconButton(onClick = onCloseClicked) { - Icon( - painter = painterResource(R.drawable.ic_close), - contentDescription = stringResource(id = R.string.description_close_button), - tint = Grey50, - ) - } - } - Row( - modifier = Modifier.padding(top = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - if (ticket.ticket.isInviteTicket) { - Text( - text = stringResource(R.string.ticketing_limit_per_person, 1), - style = MaterialTheme.typography.bodyLarge.copy(color = Grey15), - ) - } else { - HorizontalCountStepper( - modifier = Modifier.width(100.dp), - currentCount = ticketCount, - minCount = 1, - maxCount = ticket.quantity, - onClickMinus = { ticketCount-- }, - onClickPlus = { ticketCount++ }, - ) - } - Spacer(modifier = Modifier.weight(1f)) - Text( - text = stringResource(R.string.format_price, ticket.ticket.price), - style = MaterialTheme.typography.bodyLarge.copy(color = Grey15), - ) - } + if (ticket.ticket.isInviteTicket) { + Text( + text = stringResource(R.string.ticketing_limit_per_person, 1), + style = MaterialTheme.typography.bodyLarge.copy(color = Grey15), + ) + } else { + HorizontalCountStepper( + modifier = Modifier.width(100.dp), + currentCount = ticketCount, + minCount = 1, + maxCount = ticket.quantity, + onClickMinus = { ticketCount-- }, + onClickPlus = { ticketCount++ }, + ) } + Spacer(modifier = Modifier.weight(1f)) + Text( + text = stringResource(R.string.format_price, ticket.ticket.price), + style = MaterialTheme.typography.bodyLarge.copy(color = Grey15), + ) } HorizontalDivider(modifier = Modifier.fillMaxWidth(), thickness = 1.dp, color = Grey80) From 9973022aaa44f2a4f230315974d761f50912e9ed Mon Sep 17 00:00:00 2001 From: mangbaam Date: Thu, 4 Apr 2024 01:34:11 +0900 Subject: [PATCH 106/129] =?UTF-8?q?release/1.4.0=20style:=20=ED=8B=B0?= =?UTF-8?q?=EC=BC=93=20=EC=84=A0=ED=83=9D=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20"=EC=98=B5=EC=85=98=20=EC=84=A0=ED=83=9D"=20?= =?UTF-8?q?=ED=83=80=EC=9D=B4=ED=8B=80=20=ED=8B=B0=EC=BC=93=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EB=90=90=EC=9D=84=20=EB=95=8C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/ChooseTicketBottomSheet.kt | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt index 3be3360e..568cc139 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt @@ -99,12 +99,6 @@ fun ChooseTicketBottomSheet( ) .heightIn(max = 564.dp) ) { - Text( - text = stringResource(id = R.string.choose_ticket_bottomsheet_title), - style = MaterialTheme.typography.titleLarge.copy(color = Grey30), - modifier = Modifier - .padding(top = 20.dp, start = 24.dp, end = 24.dp, bottom = 12.dp) - ) uiState.selected?.let { ChooseTicketBottomSheetContent2( ticket = it, @@ -131,15 +125,23 @@ private fun ChooseTicketBottomSheetContent1( onSelectItem: (TicketWithQuantity) -> Unit, ) { val listState = rememberLazyListState() - LazyColumn( - modifier = modifier.nestedScroll(rememberNestedScrollInteropConnection()), - state = listState - ) { - items(tickets, key = { it.ticket.id }) { - TicketingTicketItem( - ticket = it, - onClick = onSelectItem, - ) + Column { + Text( + text = stringResource(id = R.string.choose_ticket_bottomsheet_title), + style = MaterialTheme.typography.titleLarge.copy(color = Grey30), + modifier = Modifier + .padding(top = 20.dp, start = 24.dp, end = 24.dp, bottom = 12.dp) + ) + LazyColumn( + modifier = modifier.nestedScroll(rememberNestedScrollInteropConnection()), + state = listState + ) { + items(tickets, key = { it.ticket.id }) { + TicketingTicketItem( + ticket = it, + onClick = onSelectItem, + ) + } } } } From c834b9395beaddcd23c23df10a014bcf22e39372 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 5 Apr 2024 17:00:05 +0900 Subject: [PATCH 107/129] =?UTF-8?q?fix=20:=20fallback=20url=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/show/ShowDetailScreen.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index 3f9b7b2f..3e2e8029 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -266,11 +266,16 @@ private fun ShowDetailAppBar( .size(44.dp), onClick = { Firebase.dynamicLinks.shortLinkAsync { - link = Uri.parse("https://preview.boolti.in/show/$showId") + val uri = Uri.parse("https://preview.boolti.in/show/$showId") + link = uri domainUriPrefix = "https://boolti.page.link" - androidParameters { } - iosParameters("com.nexters.boolti") { } + androidParameters { + fallbackUrl = uri + } + iosParameters("com.nexters.boolti") { + setFallbackUrl(uri) + } }.addOnSuccessListener { it.shortLink?.let { link -> println(link) From cb0a6d40ceec41086cfd1a06cd449b4bd82163c4 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 5 Apr 2024 21:31:30 +0900 Subject: [PATCH 108/129] =?UTF-8?q?fix=20:=20=ED=8B=B0=EC=BC=93=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4&=ED=8F=AC=EB=A7=B7=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/payment/AccountTransferContent.kt | 2 +- .../screen/payment/TicketSummarySection.kt | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt index 92b96b1b..4f2ae2a6 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt @@ -58,7 +58,7 @@ fun AccountTransferContent( modifier = Modifier.padding(top = 24.dp), poster = reservation.showImage, showName = reservation.showName, - paymentType = PaymentType.ACCOUNT_TRANSFER, + ticketName = reservation.ticketName, ticketCount = reservation.ticketCount, totalPrice = reservation.totalAmountPrice, ) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt index bdb6c598..ae288c7a 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt @@ -83,7 +83,7 @@ fun LegacyTicketSummarySection( modifier: Modifier = Modifier, poster: String, showName: String, - paymentType: PaymentType, + ticketName: String, ticketCount: Int, totalPrice: Int, ) { @@ -112,15 +112,14 @@ fun LegacyTicketSummarySection( style = point1, color = Grey05, ) - val payment = when (paymentType) { - PaymentType.ACCOUNT_TRANSFER -> R.string.payment_account_transfer - PaymentType.CARD -> R.string.payment_card - PaymentType.UNDEFINED -> R.string.blank - }.let { stringResource(id = it) } Text( modifier = Modifier.padding(top = 4.dp), - text = String.format("%s / %s", payment, ticketCount), + text = stringResource( + id = R.string.reservation_ticket_info_format, + ticketName, + ticketCount + ), color = Grey30, style = MaterialTheme.typography.bodySmall, ) @@ -159,7 +158,7 @@ private fun LegacyTicketSummaryPreview() { modifier = Modifier.padding(24.dp), poster = "", showName = "2024 TOGETHER LUCKY CLUB", - paymentType = PaymentType.ACCOUNT_TRANSFER, + ticketName = "일반 티켓 B", ticketCount = 10, totalPrice = 30000, ) From f8c5cb06a8efe0b6569e249e36286e86ec6fa8e8 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 5 Apr 2024 21:40:02 +0900 Subject: [PATCH 109/129] =?UTF-8?q?fix=20:=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EB=AC=B8=EA=B5=AC=20&=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/payment/PaymentCompleteScreen.kt | 2 +- presentation/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt index aa1f8617..92e2a75e 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt @@ -61,7 +61,7 @@ fun PaymentCompleteScreen( label = stringResource(R.string.ticketing_ticket_holder_label), value = slashFormat(reservation.ticketHolderName, reservation.ticketHolderPhoneNumber), ) - if (reservation.isInviteTicket || reservation.totalAmountPrice > 0) { + if (!reservation.isInviteTicket && reservation.totalAmountPrice > 0) { InfoRow( modifier = Modifier.padding(top = 16.dp), label = stringResource(R.string.depositor_info_label), diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 95832211..30b5b76c 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -176,7 +176,7 @@ 계좌번호 예금주 입금 마감일 - 결제가 완료되었어요 + 결제를 완료했어요 예매자 정보 확인 후 티켓이 발권됩니다. From 94443b58f61b1ac65ec351859087c7c539ee7252 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 5 Apr 2024 23:18:39 +0900 Subject: [PATCH 110/129] =?UTF-8?q?fix=20:=20=EC=B4=88=EC=B2=AD=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/data/repository/TicketingRepositoryImpl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/data/src/main/java/com/nexters/boolti/data/repository/TicketingRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/TicketingRepositoryImpl.kt index 5514c2b5..eeb65fa1 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/TicketingRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/TicketingRepositoryImpl.kt @@ -50,7 +50,6 @@ internal class TicketingRepositoryImpl @Inject constructor( val errMsg = response.errorBody()?.string() val status = InviteCodeStatus.fromString(errMsg?.errorType) emit(status) - throw InviteCodeException(status) } } From c8260c7e41ae12509e971ea00a6f4d03a4ca237c Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 5 Apr 2024 23:24:47 +0900 Subject: [PATCH 111/129] =?UTF-8?q?fix=20:=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=83=81=ED=83=9C,=20=EB=B0=9C=EA=B6=8C=20=EC=9D=BC=EC=8B=9C?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/reservations/ReservationDetailScreen.kt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index ed264b15..15e8a652 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -298,8 +298,6 @@ private fun PaymentInfo( else -> stringResource(id = R.string.reservations_unknown) } - val (stateStringId, _) = reservation.reservationState.toDescriptionAndColorPair() - Column { NormalRow( modifier = Modifier.padding(bottom = 8.dp), @@ -311,11 +309,6 @@ private fun PaymentInfo( key = stringResource(id = R.string.total_payment_amount_label), value = stringResource(id = R.string.unit_won, reservation.totalAmountPrice) ) - NormalRow( - modifier = Modifier.padding(top = 8.dp, bottom = 10.dp), - key = stringResource(id = R.string.payment_state_label), - value = stringResource(id = stateStringId) - ) } } } @@ -343,12 +336,6 @@ private fun TicketInfo( reservation.ticketCount ), ) - NormalRow( - modifier = Modifier.padding(top = 8.dp, bottom = 10.dp), - key = stringResource(id = R.string.reservation_datetime), - value = reservation.completedDateTime?.format(datetimeFormat) - ?: stringResource(id = R.string.reservation_before_completion), - ) } } } From 44bbf03c03c52923f47e4c5b63f5e1c4765df030 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 5 Apr 2024 23:36:42 +0900 Subject: [PATCH 112/129] =?UTF-8?q?fix=20:=20=EC=9E=85=EA=B8=88=20?= =?UTF-8?q?=EA=B3=84=EC=A2=8C=20=EC=A0=95=EB=B3=B4=20=EC=9A=B0=EC=B8=A1=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC,=20=EB=B3=B5=EC=82=AC=20UI=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservations/ReservationDetailScreen.kt | 54 +++++++------------ 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index 15e8a652..812fbb59 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -211,41 +212,49 @@ private fun DepositInfo( title = stringResource(id = R.string.reservation_account_info), ) { Column { - DepositInfoRow( + NormalRow( modifier = Modifier .height(32.dp) .padding(bottom = 8.dp), key = stringResource(id = R.string.bank_name), value = reservation.bankName, ) - DepositInfoRow( + Row( modifier = Modifier + .fillMaxWidth() .height(40.dp) .padding(bottom = 2.dp), - key = stringResource(id = R.string.account_number), - value = reservation.accountNumber, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { val clipboardManager = LocalClipboardManager.current val copiedMessage = stringResource(id = R.string.account_number_copied_message) - CopyButton( - label = stringResource(id = R.string.copy), - onClick = { + Text( + modifier = Modifier, + text = stringResource(id = R.string.account_number), + style = MaterialTheme.typography.bodyLarge.copy(color = Grey30), + ) + Text( + modifier = Modifier.clickable { clipboardManager.setText(AnnotatedString(reservation.accountNumber)) if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { snackbarController.showMessage(copiedMessage) } }, + text = reservation.accountNumber, + style = MaterialTheme.typography.bodyLarge.copy(color = Grey15), + textDecoration = TextDecoration.Underline, ) } - DepositInfoRow( + NormalRow( modifier = Modifier .height(40.dp) .padding(bottom = 2.dp), key = stringResource(id = R.string.account_holder), value = reservation.accountHolder, ) - DepositInfoRow( + NormalRow( modifier = Modifier .height(40.dp) .padding(bottom = 2.dp), @@ -256,33 +265,6 @@ private fun DepositInfo( } } -@Composable -private fun DepositInfoRow( - key: String, - value: String, - modifier: Modifier = Modifier, - content: (@Composable () -> Unit)? = null, -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier - .padding(end = 20.dp) - .width(80.dp), - text = key, - style = MaterialTheme.typography.bodyLarge.copy(color = Grey30), - ) - Text( - text = value, - style = MaterialTheme.typography.bodyLarge.copy(color = Grey15), - ) - Spacer(modifier = Modifier.weight(1.0f)) - content?.invoke() - } -} - @Composable private fun PaymentInfo( reservation: ReservationDetail, From d77672acdc85bf7b4c33ed3e1ce755d505e1c2e0 Mon Sep 17 00:00:00 2001 From: algosketch Date: Fri, 5 Apr 2024 23:47:08 +0900 Subject: [PATCH 113/129] =?UTF-8?q?fix=20:=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=83=81=EB=8B=A8=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservations/ReservationDetailScreen.kt | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index 812fbb59..10e9b4ba 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -48,20 +48,14 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import coil.compose.AsyncImage import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.domain.model.ReservationState import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.component.CopyButton import com.nexters.boolti.presentation.constants.datetimeFormat import com.nexters.boolti.presentation.extension.toDescriptionAndColorPair import com.nexters.boolti.presentation.screen.LocalSnackbarController -import com.nexters.boolti.presentation.screen.MainDestination -import com.nexters.boolti.presentation.screen.reservationId import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey15 import com.nexters.boolti.presentation.theme.Grey20 @@ -99,13 +93,27 @@ fun ReservationDetailScreen( .padding(innerPadding) .verticalScroll(scrollState) ) { - Text( - modifier = Modifier - .padding(horizontal = marginHorizontal) - .padding(top = 12.dp), - text = "No. ${state.reservation.csReservationId}", - style = MaterialTheme.typography.bodySmall.copy(color = Grey50), - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + val (textId, textColor) = state.reservation.reservationState.toDescriptionAndColorPair() + + Text( + modifier = Modifier + .padding(start = marginHorizontal) + .padding(top = 12.dp), + text = "No. ${state.reservation.csReservationId}", + style = MaterialTheme.typography.bodySmall.copy(color = Grey50), + ) + Text( + modifier = Modifier + .padding(end = marginHorizontal) + .padding(top = 12.dp), + text = stringResource(id = textId), + style = MaterialTheme.typography.bodySmall.copy(color = textColor) + ) + } Header(reservation = state.reservation) if (!state.reservation.isInviteTicket) { DepositInfo(reservation = state.reservation) From 699bb312e2d50977be299fa973a68998121a458f Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 6 Apr 2024 00:11:43 +0900 Subject: [PATCH 114/129] =?UTF-8?q?fix=20:=20=ED=99=98=EB=B6=88=20?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservations/ReservationDetailScreen.kt | 33 +++++++++++++++++++ presentation/src/main/res/values/strings.xml | 2 ++ 2 files changed, 35 insertions(+) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index 10e9b4ba..dc49c2bb 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -119,6 +119,9 @@ fun ReservationDetailScreen( DepositInfo(reservation = state.reservation) } PaymentInfo(reservation = state.reservation) + if (state.reservation.reservationState == ReservationState.REFUNDED) { + RefundInfo(reservation = state.reservation) + } TicketInfo(reservation = state.reservation) TicketHolderInfo(reservation = state.reservation) if (!state.reservation.isInviteTicket) DepositorInfo(reservation = state.reservation) @@ -303,6 +306,36 @@ private fun PaymentInfo( } } +@Composable +private fun RefundInfo( + reservation: ReservationDetail, +) { + Section( + modifier = Modifier.padding(top = 12.dp), + title = stringResource(id = R.string.reservation_breakdown_of_refund), + ) { + Column { + val paymentType = when (reservation.paymentType) { + PaymentType.ACCOUNT_TRANSFER -> stringResource(id = R.string.payment_account_transfer) + PaymentType.CARD -> stringResource(id = R.string.payment_card) + else -> stringResource(id = R.string.reservations_unknown) + } + + NormalRow( + modifier = Modifier.padding(bottom = 8.dp), + key = stringResource(id = R.string.reservation_price_of_refund), + value = stringResource(id = R.string.unit_won, reservation.totalAmountPrice), + ) + + NormalRow( + modifier = Modifier.padding(top = 8.dp, bottom = 10.dp), + key = stringResource(id = R.string.refund_method), + value = paymentType, + ) + } + } +} + @Composable private fun TicketInfo( reservation: ReservationDetail, diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 30b5b76c..a250d3fe 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -219,6 +219,8 @@ "%s / %d매" 주문 번호 주문 티켓 + 환불 내역 + 총 환불 금액 티켓 보기 예매 내역보기 From 281f7d2e234fae85d2e30ee65a6249e7dd1b4585 Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 6 Apr 2024 00:14:47 +0900 Subject: [PATCH 115/129] =?UTF-8?q?fix=20:=20=EC=98=88=EB=A7=A4=20?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=EC=83=81=EC=84=B8=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/reservations/ReservationDetailScreen.kt | 4 ++-- presentation/src/main/res/values/strings.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index dc49c2bb..9a6803a3 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -118,13 +118,13 @@ fun ReservationDetailScreen( if (!state.reservation.isInviteTicket) { DepositInfo(reservation = state.reservation) } + TicketHolderInfo(reservation = state.reservation) + if (!state.reservation.isInviteTicket) DepositorInfo(reservation = state.reservation) PaymentInfo(reservation = state.reservation) if (state.reservation.reservationState == ReservationState.REFUNDED) { RefundInfo(reservation = state.reservation) } TicketInfo(reservation = state.reservation) - TicketHolderInfo(reservation = state.reservation) - if (!state.reservation.isInviteTicket) DepositorInfo(reservation = state.reservation) if (!state.reservation.isInviteTicket) RefundPolicy(refundPolicy = refundPolicy) Spacer(modifier = Modifier.height(40.dp)) if (state.reservation.reservationState == ReservationState.RESERVED && diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index a250d3fe..625757eb 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -211,7 +211,7 @@ 예매 내역 상세 입금 계좌 정보 결제 정보 - 결제 상태 + 결제 수단 티켓 정보 발권 일시 발권 전 From fad30176e5c84296154637ccf4d6b6fd9fb0ea63 Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 6 Apr 2024 00:19:59 +0900 Subject: [PATCH 116/129] =?UTF-8?q?fix=20:=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=95=98?= =?UTF-8?q?=EB=8B=A8=20=EB=B2=84=ED=8A=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/payment/PaymentCompleteScreen.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt index 92e2a75e..6b494e2f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentCompleteScreen.kt @@ -97,6 +97,7 @@ fun PaymentCompleteScreen( ) } + /* Row( modifier = Modifier .fillMaxWidth() @@ -118,6 +119,7 @@ fun PaymentCompleteScreen( navigateToTicketDetail(reservation) } } + */ } } From 47b69ebc9a240fd768c14faa58e7d22dbda030ee Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 6 Apr 2024 00:28:32 +0900 Subject: [PATCH 117/129] =?UTF-8?q?fix=20:=20=EC=B4=88=EC=B2=AD=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20error=20border=20=EC=A1=B0=EA=B1=B4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/ticketing/TicketingScreen.kt | 25 +++++++++++++++---- presentation/src/main/res/values/strings.xml | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index 1d390346..294a2195 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -143,7 +143,10 @@ fun TicketingScreen( ) }, snackbarHost = { - ToastSnackbarHost(hostState = snackbarHostState, modifier = Modifier.padding(bottom = 100.dp)) + ToastSnackbarHost( + hostState = snackbarHostState, + modifier = Modifier.padding(bottom = 100.dp) + ) } ) { innerPadding -> Box(modifier = modifier.padding(innerPadding)) { @@ -195,7 +198,10 @@ fun TicketingScreen( ) } - if (!uiState.isInviteTicket && uiState.totalPrice > 0) PaymentSection(scope, snackbarHostState) // 결제 수단 + if (!uiState.isInviteTicket && uiState.totalPrice > 0) PaymentSection( + scope, + snackbarHostState + ) // 결제 수단 if (!uiState.isInviteTicket) RefundPolicySection(uiState.refundPolicy) // 취소/환불 규정 Text( @@ -250,7 +256,10 @@ fun TicketingScreen( .background(MaterialTheme.colorScheme.background) .padding(start = 20.dp, end = 20.dp, top = 8.dp, bottom = 24.dp), enabled = uiState.reservationButtonEnabled, - label = stringResource(R.string.ticketing_payment_button_label, uiState.totalPrice), + label = stringResource( + R.string.ticketing_payment_button_label, + uiState.totalPrice + ), onClick = { showConfirmDialog = true }, @@ -299,7 +308,10 @@ private fun Header( contentScale = ContentScale.Crop, ) - Column(verticalArrangement = Arrangement.Center, modifier = Modifier.padding(start = 16.dp)) { + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier.padding(start = 16.dp) + ) { Text( text = showName, style = point2, @@ -433,7 +445,10 @@ private fun InviteCodeSection( text = inviteCode.uppercase(), singleLine = true, enabled = inviteCodeStatus !is InviteCodeStatus.Valid, - isError = inviteCodeStatus is InviteCodeStatus.Invalid, + isError = inviteCodeStatus in listOf( + InviteCodeStatus.Invalid, + InviteCodeStatus.Duplicated, + ), placeholder = stringResource(R.string.ticketing_invite_code_placeholder), keyboardOptions = KeyboardOptions.Default.copy( keyboardType = KeyboardType.Password, diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 625757eb..82c66ebc 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -139,7 +139,7 @@ 사용하기 사용되었습니다 이미 사용된 초청 코드입니다 - 초청 코드가 올바르지 않아요 + 올바른 초청 코드를 입력해 주세요 초청 코드를 입력해 주세요 예매자와 결제자가 같아요 티켓 종류 From 1bda2beedc732d257c23d66ed0738f051d07b8b1 Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 6 Apr 2024 00:30:44 +0900 Subject: [PATCH 118/129] =?UTF-8?q?fix=20:=20=EB=B0=9C=EA=B6=8C=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=ED=85=8D=EC=8A=A4=ED=8A=B8,=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/presentation/extension/ReservationState.kt | 4 ++-- presentation/src/main/res/values/strings.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/extension/ReservationState.kt b/presentation/src/main/java/com/nexters/boolti/presentation/extension/ReservationState.kt index c6d08693..33c1bf46 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/extension/ReservationState.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/extension/ReservationState.kt @@ -12,8 +12,8 @@ fun ReservationState.toDescriptionAndColorPair(): Pair { ReservationState.DEPOSITING -> Pair(R.string.reservations_depositing, Grey30) ReservationState.REFUNDING -> Pair(R.string.reservations_refunding, Success) ReservationState.CANCELED -> Pair(R.string.reservations_canceled, Error) - ReservationState.RESERVED -> Pair(R.string.reservations_reserved, Grey30) + ReservationState.RESERVED -> Pair(R.string.reservations_reserved, Success) ReservationState.REFUNDED -> Pair(R.string.reservations_refunded, Error) ReservationState.UNDEFINED -> Pair(R.string.reservations_unknown, Error) } -} \ 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 82c66ebc..0f7ed9e8 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -200,7 +200,7 @@ 입금 확인 중 취소 진행 중 예매 취소 - 티켓 발권 완료 + 발권 완료 취소 완료 상세 보기 알 수 없음 From 2847dbb4e2be1100157ba1034e1f430c0c42219e Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 6 Apr 2024 00:51:56 +0900 Subject: [PATCH 119/129] =?UTF-8?q?fix=20:=20=EB=A1=9C=EA=B3=A0=20?= =?UTF-8?q?=EC=83=89=EC=83=81,=20=ED=81=AC=EA=B8=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/ticket/TicketContent.kt | 7 ++++--- .../src/main/res/drawable/ic_logo.xml | 20 +++++++------------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt index 27b95903..679acba0 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketContent.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -187,10 +188,10 @@ private fun Title( .padding(horizontal = 20.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { - Image( - modifier = Modifier.padding(end = 4.dp), + Icon( + modifier = Modifier.padding(end = 4.dp).size(20.dp), painter = painterResource(R.drawable.ic_logo), - colorFilter = ColorFilter.tint(Grey70.copy(alpha = 0.5f)), + tint = Grey80, contentDescription = null, ) Text( diff --git a/presentation/src/main/res/drawable/ic_logo.xml b/presentation/src/main/res/drawable/ic_logo.xml index f572dc9f..1371b49f 100644 --- a/presentation/src/main/res/drawable/ic_logo.xml +++ b/presentation/src/main/res/drawable/ic_logo.xml @@ -1,15 +1,9 @@ - - - - + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + From 27f899ef5c97b7f0c878788bdaf73d0f71787620 Mon Sep 17 00:00:00 2001 From: algosketch Date: Sat, 6 Apr 2024 00:55:27 +0900 Subject: [PATCH 120/129] =?UTF-8?q?fix=20:=20=EC=82=AC=EC=97=85=EC=9E=90?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/business/BusinessScreen.kt | 4 ++-- presentation/src/main/res/values/strings.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt index b742d892..797b8d1b 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt @@ -53,7 +53,7 @@ fun BusinessScreen( .verticalScroll(rememberScrollState()) ) { Text( - modifier = Modifier.padding(bottom = 20.dp), + modifier = Modifier.padding(top = 20.dp, bottom = 16.dp), text = stringResource(id = R.string.business_name), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onBackground, @@ -126,4 +126,4 @@ private fun BusinessMenu( @Composable private fun BusinessScreenPreview() { BusinessScreen(onBackPressed = {}) -} \ 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 0f7ed9e8..bc0c64ba 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -274,6 +274,6 @@ 호스팅 서비스 : 스튜디오 불티 통신판매업 신고번호 : 2024-화도수동-0518 문의전화 : 0507–1363–5690 - studio.boolti@gmail.com + 이메일 : studio.boolti@gmail.com From 0e60c46e2b6037e88244080f2eea1f967150e560 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sat, 6 Apr 2024 17:37:55 +0900 Subject: [PATCH 121/129] =?UTF-8?q?Boolti-215=20fix:=20=ED=8C=90=EB=A7=A4?= =?UTF-8?q?=20=EC=A2=85=EB=A3=8C=EB=90=9C=20=EA=B3=B5=EC=97=B0,=200?= =?UTF-8?q?=EC=9B=90=20=ED=8B=B0=EC=BC=93=EC=9D=B8=20=EA=B2=BD=EC=9A=B0?= =?UTF-8?q?=EC=97=90=EB=8A=94=20=EC=98=88=EB=A7=A4=20=EC=B7=A8=EC=86=8C=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EB=AF=B8=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/reservations/ReservationDetailScreen.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index 9a6803a3..f1f49ab1 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -65,6 +65,7 @@ import com.nexters.boolti.presentation.theme.Grey80 import com.nexters.boolti.presentation.theme.Grey90 import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.point2 +import java.time.LocalDateTime @Composable fun ReservationDetailScreen( @@ -127,8 +128,11 @@ fun ReservationDetailScreen( TicketInfo(reservation = state.reservation) if (!state.reservation.isInviteTicket) RefundPolicy(refundPolicy = refundPolicy) Spacer(modifier = Modifier.height(40.dp)) - if (state.reservation.reservationState == ReservationState.RESERVED && - !state.reservation.isInviteTicket + if ( + state.reservation.reservationState == ReservationState.RESERVED && + !state.reservation.isInviteTicket && + state.reservation.totalAmountPrice > 0 && + state.reservation.salesEndDateTime < LocalDateTime.now() ) { RefundButton( modifier = Modifier.padding(horizontal = marginHorizontal, vertical = 8.dp), From fb96ed2b1801c3bf4d4d2653c262267767e249ce Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sat, 6 Apr 2024 17:46:23 +0900 Subject: [PATCH 122/129] =?UTF-8?q?Boolti-215=20fix:=20=EC=98=88=EB=A7=A4?= =?UTF-8?q?=20=EB=82=B4=EC=97=AD=20=EC=83=81=EC=84=B8=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=20=EB=85=B8=EC=B6=9C=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/reservations/ReservationDetailScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index f1f49ab1..cf5c8173 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -121,11 +121,11 @@ fun ReservationDetailScreen( } TicketHolderInfo(reservation = state.reservation) if (!state.reservation.isInviteTicket) DepositorInfo(reservation = state.reservation) + TicketInfo(reservation = state.reservation) PaymentInfo(reservation = state.reservation) if (state.reservation.reservationState == ReservationState.REFUNDED) { RefundInfo(reservation = state.reservation) } - TicketInfo(reservation = state.reservation) if (!state.reservation.isInviteTicket) RefundPolicy(refundPolicy = refundPolicy) Spacer(modifier = Modifier.height(40.dp)) if ( @@ -287,7 +287,7 @@ private fun PaymentInfo( ) { Section( modifier = modifier.padding(top = 12.dp), - title = stringResource(id = R.string.payment_info_label), + title = stringResource(id = R.string.payment_state_label), ) { val paymentType = when (reservation.paymentType) { PaymentType.ACCOUNT_TRANSFER -> stringResource(id = R.string.payment_account_transfer) From def2ddcd9552e58c425b438bd6ffce936bd6f902 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sat, 6 Apr 2024 20:45:30 +0900 Subject: [PATCH 123/129] =?UTF-8?q?Boolti-215=20style:=20=ED=88=B4?= =?UTF-8?q?=EB=B0=94=20=EB=94=94=EC=9E=90=EC=9D=B8=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/component/BtAppBar.kt | 192 ++++++++++++++++-- .../screen/business/BusinessScreen.kt | 6 +- .../presentation/screen/login/LoginScreen.kt | 37 +--- .../screen/payment/PaymentScreen.kt | 39 ++++ .../screen/payment/PaymentToolbar.kt | 55 ----- .../screen/qr/HostedShowScreen.kt | 40 +--- .../presentation/screen/qr/QrFullScreen.kt | 8 +- .../presentation/screen/qr/QrScanScreen.kt | 40 +--- .../screen/refund/RefundScreen.kt | 8 +- .../screen/report/ReportScreen.kt | 8 +- .../reservations/ReservationDetailScreen.kt | 40 +--- .../screen/reservations/ReservationsScreen.kt | 11 +- .../screen/show/ShowDetailContentScreen.kt | 53 +---- .../screen/show/ShowDetailScreen.kt | 125 +++++------- .../screen/show/ShowImagesScreen.kt | 18 +- .../screen/signout/SignoutScreen.kt | 4 +- .../ticket/detail/TicketDetailScreen.kt | 27 +-- .../screen/ticketing/TicketingScreen.kt | 26 +-- 18 files changed, 311 insertions(+), 426 deletions(-) delete mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentToolbar.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt index abb03680..b6705d39 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt @@ -3,53 +3,211 @@ package com.nexters.boolti.presentation.component import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey10 -import com.nexters.boolti.presentation.theme.marginHorizontal @Composable fun BtAppBar( - title: String, - onBackPressed: () -> Unit, modifier: Modifier = Modifier, - @DrawableRes navIconRes: Int = R.drawable.ic_arrow_back, + title: String = "", + colors: BtAppBarColors = BtAppBarDefaults.appBarColors(), + navigateButtons: @Composable (RowScope.() -> Unit)? = null, + actionButtons: @Composable (RowScope.() -> Unit)? = null, ) { Row( modifier = modifier + .background(color = colors.containerColor) .fillMaxWidth() .height(44.dp) - .background(color = MaterialTheme.colorScheme.background), + .padding( + start = if (navigateButtons != null) 0.dp else 4.dp, + end = if (actionButtons != null) 0.dp else 4.dp, + ), verticalAlignment = Alignment.CenterVertically, ) { - IconButton( - modifier = Modifier.size(width = 48.dp, height = 44.dp), onClick = onBackPressed - ) { - Icon( - modifier = modifier - .padding(start = marginHorizontal) - .size(width = 24.dp, height = 24.dp), - tint = Grey10, - painter = painterResource(navIconRes), - contentDescription = stringResource(id = R.string.description_navigate_back), + navigateButtons?.let { + CompositionLocalProvider( + LocalContentColor provides colors.navigationIconColor, + content = { it() }, ) } Text( + modifier = Modifier + .weight(1f) + .padding( + start = if (navigateButtons != null) 0.dp else 16.dp, + end = if (actionButtons != null) 0.dp else 16.dp, + ), text = title, - style = MaterialTheme.typography.titleMedium.copy(color = Grey10), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleMedium, + color = colors.titleColor, ) + actionButtons?.let { + CompositionLocalProvider( + LocalContentColor provides colors.actionIconColor, + content = { it() }, + ) + } + } +} + +@Composable +fun BtBackAppBar( + modifier: Modifier = Modifier, + title: String = "", + colors: BtAppBarColors = BtAppBarDefaults.appBarColors(), + onClickBack: () -> Unit, +) { + BtAppBar( + modifier = modifier, + title = title, + navigateButtons = { + BtAppBarDefaults.AppBarIconButton( + onClick = onClickBack, + iconRes = R.drawable.ic_arrow_back, + ) + }, + colors = colors, + ) +} + +@Composable +fun BtCloseableAppBar( + modifier: Modifier = Modifier, + title: String = "", + colors: BtAppBarColors = BtAppBarDefaults.appBarColors(), + onClickClose: () -> Unit, +) { + BtAppBar( + modifier = modifier, + title = title, + navigateButtons = { + BtAppBarDefaults.AppBarIconButton( + onClick = onClickClose, + iconRes = R.drawable.ic_close, + description = stringResource(R.string.description_close_button), + ) + }, + colors = colors, + ) +} + +object BtAppBarDefaults { + @Composable + fun AppBarIconButton( + @DrawableRes iconRes: Int, + modifier: Modifier = Modifier, + description: String? = null, + onClick: () -> Unit, + ) { + IconButton( + modifier = modifier, + onClick = onClick, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(iconRes), + contentDescription = description, + ) + } + } + + @Composable + fun appBarColors( + containerColor: Color = MaterialTheme.colorScheme.background, + navigationIconColor: Color = Grey10, + titleColor: Color = Grey10, + actionIconColor: Color = Grey10, + ): BtAppBarColors = BtAppBarColors( + containerColor = containerColor, + navigationIconColor = navigationIconColor, + titleColor = titleColor, + actionIconColor = actionIconColor, + ) +} + +data class BtAppBarColors( + val containerColor: Color, + val navigationIconColor: Color, + val titleColor: Color, + val actionIconColor: Color, +) + +@Preview +@Composable +private fun BackBtAppBarPreview() { + BooltiTheme { + Surface { + BtBackAppBar(title = "title", onClickBack = {}) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun CloseableBtAppBarPreview() { + BooltiTheme { + Surface { + BtCloseableAppBar(title = "", onClickClose = {}) + } + } +} + +@Preview +@Composable +private fun ShowDetailAppBarPreview() { + BooltiTheme { + Surface { + BtAppBar( + navigateButtons = { + BtAppBarDefaults.AppBarIconButton( + iconRes = R.drawable.ic_arrow_back, + description = stringResource(id = R.string.description_navigate_back), + onClick = {}, + ) + BtAppBarDefaults.AppBarIconButton( + iconRes = R.drawable.ic_home, + description = stringResource(id = R.string.description_toolbar_home), + onClick = {}, + ) + }, + actionButtons = { + BtAppBarDefaults.AppBarIconButton( + iconRes = R.drawable.ic_share, + description = stringResource(id = R.string.ticketing_share), + onClick = {}, + ) + BtAppBarDefaults.AppBarIconButton( + iconRes = R.drawable.ic_verticle_more, + description = stringResource(id = R.string.description_more_menu), + onClick = {}, + ) + } + ) + } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt index 797b8d1b..cf1a7aca 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.component.BtAppBar +import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey50 import com.nexters.boolti.presentation.theme.Grey85 @@ -40,9 +40,9 @@ fun BusinessScreen( Scaffold( contentColor = MaterialTheme.colorScheme.background, topBar = { - BtAppBar( + BtBackAppBar( title = stringResource(id = R.string.business_title), - onBackPressed = onBackPressed, + onClickBack = onBackPressed, ) } ) { innerPadding -> diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt index f4863b36..6beb0455 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/login/LoginScreen.kt @@ -2,19 +2,15 @@ package com.nexters.boolti.presentation.screen.login import android.widget.Toast import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold @@ -31,7 +27,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -39,14 +34,11 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog +import com.nexters.boolti.presentation.component.BtCloseableAppBar import com.nexters.boolti.presentation.component.KakaoLoginButton import com.nexters.boolti.presentation.component.MainButton -import com.nexters.boolti.presentation.screen.MainDestination import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.subTextPadding @@ -99,7 +91,7 @@ fun LoginScreen( } Scaffold( - topBar = { LoginAppBar(onBackPressed = onBackPressed) }, + topBar = { BtCloseableAppBar(onClickClose = onBackPressed) }, containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> Box( @@ -132,31 +124,6 @@ fun LoginScreen( } } -@Composable -private fun LoginAppBar( - modifier: Modifier = Modifier, - onBackPressed: () -> Unit, -) { - Box( - modifier = modifier - .fillMaxWidth() - .height(44.dp) - .background(color = MaterialTheme.colorScheme.background), - ) { - IconButton( - modifier = Modifier.size(width = 48.dp, height = 44.dp), onClick = onBackPressed - ) { - Icon( - painter = painterResource(R.drawable.ic_close), - contentDescription = stringResource(id = R.string.description_navigate_back), - modifier - .padding(start = marginHorizontal) - .size(width = 24.dp, height = 24.dp) - ) - } - } -} - @Composable private fun SignUpBottomSheet( signUp: () -> Unit, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt index a96639f6..e4464556 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentScreen.kt @@ -5,6 +5,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -12,7 +13,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -20,7 +23,10 @@ import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.domain.model.ReservationState import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BtAppBar +import com.nexters.boolti.presentation.component.BtAppBarDefaults import com.nexters.boolti.presentation.component.ToastSnackbarHost +import com.nexters.boolti.presentation.theme.BooltiTheme import kotlinx.coroutines.launch @Composable @@ -101,3 +107,36 @@ private fun ProgressPayment( PaymentType.UNDEFINED -> Unit } } + +@Composable +private fun PaymentToolbar( + onClickHome: () -> Unit, + onClickClose: () -> Unit, +) { + BtAppBar( + navigateButtons = { + BtAppBarDefaults.AppBarIconButton( + iconRes = R.drawable.ic_home, + description = stringResource(R.string.description_toolbar_home), + onClick = onClickHome, + ) + }, + actionButtons = { + BtAppBarDefaults.AppBarIconButton( + iconRes = R.drawable.ic_close, + description = stringResource(R.string.description_close_button), + onClick = onClickClose, + ) + } + ) +} + +@Preview +@Composable +private fun PaymentToolBarPreview() { + BooltiTheme { + Surface { + PaymentToolbar({}, {}) + } + } +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentToolbar.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentToolbar.kt deleted file mode 100644 index 7f7a90bb..00000000 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentToolbar.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.nexters.boolti.presentation.screen.payment - -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.theme.BooltiTheme - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PaymentToolbar( - onClickHome: () -> Unit, - onClickClose: () -> Unit, -) { - TopAppBar( - title = {}, - navigationIcon = { - IconButton(onClick = onClickHome) { - Icon( - painter = painterResource(R.drawable.ic_home), - contentDescription = stringResource(R.string.description_toolbar_home), - ) - } - }, - actions = { - IconButton(onClick = onClickClose) { - Icon( - painter = painterResource(R.drawable.ic_close), - contentDescription = stringResource(R.string.description_close_button), - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.background, - ), - ) -} - -@Preview -@Composable -fun PaymentToolbarPreview() { - BooltiTheme { - Surface { - PaymentToolbar({}, {}) - } - } -} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt index d4640917..003e23b4 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/HostedShowScreen.kt @@ -12,15 +12,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,12 +30,9 @@ import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import com.nexters.boolti.domain.model.Show import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.theme.BooltiTheme import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey60 @@ -57,7 +50,12 @@ fun HostedShowScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() Scaffold( - topBar = { HostedShowToolbar(onClickBack) } + topBar = { + BtBackAppBar( + title = stringResource(R.string.hostedShowsTitle), + onClickBack = onClickBack, + ) + } ) { innerPadding -> if (uiState.shows.isEmpty()) { EmptyHostedShow(modifier = modifier.padding(innerPadding)) @@ -73,30 +71,6 @@ fun HostedShowScreen( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun HostedShowToolbar( - onClickBack: () -> Unit, -) { - TopAppBar( - title = { - Text(stringResource(R.string.hostedShowsTitle)) - }, navigationIcon = { - IconButton(onClick = onClickBack) { - Icon( - painter = painterResource(R.drawable.ic_arrow_back), - contentDescription = stringResource(R.string.description_navigate_back), - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.background, - titleContentColor = MaterialTheme.colorScheme.onBackground, - navigationIconContentColor = MaterialTheme.colorScheme.onBackground, - ) - ) -} - @Composable fun HostedShows( modifier: Modifier = Modifier, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt index 61b18782..46aaf8c7 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt @@ -22,13 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.screen.MainDestination -import com.nexters.boolti.presentation.screen.data -import com.nexters.boolti.presentation.screen.ticketName import com.nexters.boolti.presentation.theme.Grey10 import com.nexters.boolti.presentation.theme.Grey85 import com.nexters.boolti.presentation.theme.Grey90 @@ -52,7 +46,7 @@ fun QrFullScreen( modifier = Modifier .constrainAs(closeButton) { top.linkTo(parent.top, margin = 10.dp) - end.linkTo(parent.end, margin = 20.dp) + start.linkTo(parent.start, margin = 20.dp) } ) { Icon( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt index c1adb427..56343eee 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt @@ -7,12 +7,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHostState @@ -30,7 +28,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel @@ -41,6 +38,7 @@ import com.nexters.boolti.presentation.QrScanEvent import com.nexters.boolti.presentation.QrScanViewModel import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog +import com.nexters.boolti.presentation.component.BtCloseableAppBar import com.nexters.boolti.presentation.component.CircleBgIcon import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.theme.Error @@ -93,7 +91,10 @@ fun QrScanScreen( Scaffold( topBar = { - QrScanToolbar(showName = uiState.showName, onClickClose = onClickClose) + BtCloseableAppBar( + title = uiState.showName, + onClickClose = onClickClose, + ) }, bottomBar = { QrScanBottombar { showEntryCodeDialog = true } @@ -133,37 +134,6 @@ fun QrScanScreen( } } -@Composable -private fun QrScanToolbar( - showName: String, - onClickClose: () -> Unit, -) { - Row( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .height(44.dp) - .padding(horizontal = 20.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier - .padding(end = 20.dp) - .weight(1f), - text = showName, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onBackground, - ) - IconButton(onClick = onClickClose) { - Icon( - painter = painterResource(R.drawable.ic_close), - tint = MaterialTheme.colorScheme.onBackground, - contentDescription = stringResource(R.string.description_close_button), - ) - } - } -} - @Composable private fun QrScanBottombar(onClick: () -> Unit) { Box( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt index 172d8779..dda2ce89 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape @@ -32,7 +31,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog -import com.nexters.boolti.presentation.component.BtAppBar +import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.screen.LocalSnackbarController import com.nexters.boolti.presentation.theme.Grey15 import com.nexters.boolti.presentation.theme.Grey30 @@ -68,8 +67,9 @@ fun RefundScreen( Scaffold( topBar = { - BtAppBar( - title = stringResource(id = R.string.refund_button), onBackPressed = onBackPressed + BtBackAppBar( + title = stringResource(R.string.refund_button), + onClickBack = onBackPressed, ) }, modifier = Modifier, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt index 816968f3..d92cdab4 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/report/ReportScreen.kt @@ -21,14 +21,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTTextField -import com.nexters.boolti.presentation.component.BtAppBar +import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.component.MainButton -import com.nexters.boolti.presentation.extension.navigateToHome import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.marginHorizontal import com.nexters.boolti.presentation.theme.point4 @@ -54,7 +50,7 @@ fun ReportScreen( keyboardController?.hide() }, topBar = { - BtAppBar(title = stringResource(id = R.string.report), onBackPressed = onBackPressed) + BtBackAppBar(title = stringResource(id = R.string.report), onClickBack = onBackPressed) } ) { innerPadding -> Column( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index cf5c8173..f7075ad5 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -53,6 +52,7 @@ import com.nexters.boolti.domain.model.PaymentType import com.nexters.boolti.domain.model.ReservationDetail import com.nexters.boolti.domain.model.ReservationState import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.constants.datetimeFormat import com.nexters.boolti.presentation.extension.toDescriptionAndColorPair import com.nexters.boolti.presentation.screen.LocalSnackbarController @@ -83,7 +83,13 @@ fun ReservationDetailScreen( Scaffold( modifier = modifier, - topBar = { ReservationDetailAppBar(onBackPressed = onBackPressed) }) { innerPadding -> + topBar = { + BtBackAppBar( + title = stringResource(id = R.string.reservation_detail), + onClickBack = onBackPressed, + ) + }, + ) { innerPadding -> val state = uiState if (state !is ReservationDetailUiState.Success) return@Scaffold @@ -143,36 +149,6 @@ fun ReservationDetailScreen( } } -@Composable -private fun ReservationDetailAppBar( - onBackPressed: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .fillMaxWidth() - .height(44.dp) - .background(color = MaterialTheme.colorScheme.background), - verticalAlignment = Alignment.CenterVertically, - ) { - IconButton( - modifier = Modifier.size(width = 48.dp, height = 44.dp), onClick = onBackPressed - ) { - Icon( - painter = painterResource(R.drawable.ic_arrow_back), - contentDescription = stringResource(id = R.string.description_navigate_back), - modifier - .padding(start = marginHorizontal) - .size(width = 24.dp, height = 24.dp) - ) - } - Text( - text = stringResource(id = R.string.reservation_detail), - style = MaterialTheme.typography.titleMedium.copy(color = Grey10), - ) - } -} - @Composable private fun Header( reservation: ReservationDetail, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt index 28c06855..304b00ba 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationsScreen.kt @@ -31,17 +31,13 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import coil.compose.AsyncImage import com.nexters.boolti.domain.model.Reservation import com.nexters.boolti.domain.model.ReservationState import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.component.BtAppBar +import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.component.BtCircularProgressIndicator import com.nexters.boolti.presentation.extension.toDescriptionAndColorPair -import com.nexters.boolti.presentation.screen.MainDestination import com.nexters.boolti.presentation.theme.Grey05 import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.Grey50 @@ -62,10 +58,7 @@ fun ReservationsScreen( Scaffold( topBar = { - BtAppBar( - title = stringResource(id = R.string.my_ticketing_history), - onBackPressed = onBackPressed, - ) + BtBackAppBar(title = stringResource(id = R.string.my_ticketing_history), onClickBack = onBackPressed) } ) { innerPadding -> Box( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt index 949e6acb..3a9b77e3 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailContentScreen.kt @@ -1,34 +1,20 @@ package com.nexters.boolti.presentation.screen.show -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.screen.sharedViewModel -import com.nexters.boolti.presentation.theme.Grey10 +import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.theme.Grey30 import com.nexters.boolti.presentation.theme.marginHorizontal @@ -43,7 +29,12 @@ fun ShowDetailContentScreen( Scaffold( modifier = modifier, - topBar = { ShowDetailContentAppBar(onBackPressed = onBackPressed) } + topBar = { + BtBackAppBar( + title = stringResource(id = R.string.ticketing_all_content_title), + onClickBack = onBackPressed, + ) + }, ) { innerPadding -> Text( modifier = Modifier @@ -56,33 +47,3 @@ fun ShowDetailContentScreen( ) } } - -@Composable -private fun ShowDetailContentAppBar( - modifier: Modifier = Modifier, - onBackPressed: () -> Unit, -) { - Row( - modifier = modifier - .fillMaxWidth() - .height(44.dp) - .background(color = MaterialTheme.colorScheme.background), - verticalAlignment = Alignment.CenterVertically, - ) { - IconButton( - modifier = Modifier.size(width = 48.dp, height = 44.dp), onClick = onBackPressed - ) { - Icon( - painter = painterResource(R.drawable.ic_arrow_back), - contentDescription = stringResource(id = R.string.description_navigate_back), - modifier - .padding(start = marginHorizontal) - .size(width = 24.dp, height = 24.dp) - ) - } - Text( - text = stringResource(id = R.string.ticketing_all_content_title), - style = MaterialTheme.typography.titleMedium.copy(color = Grey10), - ) - } -} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt index 3f9b7b2f..42492510 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowDetailScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -26,8 +25,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -47,7 +44,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -57,13 +53,14 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.firebase.Firebase import com.google.firebase.dynamiclinks.androidParameters -import com.google.firebase.dynamiclinks.dynamicLink import com.google.firebase.dynamiclinks.dynamicLinks import com.google.firebase.dynamiclinks.iosParameters import com.google.firebase.dynamiclinks.shortLinkAsync import com.nexters.boolti.domain.model.ShowDetail import com.nexters.boolti.domain.model.ShowState import com.nexters.boolti.presentation.R +import com.nexters.boolti.presentation.component.BtAppBar +import com.nexters.boolti.presentation.component.BtAppBarDefaults import com.nexters.boolti.presentation.component.CopyButton import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.extension.requireActivity @@ -223,94 +220,64 @@ private fun ShowDetailAppBar( onBack: () -> Unit, onClickHome: () -> Unit, navigateToReport: () -> Unit, - modifier: Modifier = Modifier, ) { val context = LocalContext.current var isContextMenuVisible by rememberSaveable { mutableStateOf(false) } - - Row( - modifier = modifier - .height(44.dp) - .fillMaxWidth() - .background(color = MaterialTheme.colorScheme.surface), - verticalAlignment = Alignment.CenterVertically, - ) { - IconButton( - modifier = Modifier.size(width = 48.dp, height = 44.dp), - onClick = onBack, - ) { - Icon( - painter = painterResource(R.drawable.ic_arrow_back), - contentDescription = stringResource(id = R.string.description_navigate_back), - Modifier - .padding(start = marginHorizontal) - .size(width = 24.dp, height = 24.dp) + BtAppBar( + colors = BtAppBarDefaults.appBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + navigateButtons = { + BtAppBarDefaults.AppBarIconButton( + iconRes = R.drawable.ic_arrow_back, + description = stringResource(id = R.string.description_navigate_back), + onClick = onBack, ) - } - IconButton( - modifier = Modifier.size(width = 64.dp, height = 44.dp), - onClick = onClickHome, - ) { - Icon( - painter = painterResource(R.drawable.ic_home), - contentDescription = stringResource(id = R.string.description_toolbar_home), - Modifier.size(width = 24.dp, height = 24.dp) + BtAppBarDefaults.AppBarIconButton( + iconRes = R.drawable.ic_home, + description = stringResource(id = R.string.description_toolbar_home), + onClick = onClickHome, ) - } - Spacer(modifier = Modifier.weight(1.0f)) - IconButton( - modifier = Modifier - .padding(end = 10.dp) - .size(44.dp), - onClick = { - Firebase.dynamicLinks.shortLinkAsync { - link = Uri.parse("https://preview.boolti.in/show/$showId") - domainUriPrefix = "https://boolti.page.link" + }, + actionButtons = { + BtAppBarDefaults.AppBarIconButton( + iconRes = R.drawable.ic_share, + description = stringResource(id = R.string.ticketing_share), + onClick = { + Firebase.dynamicLinks.shortLinkAsync { + link = Uri.parse("https://preview.boolti.in/show/$showId") + domainUriPrefix = "https://boolti.page.link" - androidParameters { } - iosParameters("com.nexters.boolti") { } - }.addOnSuccessListener { - it.shortLink?.let { link -> - println(link) + androidParameters { } + iosParameters("com.nexters.boolti") { } + }.addOnSuccessListener { + it.shortLink?.let { link -> + println(link) - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - putExtra( - Intent.EXTRA_TEXT, - link.toString() - ) - type = "text/plain" - } + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra( + Intent.EXTRA_TEXT, + link.toString() + ) + type = "text/plain" + } - val shareIntent = Intent.createChooser(sendIntent, null) - context.startActivity(shareIntent) + val shareIntent = Intent.createChooser(sendIntent, null) + context.startActivity(shareIntent) + } } - } - }, - ) { - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(R.drawable.ic_share), - contentDescription = stringResource(id = R.string.ticketing_share), + }, ) - } - IconButton( - modifier = Modifier - .padding(end = marginHorizontal) - .size(24.dp), - onClick = { - isContextMenuVisible = true - }, - ) { - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(R.drawable.ic_verticle_more), - contentDescription = stringResource(id = R.string.description_more_menu), + BtAppBarDefaults.AppBarIconButton( + iconRes = R.drawable.ic_verticle_more, + description = stringResource(id = R.string.description_more_menu), + onClick = { isContextMenuVisible = true }, ) } - } + ) Box( modifier = Modifier diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt index 7e02d201..21a5c4a4 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/show/ShowImagesScreen.kt @@ -24,16 +24,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument import coil.compose.AsyncImage -import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.component.BtAppBar -import com.nexters.boolti.presentation.screen.sharedViewModel +import com.nexters.boolti.presentation.component.BtCloseableAppBar import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.zoomable @@ -52,13 +44,7 @@ fun ShowImagesScreen( ) { uiState.showDetail.images.size } Scaffold( - topBar = { - BtAppBar( - title = "", - onBackPressed = onBackPressed, - navIconRes = R.drawable.ic_close, - ) - }, + topBar = { BtCloseableAppBar(onClickClose = onBackPressed) }, modifier = modifier, ) { innerPadding -> Column( diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt index 3861a6f2..5d7c97da 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutScreen.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nexters.boolti.presentation.R -import com.nexters.boolti.presentation.component.BtAppBar +import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.theme.marginHorizontal @@ -42,7 +42,7 @@ fun SignoutScreen( } Scaffold( - topBar = { BtAppBar(title = stringResource(R.string.signout), onBackPressed = navigateBack) }, + topBar = { BtBackAppBar(title = stringResource(R.string.signout), onClickBack = navigateBack) }, bottomBar = { MainButton( modifier = Modifier diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt index 4cf66328..94ec03ab 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/detail/TicketDetailScreen.kt @@ -29,15 +29,12 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshContainer import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable @@ -84,6 +81,7 @@ import coil.compose.AsyncImage import com.nexters.boolti.domain.model.TicketState import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog +import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.component.DottedDivider import com.nexters.boolti.presentation.component.ToastSnackbarHost import com.nexters.boolti.presentation.extension.dayOfWeekString @@ -175,7 +173,7 @@ private fun TicketDetailScreen( } Scaffold( - topBar = { TicketDetailToolbar(onBackClicked = onBackClicked) }, + topBar = { BtBackAppBar(onClickBack = onBackClicked) }, snackbarHost = { ToastSnackbarHost( modifier = Modifier.padding(bottom = 54.dp), @@ -377,27 +375,6 @@ private fun TicketDetailScreen( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TicketDetailToolbar( - onBackClicked: () -> Unit, -) { - TopAppBar( - title = {}, - navigationIcon = { - IconButton(onClick = onBackClicked) { - Icon( - painter = painterResource(R.drawable.ic_arrow_back), - contentDescription = stringResource(R.string.description_navigate_back), - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.background, - ), - ) -} - @Composable private fun Title( ticketName: String = "", diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt index 294a2195..2334bd6f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -35,8 +34,6 @@ import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -70,6 +67,7 @@ import coil.compose.AsyncImage import com.nexters.boolti.domain.model.InviteCodeStatus import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTTextField +import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.component.BusinessInformation import com.nexters.boolti.presentation.component.MainButton import com.nexters.boolti.presentation.component.ToastSnackbarHost @@ -93,7 +91,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.time.LocalDateTime -@OptIn(ExperimentalMaterial3Api::class) @Composable fun TicketingScreen( modifier: Modifier = Modifier, @@ -122,24 +119,9 @@ fun TicketingScreen( Scaffold( topBar = { - TopAppBar( - modifier = Modifier.padding(start = 20.dp), - title = { - Text( - text = stringResource(R.string.ticketing_toolbar_title), - style = MaterialTheme.typography.titleLarge, - ) - }, - navigationIcon = { - Icon( - painter = painterResource(id = R.drawable.ic_arrow_back), - contentDescription = stringResource(R.string.description_navigate_back), - modifier = Modifier.clickable(role = Role.Button) { onBackClicked() }, - ) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.background, - ), + BtBackAppBar( + title = stringResource(R.string.ticketing_toolbar_title), + onClickBack = onBackClicked, ) }, snackbarHost = { From 617baa6c9db31e0bd57a4169f529b3e5c40bf33e Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sat, 6 Apr 2024 20:48:35 +0900 Subject: [PATCH 124/129] =?UTF-8?q?Boolti-215=20style:=20x=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=9E=88=EB=8A=94=20=ED=88=B4=EB=B0=94=20x=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=EC=9D=84=20=EC=98=A4=EB=A5=B8=EC=AA=BD?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/nexters/boolti/presentation/component/BtAppBar.kt | 2 +- .../com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt index b6705d39..88a115cc 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtAppBar.kt @@ -105,7 +105,7 @@ fun BtCloseableAppBar( BtAppBar( modifier = modifier, title = title, - navigateButtons = { + actionButtons = { BtAppBarDefaults.AppBarIconButton( onClick = onClickClose, iconRes = R.drawable.ic_close, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt index 46aaf8c7..c64ede2e 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrFullScreen.kt @@ -46,7 +46,7 @@ fun QrFullScreen( modifier = Modifier .constrainAs(closeButton) { top.linkTo(parent.top, margin = 10.dp) - start.linkTo(parent.start, margin = 20.dp) + end.linkTo(parent.end, margin = 20.dp) } ) { Icon( From 4c8e8e96ea63d7db51edb9599d6b69cc696be8b6 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sat, 6 Apr 2024 20:57:22 +0900 Subject: [PATCH 125/129] =?UTF-8?q?Boolti-215=20fix:=20navigateToHome=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=20=EC=88=98=EC=A0=95=20(Home=20=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20=EC=8B=9C=EB=8F=84,=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EA=B8=B0=EC=A1=B4=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EB=8C=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 딥링크로 바로 공연 상세로 이동한 경우 발생하는 문제 --- .../nexters/boolti/presentation/extension/NavController.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/extension/NavController.kt b/presentation/src/main/java/com/nexters/boolti/presentation/extension/NavController.kt index faf1f69b..efb98184 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/extension/NavController.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/extension/NavController.kt @@ -1,8 +1,13 @@ package com.nexters.boolti.presentation.extension import androidx.navigation.NavController +import com.nexters.boolti.presentation.screen.MainDestination fun NavController.navigateToHome() { popBackStack(graph.startDestinationId, true) - navigate(graph.startDestinationId) + try { + navigate(MainDestination.Home.route) + } catch (e: IllegalArgumentException) { + navigate(graph.startDestinationId) + } } From 8ce787ffff1bbf566d4a315dabd2253e38b08a91 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 7 Apr 2024 14:52:44 +0900 Subject: [PATCH 126/129] =?UTF-8?q?Boolti-215=20style:=20=EC=98=88?= =?UTF-8?q?=EB=A7=A4=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/boolti/presentation/extension/ReservationState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/extension/ReservationState.kt b/presentation/src/main/java/com/nexters/boolti/presentation/extension/ReservationState.kt index 33c1bf46..1390ab7b 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/extension/ReservationState.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/extension/ReservationState.kt @@ -10,7 +10,7 @@ import com.nexters.boolti.presentation.theme.Success fun ReservationState.toDescriptionAndColorPair(): Pair { return when (this) { ReservationState.DEPOSITING -> Pair(R.string.reservations_depositing, Grey30) - ReservationState.REFUNDING -> Pair(R.string.reservations_refunding, Success) + ReservationState.REFUNDING -> Pair(R.string.reservations_refunding, Grey30) ReservationState.CANCELED -> Pair(R.string.reservations_canceled, Error) ReservationState.RESERVED -> Pair(R.string.reservations_reserved, Success) ReservationState.REFUNDED -> Pair(R.string.reservations_refunded, Error) From 480c6462d3a8d0c882414f9f7492fff0f0ed888d Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 7 Apr 2024 16:49:57 +0900 Subject: [PATCH 127/129] =?UTF-8?q?Boolti-217=20fix:=20=EC=98=88=EB=A7=A4?= =?UTF-8?q?=20=EC=B7=A8=EC=86=8C=20=EB=B2=84=ED=8A=BC=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=20=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/reservations/ReservationDetailScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index f7075ad5..c3a33118 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -138,7 +138,7 @@ fun ReservationDetailScreen( state.reservation.reservationState == ReservationState.RESERVED && !state.reservation.isInviteTicket && state.reservation.totalAmountPrice > 0 && - state.reservation.salesEndDateTime < LocalDateTime.now() + state.reservation.salesEndDateTime >= LocalDateTime.now() ) { RefundButton( modifier = Modifier.padding(horizontal = marginHorizontal, vertical = 8.dp), From 8d7ef0f4439232d0dce0bb15976deb8c79e83636 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 7 Apr 2024 16:51:45 +0900 Subject: [PATCH 128/129] =?UTF-8?q?Boolti-217=20style:=20=EC=98=88?= =?UTF-8?q?=EB=A7=A4=20=EB=82=B4=EC=97=AD=20-=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EB=8B=A8=20=EB=82=B4=20=EC=88=9C=EC=84=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/reservations/ReservationDetailScreen.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt index c3a33118..5e847038 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/reservations/ReservationDetailScreen.kt @@ -273,15 +273,15 @@ private fun PaymentInfo( Column { NormalRow( - modifier = Modifier.padding(bottom = 8.dp), - key = stringResource(id = R.string.payment_type_label), - value = if (reservation.isInviteTicket) stringResource(id = R.string.invite_code_label) else paymentType - ) - NormalRow( - modifier = Modifier.padding(top = 8.dp, bottom = 10.dp), + modifier = Modifier.padding(bottom = 10.dp), key = stringResource(id = R.string.total_payment_amount_label), value = stringResource(id = R.string.unit_won, reservation.totalAmountPrice) ) + NormalRow( + modifier = Modifier.padding(top = 8.dp, bottom = 8.dp), + key = stringResource(id = R.string.payment_type_label), + value = if (reservation.isInviteTicket) stringResource(id = R.string.invite_code_label) else paymentType + ) } } } From b1475dcc1005f4b9934f3826bdf73a8b266ce976 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 7 Apr 2024 17:37:24 +0900 Subject: [PATCH 129/129] =?UTF-8?q?Boolti-217=20chore:=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=97=85=20(1.4.0=20/=208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c5fc8e68..ef73023a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] minSdk = "26" targetSdk = "34" -versionCode = "7" -versionName = "1.2.0" +versionCode = "8" +versionName = "1.4.0" packageName = "com.nexters.boolti" compileSdk = "34" targetJvm = "17" @@ -109,7 +109,7 @@ zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", ve kakao-login = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao" } retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "serializationConverter" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } -mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk"} +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } [plugins] android-application = { id = "com.android.application", version.ref = "android" }