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
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
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c3547adb..15837bd0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,7 +26,20 @@
+ android:theme="@style/Theme.Boolti" >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
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/AuthDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/AuthDataSource.kt
index fe65f6eb..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
@@ -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,
) {
@@ -48,7 +48,11 @@ class AuthDataSource @Inject constructor(
}
suspend fun logout(): Result = runCatching {
+ localLogout()
loginService.logout()
+ }
+
+ 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/DeviceTokenDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/DeviceTokenDataSource.kt
new file mode 100644
index 00000000..954c96be
--- /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
+
+internal 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)
+ })
+ }
+}
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 d5a1ec88..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
@@ -6,11 +6,9 @@ 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
-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 112db81a..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
@@ -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(
+internal 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/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 8b3d4b39..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
@@ -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
@@ -30,7 +31,7 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
-object NetworkModule {
+internal object NetworkModule {
@Singleton
@Provides
@Named("auth")
@@ -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/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..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
@@ -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 {
@@ -28,6 +28,9 @@ class AuthAuthenticator @Inject constructor(
refreshToken = it.refreshToken,
)
it.accessToken
+ } ?: run {
+ authDataSource.logout()
+ null
}
}
}
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
new file mode 100644
index 00000000..78e4c6f3
--- /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
+
+internal interface DeviceTokenService {
+ @POST("/app/papi/v1/device-token")
+ suspend fun postFcmToken(@Body request: DeviceTokenRequest): Response
+}
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 98e734f1..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
@@ -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 {
+internal 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/network/request/DeviceTokenRequest.kt b/data/src/main/java/com/nexters/boolti/data/network/request/DeviceTokenRequest.kt
new file mode 100644
index 00000000..172135f5
--- /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
+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
new file mode 100644
index 00000000..ac450aed
--- /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
+internal data class DeviceTokenResponse(
+ val tokenId: String
+)
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..87ea0edb 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,
+internal data class LoginResponse(
+ @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/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 e9d98d63..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
@@ -7,10 +7,11 @@ 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,
+ val showDate: String,
val salesTicketName: String,
val salesTicketType: String,
val ticketCount: Int,
@@ -26,12 +27,14 @@ data class ReservationDetailResponse(
val reservationPhoneNumber: String,
val depositorName: String = "",
val depositorPhoneNumber: String = "",
+ val csReservationId: String,
) {
fun toDomain(): ReservationDetail {
return ReservationDetail(
id = reservationId,
showImage = showImg,
showName = showName,
+ showDate = showDate.toLocalDateTime(),
ticketName = salesTicketName,
isInviteTicket = salesTicketType == "INVITE",
ticketCount = ticketCount,
@@ -47,6 +50,7 @@ data class ReservationDetailResponse(
ticketHolderPhoneNumber = reservationPhoneNumber,
depositorName = depositorName,
depositorPhoneNumber = depositorPhoneNumber,
+ csReservationId = csReservationId,
)
}
}
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..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
@@ -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,
@@ -18,6 +18,8 @@ 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 @@ 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/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 1d37e778..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
@@ -1,23 +1,28 @@
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
+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
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
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,
private val userDateSource: UserDataSource,
+ private val deviceTokenDataSource: DeviceTokenDataSource,
) : AuthRepository {
override val loggedIn: Flow
get() = authDataSource.loggedIn
@@ -25,24 +30,26 @@ class AuthRepositoryImpl @Inject constructor(
override val cachedUser: Flow
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 ?: "")
- }
- .mapCatching {
- !it.signUpRequired
- }
+ override suspend fun kakaoLogin(request: LoginRequest): Result {
+ return authDataSource.login(request).onSuccess { response ->
+ tokenDataSource.saveTokens(response.accessToken ?: "", response.refreshToken ?: "")
+ deviceTokenDataSource.sendFcmToken()
+ }.mapCatching(LoginResponse::toDomain)
}
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)
- }
- .mapCatching { }
+ return signUpDataSource.signUp(signUpRequest).onSuccess { response ->
+ tokenDataSource.saveTokens(response.accessToken, response.refreshToken)
+ deviceTokenDataSource.sendFcmToken()
+ }.mapCatching { }
+ }
+
+ override suspend fun signout(request: SignoutRequest): Result = runCatching {
+ userDateSource.signout(request)
+ }.onSuccess {
+ authDataSource.localLogout()
}
override fun getUserAndCache(): Flow = flow {
@@ -50,4 +57,6 @@ class AuthRepositoryImpl @Inject constructor(
authDataSource.updateUser(response)
emit(response.toDomain())
}
+
+ override suspend fun sendFcmToken(): Result = deviceTokenDataSource.sendFcmToken()
}
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..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
@@ -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 {
@@ -26,7 +26,7 @@ class ReservationRepositoryImpl @Inject constructor(
if (isSuccessful) {
emit(Unit)
} else {
- throw RuntimeException("환불 실패")
+ 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..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
@@ -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 {
@@ -50,7 +50,6 @@ class TicketingRepositoryImpl @Inject constructor(
val errMsg = response.errorBody()?.string()
val status = InviteCodeStatus.fromString(errMsg?.errorType)
emit(status)
- throw InviteCodeException(status)
}
}
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/model/ReservationDetail.kt b/domain/src/main/java/com/nexters/boolti/domain/model/ReservationDetail.kt
index 2504da9a..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,
@@ -21,4 +22,5 @@ data class ReservationDetail(
val ticketHolderPhoneNumber: String,
val depositorName: String,
val depositorPhoneNumber: String,
+ val csReservationId: String,
)
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..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,14 +43,15 @@ data class Ticket(
val usedAt: LocalDateTime? = null,
val hostName: String = "",
val hostPhoneNumber: String = "",
+ val csReservationId: String = "",
+ val csTicketId: String = "",
) {
val ticketState: TicketState
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
}
}
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..cd827031 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,8 +1,10 @@
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
+import com.nexters.boolti.domain.request.SignoutRequest
import kotlinx.coroutines.flow.Flow
interface AuthRepository {
@@ -12,12 +14,14 @@ 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
fun getUserAndCache(): Flow
+ suspend fun sendFcmToken(): Result
val loggedIn: Flow
val cachedUser: 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/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
-}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a5636d4f..ef73023a 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 = "8"
+versionName = "1.4.0"
packageName = "com.nexters.boolti"
compileSdk = "34"
targetJvm = "17"
@@ -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"
@@ -100,12 +101,15 @@ 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" }
+firebase-dynamic-links = { module = "com.google.firebase:firebase-dynamic-links-ktx", version.require = "false" }
+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" }
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" }
@@ -124,7 +128,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", "firebase-dynamic-links"]
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"]
diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts
index ff190d69..bdbccbce 100644
--- a/presentation/build.gradle.kts
+++ b/presentation/build.gradle.kts
@@ -59,10 +59,13 @@ 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)
implementation(libs.androidx.material3.android)
+ implementation(libs.zoomable)
kapt(libs.hilt.compiler)
implementation(libs.lottie)
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/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..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"])
@@ -48,18 +49,15 @@ 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)
}
/**
* 입장 확인
*/
private fun requestEntrance(entryCode: String) {
- viewModelScope.launch {
+ viewModelScope.launch(recordExceptionHandler) {
hostRepository.requestEntrance(
QrScanRequest(showId = showId, entryCode = entryCode)
).catch { e ->
@@ -69,6 +67,7 @@ class QrScanViewModel @Inject constructor(
event(QrScanEvent.ScanError(type))
}
}
+ else -> throw e
}
}.singleOrNull()?.let {
event(QrScanEvent.ScanSuccess)
@@ -77,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/base/BaseViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/base/BaseViewModel.kt
new file mode 100644
index 00000000..8a5bd4de
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/base/BaseViewModel.kt
@@ -0,0 +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.Dispatchers
+import kotlinx.coroutines.launch
+import timber.log.Timber
+
+open class BaseViewModel : ViewModel() {
+ protected val recordExceptionHandler = CoroutineExceptionHandler { _, throwable ->
+ viewModelScope.launch(Dispatchers.IO) {
+ FirebaseCrashlytics.getInstance().recordException(throwable)
+ Timber.e(throwable)
+ }
+ }
+}
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..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
@@ -3,52 +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(
- painter = painterResource(navIconRes),
- contentDescription = stringResource(id = R.string.description_navigate_back),
- modifier
- .padding(start = marginHorizontal)
- .size(width = 24.dp, height = 24.dp)
+ 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,
+ actionButtons = {
+ 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/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,
+ )
+ }
+}
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..88dd4169
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/BusinessInformation.kt
@@ -0,0 +1,58 @@
+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
+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(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .padding(vertical = 20.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(bottom = 2.dp)
+ .clickable(onClick = onClick),
+ 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/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/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/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 b1d0da01..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
@@ -14,24 +14,27 @@ 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
-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
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
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
+import com.nexters.boolti.presentation.theme.Grey95
+import com.nexters.boolti.presentation.theme.point1
@Composable
fun ShowFeed(
@@ -52,7 +55,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,
@@ -62,55 +65,39 @@ fun ShowFeed(
contentScale = ContentScale.Crop,
)
- if (showState !is ShowState.TicketingInProgress) {
+ if (showState is ShowState.WaitingTicketing || showState is ShowState.FinishedShow) {
Box(
modifier = Modifier
.fillMaxWidth()
- .aspectRatio(210f / 297f)
+ .aspectRatio(posterRatio)
.background(
brush = SolidColor(Color.Black),
alpha = 0.5f,
)
)
+ } else {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(posterRatio)
+ .background(
+ brush = Brush.verticalGradient(
+ listOf(Color.Transparent, Grey95),
+ startY = 48.dp.toPx(),
+ ),
+ alpha = 0.5f,
+ )
+ )
}
- 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)
- 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)
)
@@ -119,11 +106,46 @@ 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,
)
}
}
+
+@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/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/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/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/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/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)
+ }
}
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..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,10 +10,10 @@ 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, 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/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/DeepLinkEvent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/DeepLinkEvent.kt
new file mode 100644
index 00000000..98ea4ca1
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/DeepLinkEvent.kt
@@ -0,0 +1,20 @@
+package com.nexters.boolti.presentation.screen
+
+import kotlinx.coroutines.channels.BufferOverflow
+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 = MutableSharedFlow(
+ replay = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST,
+ )
+ val events = _events.asSharedFlow()
+
+ suspend fun sendEvent(deepLink: String) {
+ _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 e3cabf53..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
@@ -1,32 +1,53 @@
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.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.shareIn
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() {
+ deepLinkEvent: DeepLinkEvent,
+) : BaseViewModel() {
val loggedIn = authRepository.loggedIn.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
null,
)
+ val event: SharedFlow =
+ deepLinkEvent.events.filter { it.startsWith("https://app.boolti.in/home") }
+ .shareIn(
+ scope = viewModelScope,
+ started = SharingStarted.Lazily,
+ )
+
init {
initUserInfo()
+ sendFcmToken()
}
private fun initUserInfo() {
authRepository.getUserAndCache()
- .catch { }
- .launchIn(viewModelScope)
+ .launchIn(viewModelScope + recordExceptionHandler)
+ }
+
+ private fun sendFcmToken() {
+ viewModelScope.launch {
+ loggedIn.collectLatest {
+ if (it == true) authRepository.sendFcmToken()
+ }
+ }
}
}
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..3b3cf1eb 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,21 +1,31 @@
package com.nexters.boolti.presentation.screen
+import android.content.Intent
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
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 androidx.navigation.navDeepLink
+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
@@ -27,18 +37,45 @@ 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
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,
+ )
+ }
+ }
}
}
}
@@ -47,207 +84,93 @@ 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",
+ startDestination = Home.route,
) {
- composable(
- route = "home",
- ) {
- HomeScreen(
- modifier = modifier,
- onClickShowItem = {
- navController.navigate("show/$it")
- },
- onClickTicket = {
- navController.navigate("tickets/$it")
- },
- onClickQr = {
- navController.navigate("qr/${it.filter { c -> c.isLetterOrDigit() }}")
- },
- onClickQrScan = {
- navController.navigate("hostedShows")
- },
- navigateToReservations = {
- navController.navigate("reservations")
- }
- ) {
- navController.navigate("login")
- }
- }
-
- composable(
- route = "login",
- ) {
- LoginScreen(
- modifier = modifier,
- ) {
- navController.popBackStack()
- }
- }
-
- composable(
- route = "reservations",
- ) {
- ReservationsScreen(onBackPressed = {
- navController.popBackStack()
- }, navigateToDetail = { reservationId ->
- navController.navigate("reservations/$reservationId")
- })
- }
-
- composable(
- route = "reservations/{reservationId}",
- arguments = listOf(navArgument("reservationId") { type = NavType.StringType }),
- ) {
- ReservationDetailScreen(
- onBackPressed = { navController.popBackStack() },
- navigateToRefund = { id -> navController.navigate("refund/$id") },
- )
- }
-
- composable(
- route = "refund/{reservationId}",
- arguments = listOf(navArgument("reservationId") { type = NavType.StringType }),
- ) {
- RefundScreen(
- onBackPressed = { navController.popBackStack() },
- )
- }
+ HomeScreen(modifier = modifier, navigateTo = navController::navigateTo)
+ 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 = "show/{showId}",
+ route = "${ShowDetail.route}/{$showId}",
startDestination = "detail",
- arguments = listOf(navArgument("showId") { type = NavType.StringType }),
- ) {
- 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")
-
- ShowImagesScreen(
- index = index,
- viewModel = showViewModel,
- onBackPressed = { navController.popBackStack() },
- )
- }
- composable(
- route = "content",
- ) { entry ->
- val showViewModel: ShowDetailViewModel =
- entry.sharedViewModel(navController = navController)
-
- ShowDetailContentScreen(
- modifier = modifier,
- viewModel = showViewModel,
- onBackPressed = { navController.popBackStack() }
- )
- }
- composable(
- route = "report/{showId}",
- ) {
- ReportScreen(
- onBackPressed = { navController.popBackStack() },
- popupToHome = { navController.navigateToHome() },
- modifier = modifier,
- )
- }
- }
-
- composable(
- route = "tickets/{ticketId}",
- arguments = listOf(navArgument("ticketId") { type = NavType.StringType }),
- ) {
- TicketDetailScreen(modifier = modifier,
- onBackClicked = { navController.popBackStack() },
- onClickQr = { navController.navigate("qr/${it.filter { c -> c.isLetterOrDigit() }}") },
- navigateToShowDetail = { navController.navigate("show/$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 },
+ arguments = ShowDetail.arguments,
+ deepLinks = listOf(
+// 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
+ },
),
) {
- TicketingScreen(
+ ShowDetailScreen(
modifier = modifier,
- onBackClicked = { navController.popBackStack() },
- onReserved = { reservationId, showId ->
- navController.navigate("payment/$reservationId?showId=$showId")
- }
+ navigateTo = navController::navigateTo,
+ popBackStack = navController::popBackStack,
+ navigateToHome = navController::navigateToHome,
+ getSharedViewModel = { entry -> entry.sharedViewModel(navController) }
)
- }
-
- composable(
- route = "qr/{data}",
- arguments = listOf(navArgument("data") { type = NavType.StringType }),
- ) {
- QrFullScreen(modifier = modifier) {
- navController.popBackStack()
- }
- }
- composable(
- route = "hostedShows"
- ) {
- HostedShowScreen(
+ ShowImagesScreen(
+ popBackStack = navController::popBackStack,
+ getSharedViewModel = { entry -> entry.sharedViewModel(navController) }
+ )
+ ShowDetailContentScreen(
modifier = modifier,
- onClickShow = onClickQrScan,
- onClickBack = {
- navController.popBackStack()
- }
+ popBackStack = navController::popBackStack,
+ getSharedViewModel = { entry -> entry.sharedViewModel(navController) }
)
- }
-
- composable(
- route = "payment/{reservationId}?showId={showId}",
- arguments = listOf(
- navArgument("reservationId") { type = NavType.StringType },
- navArgument("showId") { type = NavType.StringType }),
- ) {
- 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()
- },
+ ReportScreen(
+ modifier = modifier,
+ navigateToHome = navController::navigateToHome,
+ popBackStack = navController::popBackStack,
)
}
+
+ 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,
+ popBackStack = navController::popBackStack,
+ )
+
+ PaymentScreen(
+ navigateTo = navController::navigateTo,
+ popBackStack = navController::popBackStack,
+ popInclusiveBackStack = { route ->
+ navController.popBackStack(
+ route = route,
+ inclusive = true,
+ )
+ },
+ navigateToHome = navController::navigateToHome,
+ )
+ BusinessScreen(popBackStack = navController::popBackStack)
}
}
@@ -261,3 +184,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/MainActivity.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainActivity.kt
index 8569df68..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
@@ -1,5 +1,9 @@
package com.nexters.boolti.presentation.screen
+import android.Manifest
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -7,10 +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() {
@@ -18,7 +28,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 +43,34 @@ class MainActivity : ComponentActivity() {
}
}
}
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ requestPermission(Manifest.permission.POST_NOTIFICATIONS, 101)
+ }
+
+ createDefaultFcmChannel()
+ subscribeDefaultTopic()
+ }
+
+ 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)
+ }
+
+ private fun subscribeDefaultTopic() {
+ val defaultTopic = if (BuildConfig.DEBUG) "dev" else "prod"
+
+ Firebase.messaging.subscribeToTopic(defaultTopic)
+ .addOnCompleteListener { task ->
+ if (!task.isSuccessful) {
+ Timber.d("구독 실패")
+ }
+ }
}
}
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..26a8ae24
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/MainDestination.kt
@@ -0,0 +1,65 @@
+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 = "show") {
+ 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")
+ data object Business : MainDestination(route = "business")
+}
+
+/**
+ * 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"
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/business/BusinessScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/business/BusinessScreen.kt
new file mode 100644
index 00000000..cf1a7aca
--- /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.BtBackAppBar
+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 = {
+ BtBackAppBar(
+ title = stringResource(id = R.string.business_title),
+ onClickBack = onBackPressed,
+ )
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .padding(horizontal = marginHorizontal)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Text(
+ modifier = Modifier.padding(top = 20.dp, bottom = 16.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 = {})
+}
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..186dfce7
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeNavigation.kt
@@ -0,0 +1,31 @@
+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) },
+ 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 cc41e6c9..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
@@ -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
@@ -27,6 +30,7 @@ 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.my.MyScreen
@@ -36,17 +40,20 @@ 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(
- modifier: Modifier,
- viewModel: HomeViewModel = hiltViewModel(),
onClickShowItem: (showId: String) -> Unit,
onClickTicket: (ticketId: String) -> Unit,
- onClickQr: (data: String) -> Unit,
+ onClickQr: (data: String, ticketName: String) -> Unit,
onClickQrScan: () -> Unit,
+ onClickSignout: () -> Unit,
navigateToReservations: () -> Unit,
+ navigateToBusiness: () -> Unit,
requireLogin: () -> Unit,
+ modifier: Modifier,
+ viewModel: HomeViewModel = hiltViewModel(),
) {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
@@ -54,6 +61,12 @@ fun HomeScreen(
val loggedIn by viewModel.loggedIn.collectAsStateWithLifecycle()
+ LaunchedEffect(Unit) {
+ viewModel.event.collect { deepLink ->
+ navController.navigate(Uri.parse(deepLink))
+ }
+ }
+
Scaffold(
bottomBar = {
HomeNavigationBar(
@@ -80,10 +93,17 @@ fun HomeScreen(
modifier = modifier.padding(innerPadding),
onClickShowItem = onClickShowItem,
navigateToReservations = navigateToReservations,
+ navigateToBusiness = navigateToBusiness,
)
}
composable(
route = Destination.Ticket.route,
+ deepLinks = listOf(
+ navDeepLink {
+ uriPattern = "https://app.boolti.in/home/tickets"
+ action = Intent.ACTION_VIEW
+ }
+ )
) {
when (loggedIn) {
true -> TicketScreen(
@@ -92,7 +112,11 @@ fun HomeScreen(
modifier = modifier.padding(innerPadding),
)
- false -> TicketLoginScreen(modifier.padding(innerPadding), onLoginClick = requireLogin)
+ false -> TicketLoginScreen(
+ modifier.padding(innerPadding),
+ onLoginClick = requireLogin
+ )
+
else -> Unit // 로그인 여부를 불러오는 중
}
}
@@ -104,6 +128,7 @@ fun HomeScreen(
requireLogin = requireLogin,
navigateToReservations = navigateToReservations,
onClickQrScan = onClickQrScan,
+ onClickSignout = onClickSignout,
)
}
}
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/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 0fae236d..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
@@ -24,20 +20,23 @@ 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
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
+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.BtCloseableAppBar
import com.nexters.boolti.presentation.component.KakaoLoginButton
import com.nexters.boolti.presentation.component.MainButton
import com.nexters.boolti.presentation.theme.Grey30
@@ -47,25 +46,21 @@ import com.nexters.boolti.presentation.theme.subTextPadding
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
+ onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
viewModel: LoginViewModel = hiltViewModel(),
- onBackPressed: () -> Unit,
) {
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,8 +83,15 @@ fun LoginScreen(
}
}
+ if (showSignOutCancelledDialog) {
+ SignOutCancelledDialog {
+ showSignOutCancelledDialog = false
+ onBackPressed()
+ }
+ }
+
Scaffold(
- topBar = { LoginAppBar(onBackPressed = onBackPressed) },
+ topBar = { BtCloseableAppBar(onClickClose = onBackPressed) },
containerColor = MaterialTheme.colorScheme.background,
) { innerPadding ->
Box(
@@ -122,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,
@@ -162,7 +139,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 +174,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/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt
index 814567a8..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
@@ -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
@@ -8,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
@@ -26,7 +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.LocalContext
+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
@@ -48,13 +46,13 @@ 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) }
+ val uriHandler = LocalUriHandler.current
LaunchedEffect(Unit) {
viewModel.fetchMyInfo()
@@ -70,34 +68,39 @@ 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 },
)
}
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 +199,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/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/payment/AccountTransferContent.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/AccountTransferContent.kt
index 76953c6e..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
@@ -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,18 +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,
ticketName = reservation.ticketName,
ticketCount = reservation.ticketCount,
- price = reservation.totalAmountPrice,
+ totalPrice = reservation.totalAmountPrice,
)
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..264e6280 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,126 @@
package com.nexters.boolti.presentation.screen.payment
+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.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.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)
+ val scrollState = rememberScrollState()
+ Box(
+ modifier = modifier.fillMaxSize(),
) {
- 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,
- )
+ 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(
+ 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),
+ )
+ }
+ 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
+ ), // TODO (카카오뱅크카드 / 일시불) 형태의 정보 추가
+ )
+ InfoRow(
+ modifier = Modifier.padding(top = 16.dp),
+ 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 = reservation.showDate,
+ )
+ }
+
+ // TODO 백엔드에 TicketId 요청 필요 <-- 1.5.0 에 주석 제거
+ /*Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.BottomCenter)
+ .padding(horizontal = marginHorizontal)
+ .padding(bottom = 20.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ SecondaryButton(
+ modifier = Modifier.weight(1f),
+ label = stringResource(R.string.show_reservation),
+ ) {
+ navigateToReservation(reservation)
+ }
+ MainButton(
+ modifier = Modifier.weight(1f),
+ 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 +129,70 @@ 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",
+ showDate = LocalDateTime.now(),
+ 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/PaymentNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentNavigation.kt
new file mode 100644
index 00000000..30b635f2
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/PaymentNavigation.kt
@@ -0,0 +1,32 @@
+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()
+ },
+ 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 ffde4b4c..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,13 +23,18 @@ 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
fun PaymentScreen(
onClickHome: () -> Unit,
onClickClose: () -> Unit,
+ navigateToReservation: (reservation: ReservationDetail) -> Unit,
+ navigateToTicketDetail: (reservation: ReservationDetail) -> Unit,
viewModel: PaymentViewModel = hiltViewModel(),
) {
val snackbarHostState = remember { SnackbarHostState() }
@@ -41,7 +49,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) {
@@ -49,10 +60,14 @@ 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
+ reservation = reservation,
+ navigateToReservation = navigateToReservation,
+ navigateToTicketDetail = navigateToTicketDetail,
)
else -> ProgressPayment(
@@ -92,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/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/payment/TicketSummarySection.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/payment/TicketSummarySection.kt
index 10686416..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
@@ -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,21 +17,75 @@ 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.domain.model.PaymentType
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,
+ showDate: LocalDateTime,
+) {
+ Card(
+ modifier = modifier,
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
+ shape = RoundedCornerShape(4.dp),
+ ) {
+ Row(
+ modifier = Modifier.padding(vertical = 16.dp, horizontal = marginHorizontal),
+ 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,
+ )
+ Text(
+ modifier = Modifier.padding(top = 4.dp),
+ text = showDate.showDateTimeString,
+ color = Grey30,
+ style = MaterialTheme.typography.bodySmall,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun LegacyTicketSummarySection(
modifier: Modifier = Modifier,
poster: String,
showName: String,
ticketName: String,
ticketCount: Int,
- price: Int,
+ totalPrice: Int,
) {
Row(
modifier = modifier,
@@ -48,24 +105,62 @@ fun TicketSummarySection(
contentScale = ContentScale.Crop,
)
Column(
- modifier = Modifier.padding(start = 16.dp),
+ modifier = Modifier.padding(start = 16.dp)
) {
Text(
text = showName,
style = point1,
color = Grey05,
)
+
Text(
modifier = Modifier.padding(top = 4.dp),
- text = "$ticketName / ${stringResource(R.string.ticket_count, ticketCount)}",
- style = MaterialTheme.typography.labelMedium,
+ text = stringResource(
+ id = R.string.reservation_ticket_info_format,
+ ticketName,
+ ticketCount
+ ),
color = Grey30,
+ style = MaterialTheme.typography.bodySmall,
)
+
Text(
modifier = Modifier.padding(top = 4.dp),
- text = stringResource(R.string.unit_won, price),
- style = MaterialTheme.typography.labelMedium,
+ text = stringResource(id = R.string.unit_won, totalPrice),
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(),
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun LegacyTicketSummaryPreview() {
+ BooltiTheme {
+ Surface {
+ LegacyTicketSummarySection(
+ modifier = Modifier.padding(24.dp),
+ poster = "",
+ showName = "2024 TOGETHER LUCKY CLUB",
+ ticketName = "일반 티켓 B",
+ ticketCount = 10,
+ totalPrice = 30000,
)
}
}
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 96e2a37d..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
@@ -36,24 +32,30 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nexters.boolti.domain.model.Show
import com.nexters.boolti.presentation.R
+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
-import com.nexters.boolti.presentation.theme.aggroFamily
+import com.nexters.boolti.presentation.theme.point1
import java.time.LocalDate
import java.time.LocalDateTime
@Composable
fun HostedShowScreen(
- modifier: Modifier = Modifier,
onClickBack: () -> Unit,
onClickShow: (showId: String, showName: String) -> Unit,
+ modifier: Modifier = Modifier,
viewModel: HostedShowViewModel = hiltViewModel(),
) {
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))
@@ -69,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,
@@ -128,9 +106,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/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/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 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/qr/QrScanScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/qr/QrScanScreen.kt
index bb4b9506..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,8 +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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.navigation.compose.hiltViewModel
@@ -42,8 +38,13 @@ 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
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 +55,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 +72,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)
}
}
@@ -88,13 +91,31 @@ fun QrScanScreen(
Scaffold(
topBar = {
- QrScanToolbar(showName = uiState.showName, onClickClose = onClickClose)
+ BtCloseableAppBar(
+ title = uiState.showName,
+ onClickClose = onClickClose,
+ )
},
bottomBar = {
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(
@@ -113,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/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..6f225f5f
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/refund/RefundInfoPage.kt
@@ -0,0 +1,444 @@
+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.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.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.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
+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,
+ 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) }
+ 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),
+ )
+ }
+ }
+ }
+
+ 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
+ )
+ }
+ }
+
+ 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(top = 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(top = 16.dp)
+ .padding(vertical = 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
+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),
+ )
+ }
+}
+
+@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/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 3557dc7e..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
@@ -1,44 +1,20 @@
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
-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.HorizontalDivider
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
@@ -48,67 +24,41 @@ 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.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 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.theme.Error
-import com.nexters.boolti.presentation.theme.Grey10
+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
-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)
@Composable
fun RefundScreen(
onBackPressed: () -> Unit,
- modifier: Modifier = Modifier,
viewModel: RefundViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val refundPolicy by viewModel.refundPolicy.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()
}
}
@@ -117,11 +67,12 @@ 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,
+ modifier = Modifier,
) { innerPadding ->
val reservation = uiState.reservation ?: return@Scaffold
@@ -144,6 +95,7 @@ fun RefundScreen(
} else {
RefundInfoPage(
uiState = uiState,
+ refundPolicy = refundPolicy,
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
@@ -153,6 +105,7 @@ fun RefundScreen(
onBankInfoChanged = viewModel::updateBankInfo,
onAccountNumberChanged = viewModel::updateAccountNumber,
onRequest = { openDialog = true },
+ onRefundPolicyChecked = viewModel::toggleRefundPolicyCheck,
)
}
}
@@ -184,7 +137,7 @@ fun RefundScreen(
value = uiState.name
)
InfoRow(
- modifier = Modifier.padding(top = 8.dp),
+ modifier = Modifier.padding(top = 12.dp),
type = stringResource(id = R.string.contact_label),
value = StringBuilder(uiState.contact).apply {
if (uiState.contact.length > 7) {
@@ -194,472 +147,31 @@ fun RefundScreen(
}.toString()
)
InfoRow(
- modifier = Modifier.padding(top = 8.dp),
+ modifier = Modifier.padding(top = 12.dp),
type = stringResource(id = R.string.bank_name),
value = uiState.bankInfo?.bankName ?: ""
)
InfoRow(
- modifier = Modifier.padding(top = 8.dp),
+ modifier = Modifier.padding(top = 12.dp),
type = stringResource(id = R.string.account_number),
value = uiState.accountNumber
)
- }
- }
- }
- }
-}
-
-@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 showNameError by remember { mutableStateOf(false) }
- var showAccountError by remember { mutableStateOf(false) }
- var showContactError 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)
- .onFocusChanged { focusState ->
- showNameError = uiState.name.isNotEmpty() &&
- !uiState.isValidName &&
- !focusState.isFocused
- },
- isError = showNameError,
- text = uiState.name,
- singleLine = true,
- keyboardOptions = KeyboardOptions(
- keyboardType = KeyboardType.Text,
- imeAction = ImeAction.Next
- ),
- placeholder = stringResource(id = R.string.refund_account_name_hint),
- 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),
- 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)
- .onFocusChanged { focusState ->
- showContactError =
- uiState.contact.isNotEmpty() &&
- !uiState.isValidContact &&
- !focusState.isFocused
- },
- 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(
- 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 = 12.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(
+ HorizontalDivider(
modifier = Modifier.padding(top = 12.dp),
- text = stringResource(id = R.string.validation_account),
- style = MaterialTheme.typography.bodySmall.copy(color = Error),
+ thickness = 1.dp,
+ color = Grey70,
)
- }
- }
- }
-
- 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,
+ InfoRow(
+ modifier = Modifier.padding(top = 12.dp),
+ type = stringResource(id = R.string.refund_price),
+ value = stringResource(
+ id = R.string.unit_won,
+ uiState.reservation!!.totalAmountPrice,
)
- }
+ )
}
}
}
- 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,
- )
- }
}
}
@@ -678,10 +190,9 @@ fun InfoRow(
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
+}
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..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,23 +9,23 @@ data class RefundUiState(
val bankInfo: BankInfo? = null,
val accountNumber: String = "",
val reservation: ReservationDetail? = null,
+ val refundPolicyChecked: Boolean = false
) {
- 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()
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 d46bf03f..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
@@ -1,10 +1,11 @@
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.domain.usecase.GetRefundPolicyUsecase
+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 +13,19 @@ 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() {
+ 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) {
@@ -49,10 +55,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 +69,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 +91,16 @@ class RefundViewModel @Inject constructor(
fun updateAccountNumber(newAccountNumber: String) {
_uiState.update { it.copy(accountNumber = newAccountNumber) }
}
-}
\ No newline at end of file
+
+ 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/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 5a099ec7..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
@@ -8,18 +8,13 @@ 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
@@ -28,12 +23,9 @@ 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.component.BtAppBar
+import com.nexters.boolti.presentation.component.BtBackAppBar
import com.nexters.boolti.presentation.component.MainButton
-import com.nexters.boolti.presentation.theme.Grey10
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
@@ -58,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(
@@ -103,4 +95,4 @@ fun ReportScreen(
)
}
}
-}
\ No newline at end of file
+}
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 e757f6d3..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
@@ -24,17 +24,14 @@ 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.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
@@ -45,6 +42,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
@@ -54,10 +52,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.CopyButton
-import com.nexters.boolti.presentation.component.ToastSnackbarHost
+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
import com.nexters.boolti.presentation.theme.Grey10
import com.nexters.boolti.presentation.theme.Grey15
import com.nexters.boolti.presentation.theme.Grey20
@@ -67,7 +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 kotlinx.coroutines.launch
+import java.time.LocalDateTime
@Composable
fun ReservationDetailScreen(
@@ -78,8 +76,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,13 +83,13 @@ fun ReservationDetailScreen(
Scaffold(
modifier = modifier,
- snackbarHost = {
- ToastSnackbarHost(
- modifier = Modifier.padding(bottom = 80.dp),
- hostState = snackbarHostState,
+ topBar = {
+ BtBackAppBar(
+ title = stringResource(id = R.string.reservation_detail),
+ onClickBack = onBackPressed,
)
},
- topBar = { ReservationDetailAppBar(onBackPressed = onBackPressed) }) { innerPadding ->
+ ) { innerPadding ->
val state = uiState
if (state !is ReservationDetailUiState.Success) return@Scaffold
@@ -104,29 +100,45 @@ fun ReservationDetailScreen(
.padding(innerPadding)
.verticalScroll(scrollState)
) {
- Text(
- modifier = Modifier
- .padding(horizontal = marginHorizontal)
- .padding(top = 12.dp),
- text = "No. ${state.reservation.id}",
- 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,
- showMessage = { message ->
- scope.launch { snackbarHostState.showSnackbar(message) }
- })
+ DepositInfo(reservation = state.reservation)
}
- PaymentInfo(reservation = state.reservation)
- TicketInfo(reservation = state.reservation)
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)
+ }
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),
@@ -137,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,
@@ -211,50 +193,59 @@ 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),
) {
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) {
- showMessage(copiedMessage)
+ 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),
@@ -265,33 +256,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,
@@ -299,7 +263,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)
@@ -307,23 +271,46 @@ private fun PaymentInfo(
else -> stringResource(id = R.string.reservations_unknown)
}
- val (stateStringId, _) = reservation.reservationState.toDescriptionAndColorPair()
-
Column {
NormalRow(
- modifier = Modifier.padding(bottom = 8.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
)
+ }
+ }
+}
+
+@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(top = 8.dp, bottom = 10.dp),
- key = stringResource(id = R.string.total_payment_amount_label),
- value = stringResource(id = R.string.unit_won, reservation.totalAmountPrice)
+ 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.payment_state_label),
- value = stringResource(id = stateStringId)
+ key = stringResource(id = R.string.refund_method),
+ value = paymentType,
)
}
}
@@ -352,12 +339,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),
- )
}
}
}
@@ -549,4 +530,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/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/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 4180fab3..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
@@ -35,7 +35,7 @@ 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.theme.Grey05
@@ -52,21 +52,17 @@ import java.time.format.DateTimeFormatter
fun ReservationsScreen(
onBackPressed: () -> Unit,
navigateToDetail: (reservationId: String) -> Unit,
- modifier: Modifier = Modifier,
viewModel: ReservationsViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
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(
- modifier = modifier
+ modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
) {
@@ -236,4 +232,4 @@ fun ReservationStateLabel(
text = stringResource(id = stringId),
style = MaterialTheme.typography.bodySmall.copy(color = color),
)
-}
\ No newline at end of file
+}
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/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 b85253e5..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,29 +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 com.nexters.boolti.presentation.R
-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
@@ -38,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
@@ -51,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),
- )
- }
-}
\ No newline at end of file
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/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 cd59ec8a..30518e75 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,7 +1,9 @@
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
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -16,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
@@ -24,13 +25,11 @@ 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.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
@@ -45,31 +44,35 @@ 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
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.google.firebase.Firebase
+import com.google.firebase.dynamiclinks.androidParameters
+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.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
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
@@ -95,23 +98,36 @@ 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
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,
- snackbarHost = {
- ToastSnackbarHost(
- modifier = Modifier.padding(bottom = 80.dp),
- hostState = snackbarHostState,
- )
- },
topBar = {
ShowDetailAppBar(
- onBack = onBack,
+ showId = uiState.showDetail.id,
+ onBack = { viewModel.sendEvent(ShowDetailEvent.PopBackStack) },
onClickHome = onClickHome,
navigateToReport = navigateToReport,
)
@@ -129,7 +145,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 }
)
@@ -137,7 +153,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,
@@ -167,12 +182,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) {
@@ -207,85 +216,73 @@ fun ShowDetailScreen(
@Composable
private fun ShowDetailAppBar(
+ showId: String,
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 = {
- 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"
- }
+ },
+ actionButtons = {
+ BtAppBarDefaults.AppBarIconButton(
+ iconRes = R.drawable.ic_share,
+ description = stringResource(id = R.string.ticketing_share),
+ onClick = {
+ Firebase.dynamicLinks.shortLinkAsync {
+ val uri = Uri.parse("https://preview.boolti.in/show/$showId")
+ link = uri
+ domainUriPrefix = "https://boolti.page.link"
- 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),
+ androidParameters {
+ fallbackUrl = uri
+ }
+ iosParameters("com.nexters.boolti") {
+ setFallbackUrl(uri)
+ }
+ }.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)
+ }
+ }
+ },
)
- }
- 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
@@ -315,13 +312,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 +357,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)
}
}
)
@@ -448,10 +442,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,
)
}
}
@@ -498,25 +489,16 @@ private fun SectionContent(
@Composable
fun ShowDetailCtaButton(
onClick: () -> Unit,
- showMessage: (message: String) -> 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)
}
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..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
@@ -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,8 @@ 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 +56,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/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 e8260185..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
@@ -25,8 +25,9 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
-import com.nexters.boolti.presentation.R
-import com.nexters.boolti.presentation.component.BtAppBar
+import com.nexters.boolti.presentation.component.BtCloseableAppBar
+import net.engawapg.lib.zoomable.rememberZoomState
+import net.engawapg.lib.zoomable.zoomable
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -43,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(
@@ -67,7 +62,8 @@ fun ShowImagesScreen(
) {
AsyncImage(
modifier = Modifier
- .fillMaxWidth(),
+ .fillMaxWidth()
+ .zoomable(rememberZoomState()),
model = uiState.showDetail.images[it].originImage,
contentDescription = null,
contentScale = ContentScale.FillWidth,
@@ -100,4 +96,4 @@ private fun Indicator(
)
}
}
-}
\ 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 51a76114..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
@@ -15,7 +15,9 @@ 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
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
@@ -43,32 +45,30 @@ 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.text.style.TextAlign
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
+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
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(
navigateToReservations: () -> Unit,
+ navigateToBusiness: () -> Unit,
onClickShowItem: (showId: String) -> Unit,
modifier: Modifier = Modifier,
viewModel: ShowViewModel = hiltViewModel()
@@ -76,12 +76,12 @@ 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) }
- var changeableAppBarHeight by remember { mutableFloatStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
@@ -89,6 +89,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)
+ }
}
}
@@ -111,6 +120,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),
@@ -125,12 +135,21 @@ fun ShowScreen(
.clickable { onClickShowItem(uiState.shows[index].id) },
)
}
+
+ item(
+ span = { GridItemSpan(2) },
+ ) {
+ BusinessInformation(
+ modifier = Modifier.padding(bottom = 12.dp),
+ onClick = navigateToBusiness
+ )
+ }
}
ShowAppBar(
modifier = Modifier.offset {
IntOffset(
x = 0,
- y = appbarOffsetHeightPx.coerceIn(-changeableAppBarHeightPx, 0f).toInt()
+ y = appbarOffsetHeightPx.coerceAtLeast(-changeableAppBarHeightPx).toInt(),
)
},
navigateToReservations = navigateToReservations,
@@ -138,18 +157,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 +170,14 @@ 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()
+ .background(MaterialTheme.colorScheme.background)
.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(
@@ -182,12 +189,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
@@ -281,4 +283,4 @@ private fun Banner(
)
Icon(painter = painterResource(id = R.drawable.ic_arrow_right), contentDescription = null)
}
-}
\ 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/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,
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/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/SignoutNotice.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNotice.kt
new file mode 100644
index 00000000..9b3bbf02
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutNotice.kt
@@ -0,0 +1,55 @@
+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(
+ 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/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..5d7c97da
--- /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.BtBackAppBar
+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 = { BtBackAppBar(title = stringResource(R.string.signout), onClickBack = 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..04ebb3f0
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/signout/SignoutViewModel.kt
@@ -0,0 +1,46 @@
+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
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class SignoutViewModel @Inject constructor(
+ private val repository: AuthRepository,
+) : ViewModel() {
+ private val _reason = MutableStateFlow("")
+ val reason = _reason.asStateFlow()
+
+ private val _event = Channel()
+ val event = _event.receiveAsFlow()
+
+ fun signout() {
+ viewModelScope.launch {
+ repository.signout(
+ request = SignoutRequest(reason.value)
+ ).onSuccess {
+ event(SignoutEvent.SignoutSuccess)
+ }.onFailure {
+ it.printStackTrace()
+ }
+ }
+ }
+
+ fun setReason(reason: String) {
+ _reason.value = reason
+ }
+
+ private fun event(event: SignoutEvent) {
+ viewModelScope.launch {
+ _event.send(event)
+ }
+ }
+}
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..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
@@ -21,21 +21,17 @@ 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.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
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
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.launch
-import javax.inject.Inject
@AndroidEntryPoint
class SplashActivity : ComponentActivity() {
@@ -52,10 +48,21 @@ class SplashActivity : ComponentActivity() {
shouldUpdate = shouldUpdate,
onSuccessVersionCheck = {
startActivity(Intent(this, MainActivity::class.java))
+ intent.extras?.getString("type")?.let { type ->
+ val notification = BtNotification(type)
+
+ notification.deepLink?.let {
+ viewModel.sendDeepLinkEvent(it)
+ }
+
+ NotificationManagerCompat.from(this).cancel(notification.id)
+ }
+
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 +128,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)
+ }
+ }
+}
+
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..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
@@ -25,7 +26,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 +39,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
@@ -53,6 +54,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
@@ -65,7 +67,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
@@ -130,7 +132,7 @@ fun TicketContent(
),
)
Column {
- Title(ticket.ticketName, 1) // TODO 개수 정보 생기면 업데이트
+ Title(ticket.ticketName, ticket.csTicketId)
AsyncImage(
modifier = Modifier
.fillMaxWidth()
@@ -138,7 +140,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(
@@ -155,7 +157,7 @@ fun TicketContent(
placeName = ticket.placeName,
entryCode = ticket.entryCode,
ticketState = ticket.ticketState,
- onClickQr = onClickQr,
+ onClickQr = { onClickQr(it, ticket.ticketName) },
)
}
// 티켓 좌상단 꼭지점 그라데이션
@@ -177,24 +179,31 @@ 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,
) {
+ Icon(
+ modifier = Modifier.padding(end = 4.dp).size(20.dp),
+ painter = painterResource(R.drawable.ic_logo),
+ tint = Grey80,
+ contentDescription = null,
+ )
Text(
modifier = Modifier.weight(1f),
- text = if (count > 1) stringResource(R.string.ticket_title, ticketName, count) else ticketName,
- style = MaterialTheme.typography.bodySmall,
+ text = ticketName,
+ 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,
)
}
}
@@ -291,7 +300,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/TicketScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticket/TicketScreen.kt
index ea8d313e..f479ce81 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,12 +28,13 @@ 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
fun TicketScreen(
onClickTicket: (String) -> Unit,
- onClickQr: (entryCode: String) -> Unit,
+ onClickQr: (entryCode: String, ticketName: String) -> Unit,
modifier: Modifier = Modifier,
viewModel: TicketViewModel = hiltViewModel(),
) {
@@ -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)
}
}
@@ -55,7 +66,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/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..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,10 +3,12 @@ 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
import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -15,19 +17,23 @@ import javax.inject.Inject
@HiltViewModel
class TicketViewModel @Inject constructor(
private val ticketRepository: TicketRepository,
-) : ViewModel() {
- private val _uiState = MutableStateFlow(TicketUiState())
+) : BaseViewModel() {
+ 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)
+ viewModelScope.launch(recordExceptionHandler) {
+ _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)
+ }
}
- }
}
}
}
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..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
@@ -69,6 +66,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,16 +75,21 @@ 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.NavGraphBuilder
+import androidx.navigation.compose.composable
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
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,13 +107,35 @@ import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.LocalDateTime
+fun NavGraphBuilder.TicketDetailScreen(
+ navigateTo: (String) -> Unit,
+ popBackStack: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ composable(
+ route = "${MainDestination.TicketDetail.route}/{$ticketId}",
+ arguments = MainDestination.TicketDetail.arguments,
+ ) {
+ TicketDetailScreen(
+ modifier = modifier,
+ onBackClicked = popBackStack,
+ onClickQr = { code, ticketName ->
+ navigateTo(
+ "${MainDestination.Qr.route}/${code.filter { c -> c.isLetterOrDigit() }}?ticketName=$ticketName"
+ )
+ },
+ navigateToShowDetail = { navigateTo("${MainDestination.ShowDetail.route}/$it") }
+ )
+ }
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun TicketDetailScreen(
+private 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()
@@ -125,7 +150,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,14 +163,17 @@ fun TicketDetailScreen(
LaunchedEffect(viewModel.event) {
viewModel.event.collect {
when (it) {
- TicketDetailEvent.ManagerCodeValid -> snackbarHostState.showSnackbar(entranceSuccessMsg)
+ TicketDetailEvent.ManagerCodeValid -> snackbarHostState.showSnackbar(
+ entranceSuccessMsg
+ )
+
TicketDetailEvent.OnRefresh -> showEnterCodeDialog = false
}
}
}
Scaffold(
- topBar = { TicketDetailToolbar(onBackClicked = onBackClicked) },
+ topBar = { BtBackAppBar(onClickBack = onBackClicked) },
snackbarHost = {
ToastSnackbarHost(
modifier = Modifier.padding(bottom = 54.dp),
@@ -181,7 +210,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,19 +241,26 @@ 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)
+ Title(ticketName = ticket.ticketName, csTicketId = ticket.csTicketId)
AsyncImage(
modifier = Modifier
@@ -246,7 +287,7 @@ fun TicketDetailScreen(
placeName = ticket.placeName,
entryCode = ticket.entryCode,
ticketState = ticket.ticketState,
- onClickQr = onClickQr,
+ onClickQr = { onClickQr(it, ticket.ticketName) },
)
}
// 티켓 좌상단 꼭지점 그라데이션
@@ -266,12 +307,13 @@ 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,
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)
@@ -333,47 +375,34 @@ 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 = "",
+ 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(Grey70.copy(alpha = 0.5f)),
+ 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,
)
}
}
@@ -458,7 +487,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)
@@ -695,7 +724,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/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..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
@@ -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 가 전달되지 않았습니다."
}
@@ -45,18 +47,14 @@ 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) }
}
- getRefundPolicyUsecase().catch { e ->
- e.printStackTrace()
- }.onEach { refundPolicy ->
+ getRefundPolicyUsecase().onEach { refundPolicy ->
_uiState.update { it.copy(refundPolicy = refundPolicy) }
- }.launchIn(viewModelScope)
+ }.launchIn(viewModelScope + recordExceptionHandler)
}
}
@@ -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/ChooseTicketBottomSheet.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/ChooseTicketBottomSheet.kt
index 22a98873..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
@@ -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,27 +92,24 @@ 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),
- modifier = Modifier
- .padding(top = 20.dp, start = 24.dp, end = 24.dp, bottom = 12.dp)
- )
uiState.selected?.let {
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,19 +121,27 @@ fun ChooseTicketBottomSheet(
@Composable
private fun ChooseTicketBottomSheetContent1(
modifier: Modifier = Modifier,
- listState: LazyListState,
tickets: List,
onSelectItem: (TicketWithQuantity) -> Unit,
) {
- LazyColumn(
- modifier = modifier.nestedScroll(rememberNestedScrollInteropConnection()),
- state = listState
- ) {
- items(tickets, key = { it.ticket.id }) {
- TicketingTicketItem(
- ticket = it,
- onClick = onSelectItem,
- )
+ val listState = rememberLazyListState()
+ 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,
+ )
+ }
}
}
}
@@ -143,34 +151,28 @@ 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),
- verticalAlignment = Alignment.CenterVertically
+ modifier = Modifier
+ .padding(top = 16.dp, end = 8.dp, start = 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),
- modifier = Modifier.padding(start = 8.dp),
- )
- }
- }
- Text(
- text = stringResource(R.string.format_price, ticket.ticket.price),
- style = MaterialTheme.typography.bodyLarge.copy(
- color = Grey15,
- ),
- modifier = Modifier.padding(top = 12.dp),
+ 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))
@@ -182,21 +184,46 @@ private fun ChooseTicketBottomSheetContent2(
)
}
}
+ Row(
+ modifier = Modifier.padding(top = 8.dp, bottom = 20.dp, start = 24.dp, end = 24.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),
+ )
+ }
- 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),
- style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary),
+ text = stringResource(R.string.format_total_price, ticket.ticket.price * ticketCount),
+ style = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.primary),
)
}
@@ -206,7 +233,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 +244,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 +255,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 +275,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 +284,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/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..08a0adf8
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingConfirmDialog.kt
@@ -0,0 +1,145 @@
+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 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
+
+@Composable
+fun TicketingConfirmDialog(
+ isInviteTicket: Boolean,
+ 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),
+ style = MaterialTheme.typography.headlineSmall
+ )
+ Column(
+ modifier = Modifier
+ .padding(top = 24.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.secondaryContainer)
+ .padding(horizontal = marginHorizontal, vertical = 16.dp),
+ ) {
+ // 예매자
+ InfoRow(
+ label = stringResource(R.string.ticket_holder),
+ value1 = reservationName,
+ value2 = reservationContact.toContactFormat(),
+ )
+ // 입금자
+ if (!isInviteTicket && totalPrice > 0) {
+ InfoRow(
+ modifier = Modifier.padding(top = 16.dp),
+ label = stringResource(R.string.depositor),
+ value1 = depositor,
+ value2 = depositorContact.toContactFormat(),
+ )
+ }
+ // 티켓
+ 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),
+ )
+ // 결제 수단
+ if (isInviteTicket || totalPrice > 0) {
+ InfoRow(
+ modifier = Modifier.padding(top = 16.dp),
+ label = stringResource(R.string.payment_type_label),
+ value1 = if (isInviteTicket) {
+ stringResource(R.string.invite_code_label)
+ } else {
+ when (paymentType) {
+ PaymentType.ACCOUNT_TRANSFER -> stringResource(R.string.payment_account_transfer)
+ PaymentType.CARD -> stringResource(R.string.payment_card)
+ PaymentType.UNDEFINED -> ""
+ }
+ },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun InfoRow(
+ modifier: Modifier = Modifier,
+ label: String,
+ value1: String,
+ value2: String? = null,
+) {
+ 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 InfoRowPreview() {
+ Surface {
+ BooltiTheme {
+ InfoRow(
+ modifier = Modifier.fillMaxWidth(),
+ label = "예매자",
+ value1 = "박명범",
+ value2 = "01020302030"
+ )
+ }
+ }
+}
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..6acf1033
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/ticketing/TicketingNavigation.kt
@@ -0,0 +1,30 @@
+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")
+ },
+ 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 cf761671..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
@@ -55,10 +52,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
@@ -68,71 +67,68 @@ 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
-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
+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
import kotlinx.coroutines.launch
import java.time.LocalDateTime
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TicketingScreen(
modifier: Modifier = Modifier,
viewModel: TicketingViewModel = hiltViewModel(),
onBackClicked: () -> Unit = {},
onReserved: (reservationId: String, showId: String) -> Unit,
+ navigateToBusiness: () -> Unit,
) {
val scrollState = rememberScrollState()
val snackbarHostState = remember { SnackbarHostState() }
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 {
when (it) {
- is TicketingEvent.TicketingSuccess -> onReserved(it.reservationId, it.showId)
+ is TicketingEvent.TicketingSuccess -> {
+ showConfirmDialog = false
+ onReserved(it.reservationId, it.showId)
+ }
}
}
}
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 = {
- ToastSnackbarHost(hostState = snackbarHostState, modifier = Modifier.padding(bottom = 100.dp))
+ ToastSnackbarHost(
+ hostState = snackbarHostState,
+ modifier = Modifier.padding(bottom = 100.dp)
+ )
}
) { innerPadding ->
Box(modifier = modifier.padding(innerPadding)) {
@@ -146,34 +142,76 @@ fun TicketingScreen(
showName = uiState.showName,
showDate = uiState.showDate,
)
+ // 예매자 정보
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,
- 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) // 취소/환불 규정
+
+ 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(),
+ onClick = navigateToBusiness
+ )
Spacer(modifier = Modifier.height(120.dp))
}
@@ -200,11 +238,31 @@ 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),
- onClick = viewModel::reservation,
+ label = stringResource(
+ R.string.ticketing_payment_button_label,
+ uiState.totalPrice
+ ),
+ onClick = {
+ showConfirmDialog = true
+ },
)
}
}
+ if (showConfirmDialog) {
+ TicketingConfirmDialog(
+ isInviteTicket = uiState.isInviteTicket,
+ 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 },
+ )
+ }
}
}
@@ -232,14 +290,17 @@ 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,
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,
)
@@ -250,7 +311,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(),
@@ -289,6 +349,7 @@ private fun RefundPolicySection(refundPolicy: List) {
color = Grey50,
)
Text(
+ modifier = Modifier.padding(start = 2.dp),
text = it,
style = MaterialTheme.typography.bodySmall,
color = Grey50,
@@ -366,7 +427,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,
@@ -427,16 +491,29 @@ 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))
}
}
@Composable
-private fun DeposorSection(
+private fun DepositorSection(
name: String = "",
phoneNumber: String = "",
isSameContactInfo: Boolean,
@@ -541,6 +618,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,
@@ -635,7 +816,27 @@ private fun SectionTicketInfo(label: String, value: String, marginTop: Dp = 16.d
private fun TicketingDetailScreenPreview() {
BooltiTheme {
Surface {
- TicketingScreen { _, _ -> }
+ TicketingScreen(
+ onReserved = { _, _ -> },
+ navigateToBusiness = {}
+ )
+ }
+ }
+}
+
+@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 a5d3ab57..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
@@ -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(
@@ -17,21 +18,52 @@ 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 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() &&
- reservationPhoneNumber.isNotBlank() &&
- inviteCodeStatus is InviteCodeStatus.Valid
+ 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 {
+ 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 {
- reservationName.isNotBlank() &&
- reservationPhoneNumber.isNotBlank() &&
- (isSameContactInfo || depositorName.isNotBlank()) &&
- (isSameContactInfo || depositorPhoneNumber.isNotBlank())
+ 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 2edb718d..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
@@ -10,6 +9,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 +22,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 +32,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
@@ -54,20 +55,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,
)
}
@@ -76,12 +77,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")
@@ -92,9 +93,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) }
}
@@ -114,13 +114,12 @@ class TicketingViewModel @Inject constructor(
}
}
getRefundPolicyUsecase()
- .catch { e -> e.printStackTrace() }
.onEach { refundPolicy ->
_uiState.update {
it.copy(refundPolicy = refundPolicy)
}
}
- .launchIn(viewModelScope)
+ .launchIn(viewModelScope + recordExceptionHandler)
}
}
@@ -131,7 +130,7 @@ class TicketingViewModel @Inject constructor(
}
fun checkInviteCode() {
- viewModelScope.launch {
+ viewModelScope.launch(recordExceptionHandler) {
repository.checkInviteCode(
CheckInviteCodeRequest(
showId = showId,
@@ -141,8 +140,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)
@@ -156,7 +155,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,13 +163,21 @@ class TicketingViewModel @Inject constructor(
}
fun setDepositorPhoneNumber(number: String) {
- _uiState.update { it.copy(depositorPhoneNumber = number) }
+ _uiState.update { it.copy(depositorContact = number) }
}
fun setInviteCode(code: String) {
_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/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..0ce2ba05
--- /dev/null
+++ b/presentation/src/main/java/com/nexters/boolti/presentation/service/BtFirebaseMessagingService.kt
@@ -0,0 +1,71 @@
+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
+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 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 java.util.UUID
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class BtFirebaseMessagingService : FirebaseMessagingService() {
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
+ @Inject
+ lateinit var authRepository: AuthRepository
+
+ @Inject
+ lateinit var deepLinkEvent: DeepLinkEvent
+
+ override fun onNewToken(token: String) {
+ scope.launch {
+ authRepository.sendFcmToken()
+ }
+ }
+
+ override fun onMessageReceived(remoteMessage: RemoteMessage) {
+ if (!checkGrantedPermission(Manifest.permission.POST_NOTIFICATIONS)) return
+
+ val notification = remoteMessage.notification ?: return
+ val btNotification = BtNotification(remoteMessage.data["type"])
+
+ 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)
+ .setColor(0xFF6827)
+ .setSmallIcon(R.drawable.ic_logo)
+ .setContentIntent(pendingIntent)
+
+ NotificationManagerCompat.from(this).notify(btNotification.id, builder.build())
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ scope.cancel()
+ }
+}
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
+ }
+}
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/java/com/nexters/boolti/presentation/theme/Type.kt b/presentation/src/main/java/com/nexters/boolti/presentation/theme/Type.kt
index 2b57ce7c..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,
@@ -127,10 +140,10 @@ 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),
+ letterSpacing = letterSpacing,
)
}
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
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/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_logo.xml b/presentation/src/main/res/drawable/ic_logo.xml
index fa642826..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">
+
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">
+
+
+
+
+
+
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/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 @@
+
+
+
+
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 00000000..f909eae5
Binary files /dev/null and b/presentation/src/main/res/font/sb_aggro_b.otf differ
diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml
index c85fe8c0..bc0c64ba 100644
--- a/presentation/src/main/res/values/strings.xml
+++ b/presentation/src/main/res/values/strings.xml
@@ -3,6 +3,8 @@
티켓
마이
+
+
뒤로 가기
포스터 사진
닫기
@@ -27,6 +29,7 @@
공연 종료
공연명으로 검색해 주세요
입금 확인 중인 티켓이 있어요!
+ 예매 중
- 월
- 화
@@ -49,6 +52,15 @@
알 수 없는 에러가 발생했습니다
복사
다음
+ 보기
+
+ 예매자
+ 결제자
+ 티켓
+ 초청 티켓
+
+ 감소
+ 증가
불티나게 팔리는 티켓, 불티
@@ -59,12 +71,21 @@
약관 동의하고 시작하기
불티를 찾아주셔서 감사합니다
불티 유저
+ 탈퇴 후 30일 이내에 로그인하여, 계정 삭제가 취소되었어요\n불티를 다시 찾아주셔서 감사해요!
- 회원 탈퇴
- 탈퇴
+ 회원 탈퇴
+ 탈퇴하기
탈퇴하시겠어요?
탈퇴일로부터 30일 이내로 로그인 시 계정 삭제를 취소할 수 있습니다. 30일이 지나면 계정 및 정보가 영구 삭제됩니다.
+ 탈퇴 전, 꼭 읽어보세요!
+
+ - 주최한 공연 정보는 사라지지 않아요
+ - 예매한 티켓은 전부 사라지며 복구할 수 없어요
+ - 탈퇴 일로부터 30일 이내 재 로그인 시 계정 삭제를 취소할 수 있어요
+
+ 탈퇴 이유를 입력해주세요
+ 예) 계정 탈퇴 후 재가입할게요
%,d원
총 %,d원
@@ -83,7 +104,7 @@
공연 정보 보기
- 티켓 선택
+ 옵션 선택
공유하기
티켓 예매 기간
일시
@@ -98,12 +119,12 @@
이미 예매한 공연
1인 1매만 예매할 수 있어요
예매 시작 D-%d
- 예매 마감
+ 예매 종료
공연 종료
1인 %d매
결제하기
예매자 정보
- 입금자 정보
+ 결제자 정보
티켓 정보
초청 코드
결제 수단
@@ -118,17 +139,24 @@
사용하기
사용되었습니다
이미 사용된 초청 코드입니다
- 초청 코드가 올바르지 않아요
+ 올바른 초청 코드를 입력해 주세요
초청 코드를 입력해 주세요
- 예매자와 입금자가 같아요
+ 예매자와 결제자가 같아요
티켓 종류
티켓 매수
총 결제 금액
+ 결제 금액
다음 페이지에서 계좌 번호를 안내해 드릴게요
지금은 계좌 이체로만 결제할 수 있어요
%,d원 결제하기
+ 결제하기
+ 결제 정보를 확인해주세요
공연장 주소가 복사되었어요
%s (%s)
+ 주문내용 확인 및 결제 동의
+ [필수] 개인정보 수집・이용 동의
+ [필수] 개인정보 제 3자 정보 제공 동의
+ [필수] 결제대행 서비스 이용약관 동의
입장 코드
@@ -148,7 +176,7 @@
계좌번호
예금주
입금 마감일
- 결제가 완료되었어요
+ 결제를 완료했어요
예매자 정보 확인 후 티켓이 발권됩니다.
@@ -158,6 +186,7 @@
예매 내역
QR 스캔
정말 로그아웃 하시겠어요?
+ 공연 등록
QR 스캔
@@ -169,28 +198,35 @@
예매 내역이 없어요
티켓을 예매하고 공연을 즐겨보세요!
입금 확인 중
- 환불 진행 중
+ 취소 진행 중
예매 취소
- 티켓 발권 완료
- 환불 완료
+ 발권 완료
+ 취소 완료
상세 보기
알 수 없음
"%s / %d매 / %,d원"
+ %d매 / %,d원"
예매 내역 상세
입금 계좌 정보
결제 정보
- 결제 상태
+ 결제 수단
티켓 정보
발권 일시
발권 전
%d매
"%s / %d매"
+ 주문 번호
+ 주문 티켓
+ 환불 내역
+ 총 환불 금액
+ 티켓 보기
+ 예매 내역보기
- 환불 요청하기
- 환불 이유를 입력해 주세요
+ 취소 요청하기
+ 취소 사유를 입력해 주세요
예) 티켓 종류 재 선택 후 다시 예매할게요
예금주 정보
실명을 입력해 주세요
@@ -199,7 +235,11 @@
계좌번호를 입력해 주세요
선택 완료하기
환불 정보를 확인해 주세요
- 환불 요청이 완료되었어요
+ 취소 요청이 완료되었어요
+ 환불 정보
+ 환불 예정 금액
+ 환불 수단
+ 취소/환불 규정을 확인했습니다
신고하기
@@ -212,4 +252,28 @@
업데이트 알림
지금 업데이트하고\n더 편리해진 불티를 만나보세요
업데이트 하러가기
-
\ No newline at end of file
+
+
+ fcm
+ 불티 알림
+ 티켓 발권 알림을 받습니다.
+
+
+ 사업자 정보
+ 스튜디오 불티는 통신판매중개자로 통신판매의 당사자가 아닙니다. 불티에서 판매되는 상품에 대한 광고, 상품주문, 배송, 환불의 의무와 책임은 각 판매자에게 있습니다.
+ 스튜디오 불티 사업자 정보
+ ⓒ Boolti. All Rights Reserved
+ 스튜디오 불티
+ 서비스 이용약관
+ 개인정보 처리방침
+ 환불 정책
+
+ - 대표 : 김혜선
+ - 사업자 등록번호 : 202–43–63442
+ - 주소 : 경기도 남양주시 화도읍 묵현로 25번길 12–5
+ - 호스팅 서비스 : 스튜디오 불티
+ - 통신판매업 신고번호 : 2024-화도수동-0518
+ - 문의전화 : 0507–1363–5690
+ - 이메일 : studio.boolti@gmail.com
+
+