diff --git a/.github/workflows/assign-reviewer.yml b/.github/workflows/assign-reviewer.yml index d3ea0164e..722ea9786 100644 --- a/.github/workflows/assign-reviewer.yml +++ b/.github/workflows/assign-reviewer.yml @@ -1,8 +1,11 @@ name: Assign Reviewer By Label on: pull_request: - types: [ opened, edited, labeled, unlabeled ] - + types: + - opened + - edited + - labeled + - unlabeled jobs: assign-reviewer: runs-on: ubuntu-latest diff --git a/.github/workflows/ci-back.yml b/.github/workflows/ci-back.yml index cec9580d9..849fea334 100644 --- a/.github/workflows/ci-back.yml +++ b/.github/workflows/ci-back.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - dev + - main paths: 'backend/**' defaults: diff --git a/.github/workflows/ci-user-aos.yml b/.github/workflows/ci-user-aos.yml index d31f3b75a..2ef6e561b 100644 --- a/.github/workflows/ci-user-aos.yml +++ b/.github/workflows/ci-user-aos.yml @@ -4,10 +4,12 @@ on: push: branches: - dev - paths: ['android/festago/**'] + - main + paths: [ 'android/festago/**' ] pull_request: branches: - dev + - main paths: 'android/festago/**' defaults: @@ -20,35 +22,35 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: zulu - cache: gradle - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: add google-services.json - run: echo '${{ secrets.ANDROID_USER_GOOGLE_SERVICES_JSON }}' > ./app/google-services.json - - - name: add local.properties - run: | - echo kakao_native_app_key=\"${{ secrets.ANDROID_USER_KAKAO_NATIVE_APP_KEY }}\" >> ./local.properties - echo kakao_redirection_scheme=\"${{ secrets.ANDROID_USER_KAKAO_REDIRECTION_SCHEME }}\" >> ./local.properties - echo base_url=\"$${{ secrets.ANDROID_USER_BASE_URL }}\" >> ./local.properties - - - name: Build with Gradle - run: ./gradlew build - - - name: Run ktlint - run: ./gradlew ktlintCheck - - - name: Run unit tests - run: ./gradlew testDebugUnitTest - - - name: Build assemble release apk - run: ./gradlew assembleRelease + - uses: actions/checkout@v3 + - name: set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: zulu + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: add google-services.json + run: echo '${{ secrets.ANDROID_USER_GOOGLE_SERVICES_JSON }}' > ./app/google-services.json + + - name: add local.properties + run: | + echo kakao_native_app_key=\"${{ secrets.ANDROID_USER_KAKAO_NATIVE_APP_KEY }}\" >> ./local.properties + echo kakao_redirection_scheme=\"${{ secrets.ANDROID_USER_KAKAO_REDIRECTION_SCHEME }}\" >> ./local.properties + echo base_url=\"$${{ secrets.ANDROID_USER_BASE_URL }}\" >> ./local.properties + + - name: Build with Gradle + run: ./gradlew build + + - name: Run ktlint + run: ./gradlew ktlintCheck + + - name: Run unit tests + run: ./gradlew testDebugUnitTest + + - name: Build assemble release apk + run: ./gradlew assembleRelease diff --git a/.github/workflows/closed-issue-notification.yml b/.github/workflows/closed-issue-notification.yml index 37da67fbc..6031248af 100644 --- a/.github/workflows/closed-issue-notification.yml +++ b/.github/workflows/closed-issue-notification.yml @@ -1,39 +1,40 @@ name: Closed Issue Notification on: - issues: - types: [closed] - + issues: + types: + - closed + jobs: - create-issue: + create-issue: name: Send closed issue notification to slack - runs-on: ubuntu-latest - steps: - - name: Send Issue - uses: 8398a7/action-slack@v3 - with: - status: custom - custom_payload: | - { - text: "*이슈가 닫혔습니다!*", - attachments: [{ - fallback: 'fallback', - color: '#7539DE', - title: 'Title', - text: '<${{ github.event.issue.html_url }}|${{ github.event.issue.title }}>', - fields: [{ - title: 'Issue number', - value: '#${{ github.event.issue.number }}', - short: true - }, - { - title: 'Author', - value: '${{ github.event.issue.user.login }}', - short: true - }], - actions: [{ + runs-on: ubuntu-latest + steps: + - name: Send Issue + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + text: "*이슈가 닫혔습니다!*", + attachments: [{ + fallback: 'fallback', + color: '#7539DE', + title: 'Title', + text: '<${{ github.event.issue.html_url }}|${{ github.event.issue.title }}>', + fields: [{ + title: 'Issue number', + value: '#${{ github.event.issue.number }}', + short: true + }, + { + title: 'Author', + value: '${{ github.event.issue.user.login }}', + short: true + }], + actions: [{ + }] }] - }] - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_WEBHOOK_URL }} - if: always() + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_WEBHOOK_URL }} + if: always() diff --git a/.github/workflows/closed-pr-notification.yml b/.github/workflows/closed-pr-notification.yml index eec190437..d92c2d37e 100644 --- a/.github/workflows/closed-pr-notification.yml +++ b/.github/workflows/closed-pr-notification.yml @@ -1,8 +1,11 @@ name: Closed PR Notification on: pull_request: - branches: [ dev ] - types: [ closed ] + branches: + - dev + - main + types: + - closed jobs: create-issue: diff --git a/.github/workflows/opend-issue-notification.yml b/.github/workflows/opend-issue-notification.yml index 4bda56c88..2204395bb 100644 --- a/.github/workflows/opend-issue-notification.yml +++ b/.github/workflows/opend-issue-notification.yml @@ -1,39 +1,40 @@ name: Opend Issue Notification on: - issues: - types: [opened] - + issues: + types: + - opened + jobs: - create-issue: + create-issue: name: Send opend issue notification to slack - runs-on: ubuntu-latest - steps: - - name: Send Issue - uses: 8398a7/action-slack@v3 - with: - status: custom - custom_payload: | - { - text: "*새로운 이슈가 생성되었습니다!*", - attachments: [{ - fallback: 'fallback', - color: '#1F7629', - title: 'Title', - text: '<${{ github.event.issue.html_url }}|${{ github.event.issue.title }}>', - fields: [{ - title: 'Issue number', - value: '#${{ github.event.issue.number }}', - short: true - }, - { - title: 'Author', - value: '${{ github.event.issue.user.login }}', - short: true - }], - actions: [{ + runs-on: ubuntu-latest + steps: + - name: Send Issue + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + text: "*새로운 이슈가 생성되었습니다!*", + attachments: [{ + fallback: 'fallback', + color: '#1F7629', + title: 'Title', + text: '<${{ github.event.issue.html_url }}|${{ github.event.issue.title }}>', + fields: [{ + title: 'Issue number', + value: '#${{ github.event.issue.number }}', + short: true + }, + { + title: 'Author', + value: '${{ github.event.issue.user.login }}', + short: true + }], + actions: [{ + }] }] - }] - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_WEBHOOK_URL }} - if: always() + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_WEBHOOK_URL }} + if: always() diff --git a/.github/workflows/opened-pr-notification.yml b/.github/workflows/opened-pr-notification.yml index 00ff0314e..1ff6bba58 100644 --- a/.github/workflows/opened-pr-notification.yml +++ b/.github/workflows/opened-pr-notification.yml @@ -1,8 +1,11 @@ name: Opened PR Notification on: pull_request: - branches: [ dev ] - types: [ opened ] + branches: + - dev + - main + types: + - opened jobs: create-issue: diff --git a/android/festago/app/build.gradle.kts b/android/festago/app/build.gradle.kts index 1bc94749f..c92b8aae2 100644 --- a/android/festago/app/build.gradle.kts +++ b/android/festago/app/build.gradle.kts @@ -9,16 +9,17 @@ plugins { id("com.google.gms.google-services") id("com.google.firebase.crashlytics") id("org.jlleitschuh.gradle.ktlint") + id("com.google.dagger.hilt.android") } android { namespace = "com.festago.festago" - compileSdk = 33 + compileSdk = 34 defaultConfig { applicationId = "com.festago.festago" minSdk = 28 - targetSdk = 33 + targetSdk = 34 versionCode = 3 versionName = "1.0.1" @@ -63,13 +64,24 @@ kotlin { jvmToolchain(17) } +kapt { + correctErrorTypes = true +} + dependencies { + // domain + implementation(project(":domain")) + // android implementation("androidx.core:core-ktx:1.10.1") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.9.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") + // hilt + implementation("com.google.dagger:hilt-android:2.44") + kapt("com.google.dagger:hilt-android-compiler:2.44") + // recyclerview implementation("androidx.recyclerview:recyclerview:1.3.1-rc01") @@ -127,6 +139,7 @@ dependencies { implementation(platform("com.google.firebase:firebase-bom:32.2.0")) implementation("com.google.firebase:firebase-analytics-ktx") implementation("com.google.firebase:firebase-crashlytics-ktx") + implementation("com.google.firebase:firebase-messaging-ktx:23.2.1") // swiperefreshlayout implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") @@ -137,8 +150,14 @@ dependencies { // Encrypted SharedPreference implementation("androidx.security:security-crypto-ktx:1.1.0-alpha06") - // domain - implementation(project(":domain")) + // turbine + testImplementation("app.cash.turbine:turbine:1.0.0") + + // inApp Update + implementation("com.google.android.play:app-update-ktx:2.1.0") + + // splash + implementation("androidx.core:core-splashscreen:1.1.0-alpha02") } fun getSecretKey(propertyKey: String): String { diff --git a/android/festago/app/src/androidTest/java/com/festago/festago/presentation/ui/reservationcomplete/ReservationCompleteActivityTest.kt b/android/festago/app/src/androidTest/java/com/festago/festago/presentation/ui/reservationcomplete/ReservationCompleteActivityTest.kt index 57700a61c..867e40c6c 100644 --- a/android/festago/app/src/androidTest/java/com/festago/festago/presentation/ui/reservationcomplete/ReservationCompleteActivityTest.kt +++ b/android/festago/app/src/androidTest/java/com/festago/festago/presentation/ui/reservationcomplete/ReservationCompleteActivityTest.kt @@ -7,14 +7,13 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import com.festago.festago.R -import com.festago.festago.presentation.model.ReservedTicketUiModel import org.junit.Rule import org.junit.Test import java.time.LocalDateTime import java.time.format.DateTimeFormatter class ReservationCompleteActivityTest { - private val reservationComplete = ReservedTicketUiModel(1L, 123, LocalDateTime.now()) + private val reservationComplete = ReservedTicketArg(1L, 123, LocalDateTime.now()) private val intent = ReservationCompleteActivity.getIntent( diff --git a/android/festago/app/src/main/AndroidManifest.xml b/android/festago/app/src/main/AndroidManifest.xml index d066f411c..140e91a45 100644 --- a/android/festago/app/src/main/AndroidManifest.xml +++ b/android/festago/app/src/main/AndroidManifest.xml @@ -3,7 +3,6 @@ xmlns:tools="http://schemas.android.com/tools"> - + + + + + + + + + @@ -26,13 +41,7 @@ android:exported="false" /> - - - - - - + android:exported="false" /> @@ -54,11 +63,17 @@ android:scheme="@string/kakao_redirection_scheme" /> - + + - + + + + diff --git a/android/festago/app/src/main/java/com/festago/festago/FestagoApplication.kt b/android/festago/app/src/main/java/com/festago/festago/FestagoApplication.kt index 91aab83e0..8f07fa96d 100644 --- a/android/festago/app/src/main/java/com/festago/festago/FestagoApplication.kt +++ b/android/festago/app/src/main/java/com/festago/festago/FestagoApplication.kt @@ -1,58 +1,34 @@ package com.festago.festago import android.app.Application -import com.festago.festago.analytics.FirebaseAnalyticsHelper -import com.festago.festago.di.AnalysisContainer -import com.festago.festago.di.AuthServiceContainer -import com.festago.festago.di.LocalDataSourceContainer -import com.festago.festago.di.NormalServiceContainer -import com.festago.festago.di.RepositoryContainer -import com.festago.festago.di.TokenContainer +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import com.festago.festago.presentation.fcm.FcmMessageType import com.kakao.sdk.common.KakaoSdk +import dagger.hilt.android.HiltAndroidApp +@HiltAndroidApp class FestagoApplication : Application() { override fun onCreate() { super.onCreate() initKakaoSdk() - initRepositoryContainer() - initFirebaseContainer() + initNotificationChannel() } private fun initKakaoSdk() { KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY) } - private fun initRepositoryContainer() { - normalServiceContainer = NormalServiceContainer(BuildConfig.BASE_URL) - - tokenContainer = TokenContainer( - normalServiceContainer = normalServiceContainer, - localDataSourceContainer = LocalDataSourceContainer(applicationContext), - ) - - authServiceContainer = AuthServiceContainer( - baseUrl = BuildConfig.BASE_URL, - tokenContainer = tokenContainer, - ) - - repositoryContainer = RepositoryContainer( - authServiceContainer = authServiceContainer, - normalServiceContainer = normalServiceContainer, - tokenContainer = tokenContainer, + private fun initNotificationChannel() { + val channel = NotificationChannel( + FcmMessageType.ENTRY_ALERT.channelId, + getString(R.string.entry_alert_channel_name), + NotificationManager.IMPORTANCE_DEFAULT ) - } - - private fun initFirebaseContainer() { - FirebaseAnalyticsHelper.init(applicationContext) - analysisContainer = AnalysisContainer() - } - - companion object DependencyContainer { - lateinit var normalServiceContainer: NormalServiceContainer - lateinit var authServiceContainer: AuthServiceContainer - lateinit var repositoryContainer: RepositoryContainer - lateinit var analysisContainer: AnalysisContainer - lateinit var tokenContainer: TokenContainer + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) } } diff --git a/android/festago/app/src/main/java/com/festago/festago/analytics/FirebaseAnalyticsHelper.kt b/android/festago/app/src/main/java/com/festago/festago/analytics/FirebaseAnalyticsHelper.kt index 062e5903f..393795bf6 100644 --- a/android/festago/app/src/main/java/com/festago/festago/analytics/FirebaseAnalyticsHelper.kt +++ b/android/festago/app/src/main/java/com/festago/festago/analytics/FirebaseAnalyticsHelper.kt @@ -3,15 +3,15 @@ package com.festago.festago.analytics import android.content.Context import android.os.Bundle import com.google.firebase.analytics.FirebaseAnalytics +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject -object FirebaseAnalyticsHelper : AnalyticsHelper { +class FirebaseAnalyticsHelper @Inject constructor( + @ApplicationContext context: Context +) : AnalyticsHelper { - private const val LOG_NAME = "festago_log" - private lateinit var firebaseAnalytics: FirebaseAnalytics - - fun init(context: Context) { - if (::firebaseAnalytics.isInitialized) return - firebaseAnalytics = FirebaseAnalytics.getInstance(context.applicationContext) + private val firebaseAnalytics: FirebaseAnalytics by lazy { + FirebaseAnalytics.getInstance(context.applicationContext) } override fun logEvent(event: AnalyticsEvent) { @@ -22,4 +22,8 @@ object FirebaseAnalyticsHelper : AnalyticsHelper { } firebaseAnalytics.logEvent(LOG_NAME, params) } + + companion object { + private const val LOG_NAME = "festago_log" + } } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/datasource/TokenLocalDataSource.kt b/android/festago/app/src/main/java/com/festago/festago/data/datasource/TokenLocalDataSource.kt index 6cce56927..83d0ec325 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/datasource/TokenLocalDataSource.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/datasource/TokenLocalDataSource.kt @@ -4,8 +4,12 @@ import android.content.Context import android.content.SharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject -class TokenLocalDataSource(context: Context) : TokenDataSource { +class TokenLocalDataSource @Inject constructor( + @ApplicationContext context: Context, +) : TokenDataSource { private val sharedPreference: SharedPreferences by lazy { val masterKeyAlias = MasterKey diff --git a/android/festago/app/src/main/java/com/festago/festago/data/di/ViewModelScopeModule.kt b/android/festago/app/src/main/java/com/festago/festago/data/di/ViewModelScopeModule.kt new file mode 100644 index 000000000..abe7d2efe --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/di/ViewModelScopeModule.kt @@ -0,0 +1,36 @@ +package com.festago.festago.data.di + +import com.festago.festago.data.repository.ReservationTicketDefaultRepository +import com.festago.festago.data.repository.SchoolDefaultRepository +import com.festago.festago.data.repository.StudentVerificationDefaultRepository +import com.festago.festago.data.repository.UserDefaultRepository +import com.festago.festago.repository.ReservationTicketRepository +import com.festago.festago.repository.SchoolRepository +import com.festago.festago.repository.StudentVerificationRepository +import com.festago.festago.repository.UserRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@InstallIn(ViewModelComponent::class) +@Module +interface ViewModelScopeModule { + + @Binds + @ViewModelScoped + fun bindsReservationTicketDefaultRepository(reservationTicketRepository: ReservationTicketDefaultRepository): ReservationTicketRepository + + @Binds + @ViewModelScoped + fun bindsStudentVerificationDefaultRepository(studentVerificationRepository: StudentVerificationDefaultRepository): StudentVerificationRepository + + @Binds + @ViewModelScoped + fun bindsUserDefaultRepository(userRepository: UserDefaultRepository): UserRepository + + @Binds + @ViewModelScoped + fun bindsSelectSchoolRepository(schoolRepository: SchoolDefaultRepository): SchoolRepository +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/AnalyticsModule.kt b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/AnalyticsModule.kt new file mode 100644 index 000000000..9434899ef --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/AnalyticsModule.kt @@ -0,0 +1,19 @@ +package com.festago.festago.data.di.singletonscope + +import com.festago.festago.analytics.AnalyticsHelper +import com.festago.festago.analytics.FirebaseAnalyticsHelper +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +interface AnalyticsModule { + @Binds + @Singleton + fun bindsFirebaseAnalyticsHelper( + analyticsHelper: FirebaseAnalyticsHelper + ): AnalyticsHelper +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/ApiModule.kt b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/ApiModule.kt new file mode 100644 index 000000000..6d6a91e99 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/ApiModule.kt @@ -0,0 +1,83 @@ +package com.festago.festago.data.di.singletonscope + +import com.festago.festago.BuildConfig +import com.festago.festago.data.retrofit.AuthInterceptor +import com.festago.festago.repository.AuthRepository +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import javax.inject.Qualifier +import javax.inject.Singleton + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AuthOkHttpClientQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class NormalRetrofitQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AuthRetrofitQualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class BaseUrlQualifier + +@InstallIn(SingletonComponent::class) +@Module +object ApiModule { + + @Provides + @Singleton + @AuthOkHttpClientQualifier + fun provideOkHttpClient(authRepository: AuthRepository): OkHttpClient = OkHttpClient + .Builder() + .addInterceptor(AuthInterceptor(authRepository)) + .build() + + @Provides + @Singleton + fun provideRetrofitConverterFactory(): retrofit2.Converter.Factory { + val json = Json { + ignoreUnknownKeys = true + } + return json.asConverterFactory("application/json".toMediaType()) + } + + @Provides + @Singleton + @NormalRetrofitQualifier + fun providesNormalRetrofit( + @BaseUrlQualifier baseUrl: String, + converterFactory: retrofit2.Converter.Factory, + ): Retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(converterFactory) + .build() + + @Provides + @Singleton + @AuthRetrofitQualifier + fun providesAuthRetrofit( + @BaseUrlQualifier baseUrl: String, + @AuthOkHttpClientQualifier okHttpClient: OkHttpClient, + converterFactory: retrofit2.Converter.Factory, + ): Retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttpClient) + .addConverterFactory(converterFactory) + .build() + + @Provides + @Singleton + @BaseUrlQualifier + fun providesBaseUrl(): String = BuildConfig.BASE_URL +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/DataSourceModule.kt b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/DataSourceModule.kt new file mode 100644 index 000000000..ed94fe610 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/DataSourceModule.kt @@ -0,0 +1,17 @@ +package com.festago.festago.data.di.singletonscope + +import com.festago.festago.data.datasource.TokenDataSource +import com.festago.festago.data.datasource.TokenLocalDataSource +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +interface DataSourceModule { + @Binds + @Singleton + fun bindsLocalTokenDataSource(tokenDataSource: TokenLocalDataSource): TokenDataSource +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/FirebaseModule.kt b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/FirebaseModule.kt new file mode 100644 index 000000000..ecbeafc94 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/FirebaseModule.kt @@ -0,0 +1,18 @@ +package com.festago.festago.data.di.singletonscope + +import com.google.firebase.messaging.FirebaseMessaging +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object FirebaseModule { + @Provides + @Singleton + fun provideFirebaseMessaging(): FirebaseMessaging { + return FirebaseMessaging.getInstance() + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/RepositoryModule.kt b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/RepositoryModule.kt new file mode 100644 index 000000000..ca239248b --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/RepositoryModule.kt @@ -0,0 +1,34 @@ +package com.festago.festago.data.di.singletonscope + +import com.festago.festago.data.repository.AuthDefaultRepository +import com.festago.festago.data.repository.FestivalDefaultRepository +import com.festago.festago.data.repository.SocialAuthKakaoRepository +import com.festago.festago.data.repository.TicketDefaultRepository +import com.festago.festago.repository.AuthRepository +import com.festago.festago.repository.FestivalRepository +import com.festago.festago.repository.SocialAuthRepository +import com.festago.festago.repository.TicketRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +interface RepositoryModule { + + @Binds + fun bindsAuthDefaultRepository(authRepository: AuthDefaultRepository): AuthRepository + + @Binds + fun bindsSocialAuthDefaultRepository(socialAuthRepository: SocialAuthKakaoRepository): SocialAuthRepository + + @Binds + @Singleton + fun bindsFestivalDefaultRepository(festivalRepository: FestivalDefaultRepository): FestivalRepository + + @Binds + @Singleton + fun bindsTicketDefaultRepository(ticketRepository: TicketDefaultRepository): TicketRepository +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/ServiceModule.kt b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/ServiceModule.kt new file mode 100644 index 000000000..3cbb12e5e --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/di/singletonscope/ServiceModule.kt @@ -0,0 +1,76 @@ +package com.festago.festago.data.di.singletonscope + +import com.festago.festago.data.service.FestivalRetrofitService +import com.festago.festago.data.service.ReservationTicketRetrofitService +import com.festago.festago.data.service.SchoolRetrofitService +import com.festago.festago.data.service.StudentVerificationRetrofitService +import com.festago.festago.data.service.TicketRetrofitService +import com.festago.festago.data.service.TokenRetrofitService +import com.festago.festago.data.service.UserRetrofitService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object ServiceModule { + + @Provides + @Singleton + fun providesFestivalRetrofitService( + @NormalRetrofitQualifier retrofit: Retrofit + ): FestivalRetrofitService { + return retrofit.create(FestivalRetrofitService::class.java) + } + + @Provides + @Singleton + fun providesTokenRetrofitService( + @NormalRetrofitQualifier retrofit: Retrofit + ): TokenRetrofitService { + return retrofit.create(TokenRetrofitService::class.java) + } + + @Provides + @Singleton + fun providesReservationTicketRetrofitService( + @NormalRetrofitQualifier retrofit: Retrofit + ): ReservationTicketRetrofitService { + return retrofit.create(ReservationTicketRetrofitService::class.java) + } + + @Provides + @Singleton + fun providesTicketRetrofitService( + @AuthRetrofitQualifier retrofit: Retrofit + ): TicketRetrofitService { + return retrofit.create(TicketRetrofitService::class.java) + } + + @Provides + @Singleton + fun providesUserRetrofitService( + @AuthRetrofitQualifier retrofit: Retrofit + ): UserRetrofitService { + return retrofit.create(UserRetrofitService::class.java) + } + + @Provides + @Singleton + fun providesStudentVerificationRetrofitService( + @AuthRetrofitQualifier retrofit: Retrofit + ): StudentVerificationRetrofitService { + return retrofit.create(StudentVerificationRetrofitService::class.java) + } + + @Provides + @Singleton + fun providesSchoolRetrofitService( + @NormalRetrofitQualifier retrofit: Retrofit + ): SchoolRetrofitService { + return retrofit.create(SchoolRetrofitService::class.java) + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/FestivalResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/FestivalResponse.kt index ba8e2195a..76c322f87 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/FestivalResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/FestivalResponse.kt @@ -7,6 +7,7 @@ import java.time.LocalDate @Serializable data class FestivalResponse( val id: Int, + val schoolId: Int, val name: String, val startDate: String, val endDate: String, diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/OauthRequest.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/OauthRequest.kt index b54e95ef1..622559726 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/OauthRequest.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/OauthRequest.kt @@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable data class OauthRequest( val socialType: String, val accessToken: String, + val fcmToken: String, ) diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationFestivalResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationFestivalResponse.kt index bf83c7e33..659bd8ede 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationFestivalResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationFestivalResponse.kt @@ -7,6 +7,7 @@ import java.time.LocalDate @Serializable data class ReservationFestivalResponse( val id: Int, + val schoolId: Int, val name: String, val startDate: String, val endDate: String, diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationStageResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationStageResponse.kt index f01c23a8f..39176cdcc 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationStageResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationStageResponse.kt @@ -1,6 +1,7 @@ package com.festago.festago.data.dto import com.festago.festago.model.ReservationStage +import com.festago.festago.model.ReservationTickets import kotlinx.serialization.Serializable import java.time.LocalDateTime @@ -17,6 +18,10 @@ data class ReservationStageResponse( lineUp = lineUp, startTime = LocalDateTime.parse(startTime), ticketOpenTime = LocalDateTime.parse(ticketOpenTime), - reservationTickets = tickets.map { it.toDomain() }, + reservationTickets = tickets.toDomain(), + ) + + private fun List.toDomain() = ReservationTickets( + this.map { it.toDomain() }, ) } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationTicketResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationTicketResponse.kt index 36966445c..6a6c73731 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationTicketResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationTicketResponse.kt @@ -1,6 +1,7 @@ package com.festago.festago.data.dto import com.festago.festago.model.ReservationTicket +import com.festago.festago.model.TicketType import kotlinx.serialization.Serializable @Serializable @@ -12,8 +13,16 @@ data class ReservationTicketResponse( ) { fun toDomain(): ReservationTicket = ReservationTicket( id = id, - ticketType = ticketType, + ticketType = convertToTicketType(ticketType), totalAmount = totalAmount, remainAmount = remainAmount, ) + + private fun convertToTicketType(ticketType: String): TicketType { + return when (ticketType) { + "STUDENT" -> TicketType.STUDENT + "VISITOR" -> TicketType.VISITOR + else -> TicketType.OTHER + } + } } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationTicketsResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationTicketsResponse.kt index e7a68e221..f6e62ef93 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationTicketsResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservationTicketsResponse.kt @@ -1,11 +1,13 @@ package com.festago.festago.data.dto -import com.festago.festago.model.ReservationTicket +import com.festago.festago.model.ReservationTickets import kotlinx.serialization.Serializable @Serializable data class ReservationTicketsResponse( val tickets: List, ) { - fun toDomain(): List = tickets.map { it.toDomain() } + fun toDomain(): ReservationTickets = ReservationTickets( + tickets.map { it.toDomain() }, + ) } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/SchoolResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/SchoolResponse.kt new file mode 100644 index 000000000..52646b6d5 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/SchoolResponse.kt @@ -0,0 +1,17 @@ +package com.festago.festago.data.dto + +import com.festago.festago.model.School +import kotlinx.serialization.Serializable + +@Serializable +data class SchoolResponse( + val id: Int, + val domain: String, + val name: String +) { + fun toDomain(): School = School( + id = id.toLong(), + domain = domain, + name = name + ) +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/SchoolsResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/SchoolsResponse.kt new file mode 100644 index 000000000..72e2d61ec --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/SchoolsResponse.kt @@ -0,0 +1,11 @@ +package com.festago.festago.data.dto + +import com.festago.festago.model.School +import kotlinx.serialization.Serializable + +@Serializable +data class SchoolsResponse( + val schools: List +) { + fun toDomain(): List = schools.map { it.toDomain() } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/SendVerificationRequest.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/SendVerificationRequest.kt new file mode 100644 index 000000000..fc4d5af01 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/SendVerificationRequest.kt @@ -0,0 +1,9 @@ +package com.festago.festago.data.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class SendVerificationRequest( + val username: String, + val schoolId: Long, +) diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/VerificationRequest.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/VerificationRequest.kt new file mode 100644 index 000000000..83584b46b --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/VerificationRequest.kt @@ -0,0 +1,13 @@ +package com.festago.festago.data.dto + +import com.festago.festago.model.StudentVerificationCode +import kotlinx.serialization.Serializable + +@Serializable +data class VerificationRequest( + val code: String, +) { + companion object { + fun from(code: StudentVerificationCode) = VerificationRequest(code.value) + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/repository/AuthDefaultRepository.kt b/android/festago/app/src/main/java/com/festago/festago/data/repository/AuthDefaultRepository.kt index 2594cb3c4..ba1ee638d 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/repository/AuthDefaultRepository.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/repository/AuthDefaultRepository.kt @@ -1,43 +1,50 @@ package com.festago.festago.data.repository -import com.festago.festago.data.service.UserRetrofitService -import com.festago.festago.data.util.runCatchingWithErrorHandler +import com.festago.festago.data.datasource.TokenDataSource +import com.festago.festago.data.dto.OauthRequest +import com.festago.festago.data.service.TokenRetrofitService +import com.festago.festago.data.util.onSuccessOrCatch +import com.festago.festago.data.util.runCatchingResponse import com.festago.festago.repository.AuthRepository -import com.festago.festago.repository.TokenRepository -import com.kakao.sdk.user.UserApiClient +import com.festago.festago.repository.SocialAuthRepository +import com.google.firebase.messaging.FirebaseMessaging +import kotlinx.coroutines.tasks.await +import javax.inject.Inject -class AuthDefaultRepository( - private val userRetrofitService: UserRetrofitService, - private val tokenRepository: TokenRepository, +class AuthDefaultRepository @Inject constructor( + private val socialAuthRepository: SocialAuthRepository, + private val tokenRetrofitService: TokenRetrofitService, + private val tokenDataSource: TokenDataSource, + private val firebaseMessaging: FirebaseMessaging, ) : AuthRepository { + override var token: String? + get() = tokenDataSource.token + set(value) { + tokenDataSource.token = value + } override val isSigned: Boolean - get() = tokenRepository.token != null - - override val token: String? - get() = tokenRepository.token + get() = tokenDataSource.token != null - override suspend fun signIn(socialType: String, token: String): Result { - return tokenRepository.signIn(socialType, token) - } + override suspend fun signIn(): Result = + runCatchingResponse { + val fcmToken = firebaseMessaging.token.await() + tokenRetrofitService.getOauthToken( + OauthRequest( + socialAuthRepository.socialType, + socialAuthRepository.getSocialToken().getOrThrow(), + fcmToken, + ), + ) + }.onSuccessOrCatch { tokenDataSource.token = it.accessToken } override suspend fun signOut(): Result { - UserApiClient.instance.logout { - tokenRepository.token = null - } - return Result.success(Unit) + tokenDataSource.token = null + return socialAuthRepository.signOut() } - override suspend fun deleteAccount(): Result { - userRetrofitService.deleteUserAccount().runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { - UserApiClient.instance.unlink { error -> - if (error == null) { - tokenRepository.token = null - } - } - return Result.success(Unit) - } - } + override suspend fun deleteAccount(): Result = runCatchingResponse { + socialAuthRepository.deleteAccount() + tokenRetrofitService.deleteUserAccount("Bearer ${tokenDataSource.token}") + }.apply { tokenDataSource.token = null } } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/repository/FestivalDefaultRepository.kt b/android/festago/app/src/main/java/com/festago/festago/data/repository/FestivalDefaultRepository.kt index d7fece84d..12326dd03 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/repository/FestivalDefaultRepository.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/repository/FestivalDefaultRepository.kt @@ -1,23 +1,21 @@ package com.festago.festago.data.repository import com.festago.festago.data.service.FestivalRetrofitService -import com.festago.festago.data.util.runCatchingWithErrorHandler +import com.festago.festago.data.util.onSuccessOrCatch +import com.festago.festago.data.util.runCatchingResponse import com.festago.festago.model.Festival import com.festago.festago.model.Reservation import com.festago.festago.repository.FestivalRepository +import javax.inject.Inject -class FestivalDefaultRepository( +class FestivalDefaultRepository @Inject constructor( private val festivalRetrofitService: FestivalRetrofitService, ) : FestivalRepository { - override suspend fun loadFestivals(): Result> { - festivalRetrofitService.getFestivals().runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { return Result.success(it.toDomain()) } - } + override suspend fun loadFestivals(): Result> = + runCatchingResponse { festivalRetrofitService.getFestivals() } + .onSuccessOrCatch { it.toDomain() } - override suspend fun loadFestivalDetail(festivalId: Long): Result { - festivalRetrofitService.getFestivalDetail(festivalId).runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { return Result.success(it.toDomain()) } - } + override suspend fun loadFestivalDetail(festivalId: Long): Result = + runCatchingResponse { festivalRetrofitService.getFestivalDetail(festivalId) } + .onSuccessOrCatch { it.toDomain() } } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/repository/ReservationTicketDefaultRepository.kt b/android/festago/app/src/main/java/com/festago/festago/data/repository/ReservationTicketDefaultRepository.kt index 8519a45fe..5763f98aa 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/repository/ReservationTicketDefaultRepository.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/repository/ReservationTicketDefaultRepository.kt @@ -1,18 +1,17 @@ package com.festago.festago.data.repository import com.festago.festago.data.service.ReservationTicketRetrofitService -import com.festago.festago.data.util.runCatchingWithErrorHandler -import com.festago.festago.model.ReservationTicket +import com.festago.festago.data.util.onSuccessOrCatch +import com.festago.festago.data.util.runCatchingResponse +import com.festago.festago.model.ReservationTickets import com.festago.festago.repository.ReservationTicketRepository +import javax.inject.Inject -class ReservationTicketDefaultRepository( +class ReservationTicketDefaultRepository @Inject constructor( private val reservationTicketRetrofitService: ReservationTicketRetrofitService, ) : ReservationTicketRepository { - override suspend fun loadTicketTypes(stageId: Int): Result> { - reservationTicketRetrofitService.getReservationTickets(stageId) - .runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { return Result.success(it.toDomain()) } - } + override suspend fun loadTicketTypes(stageId: Int): Result = + runCatchingResponse { reservationTicketRetrofitService.getReservationTickets(stageId) } + .onSuccessOrCatch { it.toDomain() } } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/repository/SchoolDefaultRepository.kt b/android/festago/app/src/main/java/com/festago/festago/data/repository/SchoolDefaultRepository.kt new file mode 100644 index 000000000..8f482c3c8 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/repository/SchoolDefaultRepository.kt @@ -0,0 +1,29 @@ +package com.festago.festago.data.repository + +import com.festago.festago.data.service.SchoolRetrofitService +import com.festago.festago.data.util.onSuccessOrCatch +import com.festago.festago.data.util.runCatchingResponse +import com.festago.festago.model.School +import com.festago.festago.repository.SchoolRepository +import javax.inject.Inject + +class SchoolDefaultRepository @Inject constructor( + private val schoolRetrofitService: SchoolRetrofitService, +) : SchoolRepository { + + override suspend fun loadSchools(): Result> = + runCatchingResponse { schoolRetrofitService.getSchools() } + .onSuccessOrCatch { it.toDomain() } + + override suspend fun loadSchoolEmail(schoolId: Long): Result { + return runCatchingResponse { schoolRetrofitService.getSchools() } + .onSuccessOrCatch { + val school = it.schools.find { school -> school.id.toLong() == schoolId } + school?.domain ?: throw IllegalArgumentException(MATCH_SCHOOL_NOT_FOUND) + } + } + + companion object { + private const val MATCH_SCHOOL_NOT_FOUND = "MATCH_SCHOOL_NOT_FOUND" + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/repository/SocialAuthKakaoRepository.kt b/android/festago/app/src/main/java/com/festago/festago/data/repository/SocialAuthKakaoRepository.kt new file mode 100644 index 000000000..330341858 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/repository/SocialAuthKakaoRepository.kt @@ -0,0 +1,112 @@ +package com.festago.festago.data.repository + +import android.content.Context +import com.festago.festago.repository.SocialAuthRepository +import com.kakao.sdk.auth.AuthApiClient +import com.kakao.sdk.auth.TokenManagerProvider +import com.kakao.sdk.auth.model.OAuthToken +import com.kakao.sdk.common.model.ClientError +import com.kakao.sdk.common.model.ClientErrorCause +import com.kakao.sdk.common.model.KakaoSdkError +import com.kakao.sdk.user.UserApiClient +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class SocialAuthKakaoRepository @Inject constructor( + @ApplicationContext private val context: Context, +) : SocialAuthRepository { + + override val socialType: String = SOCIAL_TYPE_KAKAO + + override suspend fun getSocialToken(): Result = runCatching { + if (AuthApiClient.instance.hasToken()) { + val error = accessTokenInfo() + + if (error is KakaoSdkError && error.isInvalidTokenError()) { + loginWithKakao(context) + } else if (error != null) { + throw error + } + } else { + loginWithKakao(context) + } + + TokenManagerProvider.instance.manager.getToken()?.accessToken + ?: throw Exception("Unknown error") + } + + private suspend fun accessTokenInfo(): Throwable? { + return suspendCoroutine { continuation -> + UserApiClient.instance.accessTokenInfo { _, throwable -> + continuation.resume(throwable) + } + } + } + + override suspend fun signOut(): Result { + UserApiClient.instance.logout {} + return Result.success(Unit) + } + + override suspend fun deleteAccount(): Result { + return suspendCoroutine> { continuation -> + TokenManagerProvider.instance.manager.getToken()?.let { + UserApiClient.instance.unlink { error -> + if (error == null) { + continuation.resume(Result.success(Unit)) + } else { + continuation.resumeWithException(error) + } + } + } ?: continuation.resume(Result.success(Unit)) + } + } + + private suspend fun loginWithKakao(context: Context): OAuthToken { + return if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { + try { + loginWithKakaoTalk(context) + } catch (error: Throwable) { + if (error is ClientError && error.reason == ClientErrorCause.Cancelled) throw error + loginWithKakaoAccount(context) + } + } else { + loginWithKakaoAccount(context) + } + } + + private suspend fun loginWithKakaoTalk(context: Context): OAuthToken { + return suspendCoroutine { continuation -> + UserApiClient.instance.loginWithKakaoTalk(context) { token, error -> + if (error != null) { + continuation.resumeWithException(error) + } else if (token != null) { + continuation.resume(token) + } else { + continuation.resumeWithException(RuntimeException("Failure get kakao access token")) + } + } + } + } + + private suspend fun loginWithKakaoAccount(context: Context): OAuthToken { + return suspendCoroutine { continuation -> + UserApiClient.instance.loginWithKakaoAccount(context) { token, error -> + if (error != null) { + continuation.resumeWithException(error) + } else if (token != null) { + continuation.resume(token) + } else { + continuation.resumeWithException(RuntimeException("Failure get kakao access token")) + } + } + } + } + + companion object { + private const val SOCIAL_TYPE_KAKAO = "KAKAO" + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/repository/StudentVerificationDefaultRepository.kt b/android/festago/app/src/main/java/com/festago/festago/data/repository/StudentVerificationDefaultRepository.kt new file mode 100644 index 000000000..becc28334 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/repository/StudentVerificationDefaultRepository.kt @@ -0,0 +1,28 @@ +package com.festago.festago.data.repository + +import com.festago.festago.data.dto.SendVerificationRequest +import com.festago.festago.data.dto.VerificationRequest +import com.festago.festago.data.service.StudentVerificationRetrofitService +import com.festago.festago.data.util.runCatchingResponse +import com.festago.festago.model.StudentVerificationCode +import com.festago.festago.repository.StudentVerificationRepository +import javax.inject.Inject + +class StudentVerificationDefaultRepository @Inject constructor( + private val studentVerificationRetrofitService: StudentVerificationRetrofitService, +) : StudentVerificationRepository { + + override suspend fun sendVerificationCode(userName: String, schoolId: Long): Result = + runCatchingResponse { + studentVerificationRetrofitService.sendVerificationCode( + SendVerificationRequest(userName, schoolId), + ) + } + + override suspend fun requestVerificationCodeConfirm(code: StudentVerificationCode): Result = + runCatchingResponse { + studentVerificationRetrofitService.requestVerification( + VerificationRequest.from(code), + ) + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/repository/TicketDefaultRepository.kt b/android/festago/app/src/main/java/com/festago/festago/data/repository/TicketDefaultRepository.kt index a04facbac..33ac652e5 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/repository/TicketDefaultRepository.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/repository/TicketDefaultRepository.kt @@ -2,44 +2,38 @@ package com.festago.festago.data.repository import com.festago.festago.data.dto.ReservedTicketRequest import com.festago.festago.data.service.TicketRetrofitService -import com.festago.festago.data.util.runCatchingWithErrorHandler +import com.festago.festago.data.util.onSuccessOrCatch +import com.festago.festago.data.util.runCatchingResponse import com.festago.festago.model.ReservedTicket import com.festago.festago.model.Ticket import com.festago.festago.model.TicketCode import com.festago.festago.repository.TicketRepository +import javax.inject.Inject -class TicketDefaultRepository( +class TicketDefaultRepository @Inject constructor( private val ticketRetrofitService: TicketRetrofitService, ) : TicketRepository { - override suspend fun loadTicket(ticketId: Long): Result { - ticketRetrofitService.getTicket(ticketId).runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { return Result.success(it.toDomain()) } - } + override suspend fun loadTicket(ticketId: Long): Result = + runCatchingResponse { ticketRetrofitService.getTicket(ticketId) } + .onSuccessOrCatch { it.toDomain() } - override suspend fun loadCurrentTickets(): Result> { - ticketRetrofitService.getCurrentTickets().runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { return Result.success(it.toDomain()) } - } + override suspend fun loadCurrentTickets(): Result> = + runCatchingResponse { ticketRetrofitService.getCurrentTickets() } + .onSuccessOrCatch { it.toDomain() } - override suspend fun loadTicketCode(ticketId: Long): Result { - ticketRetrofitService.getTicketCode(ticketId).runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { return Result.success(it.toDomain()) } - } + override suspend fun loadTicketCode(ticketId: Long): Result = + runCatchingResponse { ticketRetrofitService.getTicketCode(ticketId) } + .onSuccessOrCatch { it.toDomain() } - override suspend fun loadHistoryTickets(size: Int): Result> { - ticketRetrofitService.getHistoryTickets(size).runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { return Result.success(it.toDomain()) } - } + override suspend fun loadHistoryTickets(size: Int): Result> = + runCatchingResponse { ticketRetrofitService.getHistoryTickets(size) } + .onSuccessOrCatch { it.toDomain() } - override suspend fun reserveTicket(ticketId: Int): Result { - ticketRetrofitService.postReserveTicket(ReservedTicketRequest(ticketId)) - .runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { return Result.success(it.toDomain()) } - } + override suspend fun reserveTicket(ticketId: Int): Result = + runCatchingResponse { + ticketRetrofitService.postReserveTicket( + ReservedTicketRequest(ticketId), + ) + }.onSuccessOrCatch { it.toDomain() } } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/repository/TokenDefaultRepository.kt b/android/festago/app/src/main/java/com/festago/festago/data/repository/TokenDefaultRepository.kt deleted file mode 100644 index 3b9c7d87c..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/data/repository/TokenDefaultRepository.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.festago.festago.data.repository - -import com.festago.festago.data.datasource.TokenDataSource -import com.festago.festago.data.dto.OauthRequest -import com.festago.festago.data.service.TokenRetrofitService -import com.festago.festago.data.util.runCatchingWithErrorHandler -import com.festago.festago.repository.TokenRepository -import kotlinx.coroutines.runBlocking - -class TokenDefaultRepository( - private val tokenLocalDataSource: TokenDataSource, - private val tokenRetrofitService: TokenRetrofitService, -) : TokenRepository { - override var token: String? - get() = tokenLocalDataSource.token - set(value) { - tokenLocalDataSource.token = value - } - - override suspend fun signIn(socialType: String, token: String): Result { - tokenRetrofitService.getOauthToken(OauthRequest(socialType, token)) - .runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { - tokenLocalDataSource.token = it.accessToken - return Result.success(Unit) - } - } - - override fun refreshToken(token: String): Result = runBlocking { - tokenRetrofitService.getOauthToken(OauthRequest("KAKAO", token)) - .runCatchingWithErrorHandler() - .getOrElse { error -> return@runBlocking Result.failure(error) } - .let { - tokenLocalDataSource.token = it.accessToken - return@runBlocking Result.success(Unit) - } - } -} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/repository/UserDefaultRepository.kt b/android/festago/app/src/main/java/com/festago/festago/data/repository/UserDefaultRepository.kt index 5babfa87c..354366dfc 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/repository/UserDefaultRepository.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/repository/UserDefaultRepository.kt @@ -1,16 +1,16 @@ package com.festago.festago.data.repository import com.festago.festago.data.service.UserRetrofitService -import com.festago.festago.data.util.runCatchingWithErrorHandler +import com.festago.festago.data.util.onSuccessOrCatch +import com.festago.festago.data.util.runCatchingResponse import com.festago.festago.model.UserProfile import com.festago.festago.repository.UserRepository +import javax.inject.Inject -class UserDefaultRepository( +class UserDefaultRepository @Inject constructor( private val userProfileService: UserRetrofitService, ) : UserRepository { - override suspend fun loadUserProfile(): Result { - userProfileService.getUserProfile().runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { return Result.success(it.toDomain()) } - } + override suspend fun loadUserProfile(): Result = + runCatchingResponse { userProfileService.getUserProfile() } + .onSuccessOrCatch { it.toDomain() } } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/retrofit/AuthInterceptor.kt b/android/festago/app/src/main/java/com/festago/festago/data/retrofit/AuthInterceptor.kt index 80151658b..1c861145f 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/retrofit/AuthInterceptor.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/retrofit/AuthInterceptor.kt @@ -1,27 +1,38 @@ package com.festago.festago.data.retrofit +import com.festago.festago.repository.AuthRepository +import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Response -class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor { +class AuthInterceptor(private val authRepository: AuthRepository) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(request = getNewRequest(chain)) - if (response.code == 401) { + if (isCodeUnauthorized(response)) { response.close() - tokenManager.refreshToken() + refreshToken() return chain.proceed(request = getNewRequest(chain)) } return response } + @Synchronized + private fun refreshToken() { + runBlocking { authRepository.signIn() } + } + + private fun isCodeUnauthorized(response: Response) = response.code == RESPONSE_CODE_UNAUTHORIZED + private fun getNewRequest(chain: Interceptor.Chain) = chain.request() .newBuilder() - .addHeader(HEADER_AUTHORIZATION, AUTHORIZATION_TOKEN_FORMAT.format(tokenManager.token)) + .addHeader(HEADER_AUTHORIZATION, AUTHORIZATION_TOKEN_FORMAT.format(authRepository.token)) .build() companion object { private const val HEADER_AUTHORIZATION = "Authorization" private const val AUTHORIZATION_TOKEN_FORMAT = "Bearer %s" + private const val RESPONSE_CODE_UNAUTHORIZED = 401 } } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/retrofit/TokenManager.kt b/android/festago/app/src/main/java/com/festago/festago/data/retrofit/TokenManager.kt deleted file mode 100644 index 4cb33e5a4..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/data/retrofit/TokenManager.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.festago.festago.data.retrofit - -import com.festago.festago.repository.TokenRepository -import com.kakao.sdk.auth.TokenManagerProvider - -class TokenManager(private val tokenRepository: TokenRepository) { - - val token: String - get() = tokenRepository.token ?: NULL_TOKEN - - fun refreshToken() { - tokenRepository.refreshToken( - token = TokenManagerProvider.instance.manager.getToken()?.accessToken ?: NULL_TOKEN, - ) - } - - companion object { - private const val NULL_TOKEN = "null" - } -} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/service/SchoolRetrofitService.kt b/android/festago/app/src/main/java/com/festago/festago/data/service/SchoolRetrofitService.kt new file mode 100644 index 000000000..b21234535 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/service/SchoolRetrofitService.kt @@ -0,0 +1,11 @@ +package com.festago.festago.data.service + +import com.festago.festago.data.dto.SchoolsResponse +import retrofit2.Response +import retrofit2.http.GET + +interface SchoolRetrofitService { + + @GET("/schools") + suspend fun getSchools(): Response +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/service/StudentVerificationRetrofitService.kt b/android/festago/app/src/main/java/com/festago/festago/data/service/StudentVerificationRetrofitService.kt new file mode 100644 index 000000000..56718555f --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/service/StudentVerificationRetrofitService.kt @@ -0,0 +1,19 @@ +package com.festago.festago.data.service + +import com.festago.festago.data.dto.SendVerificationRequest +import com.festago.festago.data.dto.VerificationRequest +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + +interface StudentVerificationRetrofitService { + @POST("/students/send-verification") + suspend fun sendVerificationCode( + @Body sendVerificationRequest: SendVerificationRequest, + ): Response + + @POST("/students/verification") + suspend fun requestVerification( + @Body verificationRequest: VerificationRequest, + ): Response +} diff --git a/android/festago/app/src/main/java/com/festago/festago/data/service/TokenRetrofitService.kt b/android/festago/app/src/main/java/com/festago/festago/data/service/TokenRetrofitService.kt index 6e54d2a89..932391245 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/service/TokenRetrofitService.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/service/TokenRetrofitService.kt @@ -4,6 +4,8 @@ import com.festago.festago.data.dto.OauthRequest import com.festago.festago.data.dto.OauthTokenResponse import retrofit2.Response import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.Header import retrofit2.http.POST interface TokenRetrofitService { @@ -12,4 +14,9 @@ interface TokenRetrofitService { suspend fun getOauthToken( @Body oauthRequest: OauthRequest, ): Response + + @DELETE("/auth") + suspend fun deleteUserAccount( + @Header("authorization") token: String, + ): Response } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/service/UserRetrofitService.kt b/android/festago/app/src/main/java/com/festago/festago/data/service/UserRetrofitService.kt index 5874d86b3..7b645cc24 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/service/UserRetrofitService.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/service/UserRetrofitService.kt @@ -2,13 +2,9 @@ package com.festago.festago.data.service import com.festago.festago.data.dto.UserProfileResponse import retrofit2.Response -import retrofit2.http.DELETE import retrofit2.http.GET interface UserRetrofitService { @GET("/members/profile") suspend fun getUserProfile(): Response - - @DELETE("/auth") - suspend fun deleteUserAccount(): Response } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/util/ResponseExt.kt b/android/festago/app/src/main/java/com/festago/festago/data/util/ResponseExt.kt index 750ace4c3..8203cc483 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/util/ResponseExt.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/util/ResponseExt.kt @@ -2,18 +2,21 @@ package com.festago.festago.data.util import retrofit2.Response -fun Response.runCatchingWithErrorHandler(): Result { +suspend fun runCatchingResponse( + block: suspend () -> Response, +): Result { try { - if (this.isSuccessful && this.body() != null) { - return Result.success(this.body()!!) + val response = block() + if (response.isSuccessful && response.body() != null) { + return Result.success(response.body()!!) } return Result.failure( Throwable( "{" + - "code: ${this.code()}," + - "message: ${this.message()}, " + - "body: ${this.errorBody()?.string()}" + + "code: ${response.code()}," + + "message: ${response.message()}, " + + "body: ${response.errorBody()?.string()}" + "}", ), ) diff --git a/android/festago/app/src/main/java/com/festago/festago/data/util/ResultExt.kt b/android/festago/app/src/main/java/com/festago/festago/data/util/ResultExt.kt new file mode 100644 index 000000000..bc058d668 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/util/ResultExt.kt @@ -0,0 +1,12 @@ +package com.festago.festago.data.util + +suspend fun Result.onSuccessOrCatch(block: suspend (T) -> R): Result { + return try { + onSuccess { return Result.success(block(it)) } + onFailure { return Result.failure(it) } + + throw Throwable("This line should not be reached") + } catch (e: Exception) { + Result.failure(e) + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/di/AnalysisContainer.kt b/android/festago/app/src/main/java/com/festago/festago/di/AnalysisContainer.kt deleted file mode 100644 index ad63cda83..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/di/AnalysisContainer.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.festago.festago.di - -import com.festago.festago.analytics.AnalyticsHelper -import com.festago.festago.analytics.FirebaseAnalyticsHelper - -class AnalysisContainer { - val analyticsHelper: AnalyticsHelper = FirebaseAnalyticsHelper -} diff --git a/android/festago/app/src/main/java/com/festago/festago/di/AuthServiceContainer.kt b/android/festago/app/src/main/java/com/festago/festago/di/AuthServiceContainer.kt deleted file mode 100644 index 05e512157..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/di/AuthServiceContainer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.festago.festago.di - -import com.festago.festago.data.retrofit.AuthInterceptor -import com.festago.festago.data.service.TicketRetrofitService -import com.festago.festago.data.service.UserRetrofitService -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import retrofit2.Retrofit - -class AuthServiceContainer(baseUrl: String, tokenContainer: TokenContainer) { - - private val okHttpClient: OkHttpClient = OkHttpClient - .Builder() - .addInterceptor(AuthInterceptor(tokenContainer.tokenManager)) - .build() - - private val authRetrofit: Retrofit = Retrofit.Builder() - .baseUrl(baseUrl) - .client(okHttpClient) - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .build() - - val ticketRetrofitService: TicketRetrofitService by lazy { - authRetrofit.create(TicketRetrofitService::class.java) - } - - val userRetrofitService: UserRetrofitService by lazy { - authRetrofit.create(UserRetrofitService::class.java) - } -} diff --git a/android/festago/app/src/main/java/com/festago/festago/di/LocalDataSourceContainer.kt b/android/festago/app/src/main/java/com/festago/festago/di/LocalDataSourceContainer.kt deleted file mode 100644 index a1b9aa242..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/di/LocalDataSourceContainer.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.festago.festago.di - -import android.content.Context -import com.festago.festago.data.datasource.TokenDataSource -import com.festago.festago.data.datasource.TokenLocalDataSource - -class LocalDataSourceContainer(context: Context) { - val tokenDataSource: TokenDataSource = TokenLocalDataSource(context) -} diff --git a/android/festago/app/src/main/java/com/festago/festago/di/NormalServiceContainer.kt b/android/festago/app/src/main/java/com/festago/festago/di/NormalServiceContainer.kt deleted file mode 100644 index 509fd36f0..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/di/NormalServiceContainer.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.festago.festago.di - -import com.festago.festago.data.service.FestivalRetrofitService -import com.festago.festago.data.service.ReservationTicketRetrofitService -import com.festago.festago.data.service.TokenRetrofitService -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType -import retrofit2.Retrofit - -class NormalServiceContainer(baseUrl: String) { - private val normalRetrofit: Retrofit = Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .build() - - val festivalRetrofitService: FestivalRetrofitService by lazy { - normalRetrofit.create(FestivalRetrofitService::class.java) - } - - val tokenRetrofitService: TokenRetrofitService by lazy { - normalRetrofit.create(TokenRetrofitService::class.java) - } - - val reservationTicketRetrofitService: ReservationTicketRetrofitService by lazy { - normalRetrofit.create(ReservationTicketRetrofitService::class.java) - } -} diff --git a/android/festago/app/src/main/java/com/festago/festago/di/RepositoryContainer.kt b/android/festago/app/src/main/java/com/festago/festago/di/RepositoryContainer.kt deleted file mode 100644 index 886a0d2e0..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/di/RepositoryContainer.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.festago.festago.di - -import com.festago.festago.data.repository.AuthDefaultRepository -import com.festago.festago.data.repository.FestivalDefaultRepository -import com.festago.festago.data.repository.ReservationTicketDefaultRepository -import com.festago.festago.data.repository.TicketDefaultRepository -import com.festago.festago.data.repository.UserDefaultRepository -import com.festago.festago.repository.AuthRepository -import com.festago.festago.repository.FestivalRepository -import com.festago.festago.repository.ReservationTicketRepository -import com.festago.festago.repository.TicketRepository -import com.festago.festago.repository.UserRepository - -class RepositoryContainer( - private val authServiceContainer: AuthServiceContainer, - private val normalServiceContainer: NormalServiceContainer, - tokenContainer: TokenContainer, -) { - val authRepository: AuthRepository = AuthDefaultRepository( - tokenRepository = tokenContainer.tokenRepository, - userRetrofitService = authServiceContainer.userRetrofitService, - ) - - val festivalRepository: FestivalRepository - get() = FestivalDefaultRepository( - festivalRetrofitService = normalServiceContainer.festivalRetrofitService, - ) - - val ticketRepository: TicketRepository - get() = TicketDefaultRepository( - ticketRetrofitService = authServiceContainer.ticketRetrofitService, - ) - - val userRepository: UserRepository - get() = UserDefaultRepository( - userProfileService = authServiceContainer.userRetrofitService, - ) - - val reservationTicketRepository: ReservationTicketRepository - get() = ReservationTicketDefaultRepository( - reservationTicketRetrofitService = normalServiceContainer.reservationTicketRetrofitService, - ) -} diff --git a/android/festago/app/src/main/java/com/festago/festago/di/TokenContainer.kt b/android/festago/app/src/main/java/com/festago/festago/di/TokenContainer.kt deleted file mode 100644 index d17a15af9..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/di/TokenContainer.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.festago.festago.di - -import com.festago.festago.data.repository.TokenDefaultRepository -import com.festago.festago.data.retrofit.TokenManager - -class TokenContainer( - normalServiceContainer: NormalServiceContainer, - localDataSourceContainer: LocalDataSourceContainer, -) { - val tokenRepository = TokenDefaultRepository( - tokenRetrofitService = normalServiceContainer.tokenRetrofitService, - tokenLocalDataSource = localDataSourceContainer.tokenDataSource, - ) - val tokenManager = TokenManager(tokenRepository) -} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/fcm/FcmMessageType.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/fcm/FcmMessageType.kt new file mode 100644 index 000000000..6bb066104 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/fcm/FcmMessageType.kt @@ -0,0 +1,7 @@ +package com.festago.festago.presentation.fcm + +enum class FcmMessageType(val id: Int, val channelId: String) { + ENTRY_ALERT(id = 0, channelId = "ENTRY_ALERT"), + ENTRY_PROCESS(id = 1, channelId = "ENTRY_PROCESS"), + ; +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/fcm/NotificationManager.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/fcm/NotificationManager.kt new file mode 100644 index 000000000..6abea300b --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/fcm/NotificationManager.kt @@ -0,0 +1,46 @@ +package com.festago.festago.presentation.fcm + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.festago.festago.R +import com.festago.festago.presentation.fcm.FcmMessageType.ENTRY_ALERT +import com.festago.festago.presentation.ui.home.HomeActivity +import com.festago.festago.presentation.util.checkNotificationPermission + +class NotificationManager(private val context: Context) { + + private val intent = HomeActivity.getIntent(context).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + + private val pendingIntent = PendingIntent.getActivity( + context, + HOME_REQUEST_CODE, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + private val entryAlertNotificationBuilder = + NotificationCompat.Builder(context, ENTRY_ALERT.channelId) + .setSmallIcon(R.mipmap.ic_festago_logo_round) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + + fun sendEntryAlertNotification(title: String, body: String) { + entryAlertNotificationBuilder + .setContentTitle(title) + .setContentText(body) + + checkNotificationPermission(context) { + NotificationManagerCompat.from(context).notify(0, entryAlertNotificationBuilder.build()) + } + } + + companion object { + private const val HOME_REQUEST_CODE = 0 + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/fcm/TicketEntryService.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/fcm/TicketEntryService.kt new file mode 100644 index 000000000..b2061db31 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/fcm/TicketEntryService.kt @@ -0,0 +1,42 @@ +package com.festago.festago.presentation.fcm + +import com.festago.festago.presentation.fcm.FcmMessageType.ENTRY_ALERT +import com.festago.festago.presentation.fcm.FcmMessageType.ENTRY_PROCESS +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.runBlocking + +class TicketEntryService : FirebaseMessagingService() { + + private val notificationManager by lazy { NotificationManager(this) } + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + when (remoteMessage.notification?.channelId) { + ENTRY_ALERT.channelId -> handleEntryAlert(remoteMessage) + + ENTRY_PROCESS.channelId -> { + runBlocking { + ticketStateChangeEvent.emit(Unit) + } + } + + else -> Unit + } + } + + private fun handleEntryAlert(remoteMessage: RemoteMessage) { + notificationManager.sendEntryAlertNotification( + remoteMessage.notification?.title ?: "", + remoteMessage.notification?.body ?: "" + ) + } + + override fun onNewToken(token: String) { + // TODO: 토큰이 변경되었을 때 처리 + } + + companion object { + val ticketStateChangeEvent: MutableSharedFlow = MutableSharedFlow() + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/FestivalMapper.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/FestivalMapper.kt deleted file mode 100644 index b69bfa493..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/FestivalMapper.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.festago.festago.presentation.mapper - -import com.festago.festago.model.Festival -import com.festago.festago.presentation.model.FestivalUiModel - -fun Festival.toPresentation(): FestivalUiModel = - FestivalUiModel(id, name, startDate, endDate, thumbnail) - -fun List.toPresentation(): List = this.map { it.toPresentation() } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/ReservationMapper.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/ReservationMapper.kt deleted file mode 100644 index e69de29bb..000000000 diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/ReservationStageMapper.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/ReservationStageMapper.kt deleted file mode 100644 index 4c5c35f64..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/ReservationStageMapper.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.festago.festago.presentation.mapper - -import com.festago.festago.model.ReservationStage -import com.festago.festago.presentation.model.ReservationStageUiModel -import com.festago.festago.presentation.ui.ticketreserve.TicketReserveItemUiState -import java.time.LocalDateTime - -fun List.toPresentation() = map { it.toPresentation() } - -fun ReservationStage.toPresentation() = ReservationStageUiModel( - id = id, - lineUp = lineUp, - reservationTickets = reservationTickets.map { it.toPresentation() }, - startTime = startTime, - ticketOpenTime = ticketOpenTime, - canReserve = LocalDateTime.now().isAfter(ticketOpenTime), -) - -fun ReservationStageUiModel.toDomain() = ReservationStage( - id = id, - lineUp = lineUp, - reservationTickets = reservationTickets.map { it.toDomain() }, - startTime = startTime, - ticketOpenTime = ticketOpenTime, -) - -fun TicketReserveItemUiState.toPresentation() = ReservationStageUiModel( - id = id, - lineUp = lineUp, - startTime = startTime, - ticketOpenTime = ticketOpenTime, - reservationTickets = reservationTickets, - canReserve = canReserve, -) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/ReservationTicketMapper.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/ReservationTicketMapper.kt deleted file mode 100644 index 4f17bb0f2..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/ReservationTicketMapper.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.festago.festago.presentation.mapper - -import com.festago.festago.model.ReservationTicket -import com.festago.festago.presentation.model.ReservationTicketUiModel - -fun ReservationTicket.toPresentation() = ReservationTicketUiModel( - id = id, - remainAmount = remainAmount, - ticketType = ticketType, - totalAmount = totalAmount, -) - -fun ReservationTicketUiModel.toDomain() = ReservationTicket( - id = id, - remainAmount = remainAmount, - ticketType = ticketType, - totalAmount = totalAmount, -) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/ReservedTicketMapper.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/ReservedTicketMapper.kt deleted file mode 100644 index 81eaaaca3..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/ReservedTicketMapper.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.festago.festago.presentation.mapper - -import com.festago.festago.model.ReservedTicket -import com.festago.festago.presentation.model.ReservedTicketUiModel - -fun ReservedTicket.toPresentation() = ReservedTicketUiModel( - ticketId = id, - number = number, - entryTime = entryTime, -) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/StageMapper.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/StageMapper.kt deleted file mode 100644 index f6ca17e9b..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/StageMapper.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.festago.festago.presentation.mapper - -import com.festago.festago.model.Stage -import com.festago.festago.presentation.model.StageUiModel - -fun Stage.toPresentation() = StageUiModel( - id = id, - startTime = startTime, -) - -fun StageUiModel.toDomain() = Stage( - id = id, - startTime = startTime, -) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/TicketCodeMapper.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/TicketCodeMapper.kt deleted file mode 100644 index 56117dafd..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/TicketCodeMapper.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.festago.festago.presentation.mapper - -import com.festago.festago.model.TicketCode -import com.festago.festago.presentation.model.TicketCodeUiModel - -fun TicketCode.toPresentation(): TicketCodeUiModel = TicketCodeUiModel(code = code, period = period) - -fun TicketCodeUiModel.toDomain(): TicketCode = TicketCode(code = code, period = period) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/TicketMapper.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/TicketMapper.kt deleted file mode 100644 index 9c8506a44..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/TicketMapper.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.festago.festago.presentation.mapper - -import com.festago.festago.model.Ticket -import com.festago.festago.presentation.model.TicketUiModel - -fun Ticket.toPresentation(): TicketUiModel = TicketUiModel( - id = id, - number = number, - entryTime = entryTime, - condition = condition.toPresentation(), - stage = stage.toPresentation(), - reserveAt = reserveAt, - festivalId = festivalTicket.id, - festivalName = festivalTicket.name, - festivalThumbnail = festivalTicket.thumbnail, -) - -fun List.toPresentation(): List = - this.map { ticket -> ticket.toPresentation() } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/TicketStateMapper.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/TicketStateMapper.kt deleted file mode 100644 index c74b34fd7..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/TicketStateMapper.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.festago.festago.presentation.mapper - -import com.festago.festago.model.TicketCondition -import com.festago.festago.presentation.model.TicketConditionUiModel - -fun TicketCondition.toPresentation(): TicketConditionUiModel = - when (this) { - TicketCondition.BEFORE_ENTRY -> TicketConditionUiModel.BEFORE_ENTRY - TicketCondition.AFTER_ENTRY -> TicketConditionUiModel.AFTER_ENTRY - TicketCondition.AWAY -> TicketConditionUiModel.AWAY - } - -fun TicketConditionUiModel.toDomain(): TicketCondition = - when (this) { - TicketConditionUiModel.BEFORE_ENTRY -> TicketCondition.BEFORE_ENTRY - TicketConditionUiModel.AFTER_ENTRY -> TicketCondition.AFTER_ENTRY - TicketConditionUiModel.AWAY -> TicketCondition.AWAY - } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/UserProfileMapper.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/UserProfileMapper.kt deleted file mode 100644 index a959a0af5..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/mapper/UserProfileMapper.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.festago.festago.presentation.mapper - -import com.festago.festago.model.UserProfile -import com.festago.festago.presentation.model.UserProfileUiModel - -fun UserProfile.toPresentation(): UserProfileUiModel = UserProfileUiModel( - memberId = memberId, - nickName = nickName, - profileImage = profileImage, -) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/FestivalUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/model/FestivalUiModel.kt deleted file mode 100644 index 44dadbad9..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/FestivalUiModel.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.festago.festago.presentation.model - -import java.time.LocalDate - -data class FestivalUiModel( - val id: Long, - val name: String, - val startDate: LocalDate, - val endDate: LocalDate, - val thumbnail: String, -) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservationStageUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservationStageUiModel.kt deleted file mode 100644 index a2662550a..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservationStageUiModel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.festago.festago.presentation.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import java.time.LocalDateTime - -@Parcelize -data class ReservationStageUiModel( - val id: Int, - val lineUp: String, - val startTime: LocalDateTime, - val ticketOpenTime: LocalDateTime, - val reservationTickets: List, - val canReserve: Boolean, -) : Parcelable diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/StageUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/model/StageUiModel.kt deleted file mode 100644 index 7ab8bc6e4..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/StageUiModel.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.festago.festago.presentation.model - -import java.time.LocalDateTime - -data class StageUiModel( - val id: Int = -1, - val startTime: LocalDateTime = LocalDateTime.MIN, -) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketCodeUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketCodeUiModel.kt deleted file mode 100644 index 6c7d699b1..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketCodeUiModel.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.festago.festago.presentation.model - -data class TicketCodeUiModel( - val code: String, - val period: Int, -) { - companion object { - val EMPTY = TicketCodeUiModel(code = "code", period = 0) - } -} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketConditionUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketConditionUiModel.kt deleted file mode 100644 index 53fabaa82..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketConditionUiModel.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.festago.festago.presentation.model - -import androidx.annotation.StringRes -import com.festago.festago.R - -enum class TicketConditionUiModel(@StringRes val stateName: Int) { - - BEFORE_ENTRY(R.string.all_ticket_state_before_entry), - AFTER_ENTRY(R.string.all_ticket_state_after_entry), - AWAY(R.string.all_ticket_state_away), -} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketUiModel.kt deleted file mode 100644 index 432bf2ae6..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketUiModel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.festago.festago.presentation.model - -import java.time.LocalDateTime - -data class TicketUiModel( - val id: Long = -1, - val number: Int = -1, - val entryTime: LocalDateTime = LocalDateTime.MIN, - val reserveAt: LocalDateTime = LocalDateTime.MIN, - val condition: TicketConditionUiModel = TicketConditionUiModel.BEFORE_ENTRY, - val stage: StageUiModel = StageUiModel(), - val festivalId: Int = -1, - val festivalName: String = "", - val festivalThumbnail: String = "", -) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/UserProfileUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/model/UserProfileUiModel.kt deleted file mode 100644 index 223f24371..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/UserProfileUiModel.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.festago.festago.presentation.model - -data class UserProfileUiModel( - val memberId: Long = -1, - val nickName: String = "", - val profileImage: String = "", -) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ViewModelFactory.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ViewModelFactory.kt deleted file mode 100644 index b7a1888d5..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ViewModelFactory.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.festago.festago.presentation.ui - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.festago.festago.FestagoApplication -import com.festago.festago.presentation.ui.home.HomeViewModel -import com.festago.festago.presentation.ui.home.festivallist.FestivalListViewModel -import com.festago.festago.presentation.ui.home.mypage.MyPageViewModel -import com.festago.festago.presentation.ui.home.ticketlist.TicketListViewModel -import com.festago.festago.presentation.ui.signin.SignInViewModel -import com.festago.festago.presentation.ui.ticketentry.TicketEntryViewModel -import com.festago.festago.presentation.ui.tickethistory.TicketHistoryViewModel -import com.festago.festago.presentation.ui.ticketreserve.TicketReserveViewModel - -@Suppress("UNCHECKED_CAST") -val FestagoViewModelFactory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { - val repositoryContainer = FestagoApplication.repositoryContainer - val analysisContainer = FestagoApplication.analysisContainer - - override fun create(modelClass: Class): T { - return when { - modelClass.isAssignableFrom(TicketHistoryViewModel::class.java) -> TicketHistoryViewModel( - ticketRepository = repositoryContainer.ticketRepository, - analyticsHelper = analysisContainer.analyticsHelper, - ) - - modelClass.isAssignableFrom(TicketReserveViewModel::class.java) -> TicketReserveViewModel( - reservationTicketRepository = repositoryContainer.reservationTicketRepository, - festivalRepository = repositoryContainer.festivalRepository, - ticketRepository = repositoryContainer.ticketRepository, - authRepository = repositoryContainer.authRepository, - analyticsHelper = analysisContainer.analyticsHelper, - ) - - modelClass.isAssignableFrom(TicketEntryViewModel::class.java) -> TicketEntryViewModel( - ticketRepository = repositoryContainer.ticketRepository, - analyticsHelper = analysisContainer.analyticsHelper, - ) - - modelClass.isAssignableFrom(SignInViewModel::class.java) -> SignInViewModel( - authRepository = repositoryContainer.authRepository, - analyticsHelper = analysisContainer.analyticsHelper, - ) - - modelClass.isAssignableFrom(HomeViewModel::class.java) -> HomeViewModel( - authRepository = repositoryContainer.authRepository, - ) - - modelClass.isAssignableFrom(FestivalListViewModel::class.java) -> FestivalListViewModel( - festivalRepository = repositoryContainer.festivalRepository, - analyticsHelper = analysisContainer.analyticsHelper, - ) - - modelClass.isAssignableFrom(MyPageViewModel::class.java) -> MyPageViewModel( - userRepository = repositoryContainer.userRepository, - ticketRepository = repositoryContainer.ticketRepository, - authRepository = repositoryContainer.authRepository, - analyticsHelper = analysisContainer.analyticsHelper, - ) - - modelClass.isAssignableFrom(TicketListViewModel::class.java) -> TicketListViewModel( - ticketRepository = repositoryContainer.ticketRepository, - analyticsHelper = analysisContainer.analyticsHelper, - ) - - else -> throw IllegalArgumentException("ViewModelFactory에 정의되지않은 뷰모델을 생성하였습니다 : ${modelClass.name}") - } as T - } -} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/customview/OkDialogFragment.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/customview/OkDialogFragment.kt index 7d4a431d9..1b3500a8a 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/customview/OkDialogFragment.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/customview/OkDialogFragment.kt @@ -11,7 +11,9 @@ import android.view.WindowManager import androidx.fragment.app.DialogFragment import com.festago.festago.databinding.FragmentOkDialogBinding import com.festago.festago.presentation.ui.customview.OkDialogFragment.OnClickListener +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class OkDialogFragment : DialogFragment() { private var _binding: FragmentOkDialogBinding? = null diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeActivity.kt index d2df6fc11..9b7fe0a8a 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeActivity.kt @@ -3,58 +3,102 @@ package com.festago.festago.presentation.ui.home import android.content.Context import android.content.Intent import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import com.festago.festago.R import com.festago.festago.databinding.ActivityHomeBinding -import com.festago.festago.presentation.ui.FestagoViewModelFactory import com.festago.festago.presentation.ui.home.festivallist.FestivalListFragment import com.festago.festago.presentation.ui.home.mypage.MyPageFragment import com.festago.festago.presentation.ui.home.ticketlist.TicketListFragment import com.festago.festago.presentation.ui.signin.SignInActivity +import com.festago.festago.presentation.util.repeatOnStarted +import com.festago.festago.presentation.util.requestNotificationPermission +import com.google.android.material.navigation.NavigationBarView +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class HomeActivity : AppCompatActivity() { - private var _binding: ActivityHomeBinding? = null - private val binding get() = _binding!! + private val binding by lazy { ActivityHomeBinding.inflate(layoutInflater) } - private val vm: HomeViewModel by viewModels { FestagoViewModelFactory } + private val vm: HomeViewModel by viewModels() + + private lateinit var resultLauncher: ActivityResultLauncher + + private val navigationBarView by lazy { binding.nvHome as NavigationBarView } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initBinding() initView() initObserve() + initResultLauncher() + } + + private fun initResultLauncher() { + resultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == SignInActivity.RESULT_NOT_SIGN_IN) { + navigationBarView.selectedItemId = R.id.item_festival + } + } + initNotificationPermission() } private fun initBinding() { - _binding = ActivityHomeBinding.inflate(layoutInflater) setContentView(binding.root) } private fun initView() { - binding.bnvHome.setOnItemSelectedListener { - vm.loadHomeItem(getItemType(it.itemId)) + navigationBarView.setOnItemSelectedListener { + vm.selectItem(getItemType(it.itemId)) true } binding.fabTicket.setOnClickListener { - binding.bnvHome.selectedItemId = R.id.item_ticket + navigationBarView.selectedItemId = R.id.item_ticket } changeFragment() } private fun initObserve() { - vm.event.observe(this) { event -> - when (event) { - is HomeEvent.ShowFestivalList -> showFestivalList() - is HomeEvent.ShowTicketList -> showTicketList() - is HomeEvent.ShowMyPage -> showMyPage() - is HomeEvent.ShowSignIn -> showSignIn() + repeatOnStarted(this) { + vm.event.collect { event -> + when (event) { + is HomeEvent.ShowSignIn -> showSignIn() + } + } + } + + repeatOnStarted(this) { + vm.selectedItem.collect { homeItemType -> + when (homeItemType) { + HomeItemType.FESTIVAL_LIST -> showFestivalList() + HomeItemType.TICKET_LIST -> showTicketList() + HomeItemType.MY_PAGE -> showMyPage() + } + } + } + } + + private fun initNotificationPermission() { + val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { isGranted: Boolean -> + if (!isGranted) { + Toast.makeText( + this, + getString(R.string.home_notification_permission_denied), + Toast.LENGTH_SHORT, + ).show() } } + requestNotificationPermission(requestPermissionLauncher) } private fun getItemType(menuItemId: Int): HomeItemType { @@ -82,7 +126,7 @@ class HomeActivity : AppCompatActivity() { } private fun showSignIn() { - startActivity(SignInActivity.getIntent(this)) + resultLauncher.launch(SignInActivity.getIntent(this)) } private inline fun changeFragment() { diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeEvent.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeEvent.kt index 4539dd48b..f3e5725b7 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeEvent.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeEvent.kt @@ -1,8 +1,5 @@ package com.festago.festago.presentation.ui.home sealed interface HomeEvent { - object ShowFestivalList : HomeEvent - object ShowTicketList : HomeEvent - object ShowMyPage : HomeEvent object ShowSignIn : HomeEvent } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeViewModel.kt index 23e25f3e4..a6dbf1022 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeViewModel.kt @@ -1,24 +1,42 @@ package com.festago.festago.presentation.ui.home import androidx.lifecycle.ViewModel -import com.festago.festago.presentation.ui.home.HomeItemType.FESTIVAL_LIST -import com.festago.festago.presentation.ui.home.HomeItemType.MY_PAGE -import com.festago.festago.presentation.ui.home.HomeItemType.TICKET_LIST -import com.festago.festago.presentation.util.MutableSingleLiveData -import com.festago.festago.presentation.util.SingleLiveData +import androidx.lifecycle.viewModelScope import com.festago.festago.repository.AuthRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject -class HomeViewModel(private val authRepository: AuthRepository) : ViewModel() { +@HiltViewModel +class HomeViewModel @Inject constructor(private val authRepository: AuthRepository) : ViewModel() { - private val _event = MutableSingleLiveData() - val event: SingleLiveData = _event + private val _event = MutableSharedFlow() + val event: SharedFlow = _event.asSharedFlow() - fun loadHomeItem(homeItemType: HomeItemType) { - when { - homeItemType == FESTIVAL_LIST -> _event.setValue(HomeEvent.ShowFestivalList) - !authRepository.isSigned -> _event.setValue(HomeEvent.ShowSignIn) - homeItemType == TICKET_LIST -> _event.setValue(HomeEvent.ShowTicketList) - homeItemType == MY_PAGE -> _event.setValue(HomeEvent.ShowMyPage) + private val _selectedItem = MutableStateFlow(HomeItemType.FESTIVAL_LIST) + val selectedItem: StateFlow = _selectedItem.asStateFlow() + + fun selectItem(homeItemType: HomeItemType) { + when (homeItemType) { + HomeItemType.FESTIVAL_LIST -> _selectedItem.value = homeItemType + HomeItemType.TICKET_LIST -> selectItemOrSignIn(HomeItemType.TICKET_LIST) + HomeItemType.MY_PAGE -> selectItemOrSignIn(HomeItemType.MY_PAGE) + } + } + + private fun selectItemOrSignIn(homeItemType: HomeItemType) { + viewModelScope.launch { + if (authRepository.isSigned) { + _selectedItem.emit(homeItemType) + } else { + _event.emit(HomeEvent.ShowSignIn) + } } } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt index a69fb51c5..04e701e44 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt @@ -6,18 +6,21 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.GridLayoutManager import com.festago.festago.R import com.festago.festago.databinding.FragmentFestivalListBinding -import com.festago.festago.presentation.ui.FestagoViewModelFactory import com.festago.festago.presentation.ui.home.ticketlist.TicketListFragment import com.festago.festago.presentation.ui.ticketreserve.TicketReserveActivity +import com.festago.festago.presentation.util.repeatOnStarted +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class FestivalListFragment : Fragment(R.layout.fragment_festival_list) { private var _binding: FragmentFestivalListBinding? = null private val binding get() = _binding!! - private val vm: FestivalListViewModel by viewModels { FestagoViewModelFactory } + private val vm: FestivalListViewModel by viewModels() private lateinit var adapter: FestivalListAdapter override fun onCreateView( @@ -37,22 +40,41 @@ class FestivalListFragment : Fragment(R.layout.fragment_festival_list) { } private fun initObserve() { - vm.uiState.observe(viewLifecycleOwner) { - binding.uiState = it - updateUi(it) + repeatOnStarted(viewLifecycleOwner) { + vm.uiState.collect { + binding.uiState = it + updateUi(it) + } } - vm.event.observe(viewLifecycleOwner) { - handleEvent(it) + repeatOnStarted(viewLifecycleOwner) { + vm.event.collect { + handleEvent(it) + } } } + private val Int.dp: Int get() = (this / resources.displayMetrics.density).toInt() + private fun initView() { adapter = FestivalListAdapter() binding.rvFestivalList.adapter = adapter + + binding.rvFestivalList.layoutManager.apply { + if (this is GridLayoutManager) { + val spanSize = (resources.displayMetrics.widthPixels.dp / 160) + spanCount = when { + spanSize < 2 -> 2 + spanSize > 4 -> 4 + else -> spanSize + } + } + } + vm.loadFestivals() binding.srlFestivalList.setOnRefreshListener { vm.loadFestivals() + binding.srlFestivalList.isRefreshing = false } } @@ -60,7 +82,7 @@ class FestivalListFragment : Fragment(R.layout.fragment_festival_list) { when (uiState) { is FestivalListUiState.Loading, is FestivalListUiState.Error, - -> binding.srlFestivalList.isRefreshing = false + -> Unit is FestivalListUiState.Success -> handleSuccess(uiState) } @@ -68,7 +90,6 @@ class FestivalListFragment : Fragment(R.layout.fragment_festival_list) { private fun handleSuccess(uiState: FestivalListUiState.Success) { adapter.submitList(uiState.festivals) - binding.srlFestivalList.isRefreshing = false } private fun handleEvent(event: FestivalListEvent) { diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt index 08da64fdb..78d86dc88 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt @@ -1,26 +1,32 @@ package com.festago.festago.presentation.ui.home.festivallist -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure import com.festago.festago.presentation.ui.home.festivallist.FestivalListEvent.ShowTicketReserve -import com.festago.festago.presentation.util.MutableSingleLiveData -import com.festago.festago.presentation.util.SingleLiveData import com.festago.festago.repository.FestivalRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import javax.inject.Inject -class FestivalListViewModel( +@HiltViewModel +class FestivalListViewModel @Inject constructor( private val festivalRepository: FestivalRepository, private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _uiState = MutableLiveData(FestivalListUiState.Loading) - val uiState: LiveData = _uiState - private val _event = MutableSingleLiveData() - val event: SingleLiveData = _event + private val _uiState = MutableStateFlow(FestivalListUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _event = MutableSharedFlow() + val event: SharedFlow = _event.asSharedFlow() fun loadFestivals() { viewModelScope.launch { @@ -46,7 +52,9 @@ class FestivalListViewModel( } fun showTicketReserve(festivalId: Long) { - _event.setValue(ShowTicketReserve(festivalId)) + viewModelScope.launch { + _event.emit(ShowTicketReserve(festivalId)) + } } companion object { diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageFragment.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageFragment.kt index a0dac3641..8859752bc 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageFragment.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageFragment.kt @@ -9,17 +9,20 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.festago.festago.R import com.festago.festago.databinding.FragmentMyPageBinding -import com.festago.festago.presentation.ui.FestagoViewModelFactory import com.festago.festago.presentation.ui.home.HomeActivity +import com.festago.festago.presentation.ui.selectschool.SelectSchoolActivity import com.festago.festago.presentation.ui.signin.SignInActivity import com.festago.festago.presentation.ui.tickethistory.TicketHistoryActivity +import com.festago.festago.presentation.util.repeatOnStarted +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MyPageFragment : Fragment(R.layout.fragment_my_page) { private var _binding: FragmentMyPageBinding? = null private val binding get() = _binding!! - private val vm: MyPageViewModel by viewModels { FestagoViewModelFactory } + private val vm: MyPageViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, @@ -38,26 +41,37 @@ class MyPageFragment : Fragment(R.layout.fragment_my_page) { } private fun initObserve() { - vm.uiState.observe(viewLifecycleOwner) { uiState -> - binding.uiState = uiState - when (uiState) { - is MyPageUiState.Loading, is MyPageUiState.Error -> Unit - - is MyPageUiState.Success -> handleSuccess(uiState) + repeatOnStarted(viewLifecycleOwner) { + vm.uiState.collect { uiState -> + handleUiState(uiState) } - binding.srlMyPage.isRefreshing = false } - vm.event.observe(viewLifecycleOwner) { event -> - when (event) { - is MyPageEvent.ShowSignIn -> handleShowSignInEvent() - is MyPageEvent.SignOutSuccess -> handleSignOutSuccessEvent() - is MyPageEvent.DeleteAccountSuccess -> handleDeleteAccountSuccess() - is MyPageEvent.ShowTicketHistory -> handleShowTicketHistory() - is MyPageEvent.ShowConfirmDelete -> handleShowConfirmDelete() + repeatOnStarted(viewLifecycleOwner) { + vm.event.collect { event -> + handleEvent(event) } } } + private fun handleUiState(uiState: MyPageUiState) { + binding.uiState = uiState + when (uiState) { + is MyPageUiState.Loading, is MyPageUiState.Error -> Unit + + is MyPageUiState.Success -> handleSuccess(uiState) + } + } + + private fun handleEvent(event: MyPageEvent) { + when (event) { + is MyPageEvent.ShowSignIn -> handleShowSignInEvent() + is MyPageEvent.SignOutSuccess -> handleSignOutSuccessEvent() + is MyPageEvent.DeleteAccountSuccess -> handleDeleteAccountSuccess() + is MyPageEvent.ShowTicketHistory -> handleShowTicketHistory() + is MyPageEvent.ShowConfirmDelete -> handleShowConfirmDelete() + } + } + private fun handleShowSignInEvent() { startActivity(SignInActivity.getIntent(requireContext())) } @@ -101,6 +115,11 @@ class MyPageFragment : Fragment(R.layout.fragment_my_page) { binding.srlMyPage.setOnRefreshListener { vm.loadUserInfo() + binding.srlMyPage.isRefreshing = false + } + + binding.tvSchoolAuthorization.setOnClickListener { + startActivity(SelectSchoolActivity.getIntent(requireContext())) } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageUiState.kt index 389c85bba..4d6f74f69 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageUiState.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageUiState.kt @@ -1,16 +1,16 @@ package com.festago.festago.presentation.ui.home.mypage -import com.festago.festago.presentation.model.TicketUiModel -import com.festago.festago.presentation.model.UserProfileUiModel +import com.festago.festago.model.Ticket +import com.festago.festago.model.UserProfile sealed interface MyPageUiState { object Loading : MyPageUiState data class Success( - val userProfile: UserProfileUiModel = UserProfileUiModel(), - val ticket: TicketUiModel = TicketUiModel(), + val userProfile: UserProfile, + val ticket: Ticket?, ) : MyPageUiState { - val hasTicket: Boolean get() = ticket.id != -1L + val hasTicket: Boolean get() = ticket != null } object Error : MyPageUiState diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageViewModel.kt index 33b8a3db6..d1d91d66f 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/mypage/MyPageViewModel.kt @@ -1,78 +1,54 @@ package com.festago.festago.presentation.ui.home.mypage -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.presentation.mapper.toPresentation -import com.festago.festago.presentation.model.TicketUiModel -import com.festago.festago.presentation.util.MutableSingleLiveData -import com.festago.festago.presentation.util.SingleLiveData import com.festago.festago.repository.AuthRepository import com.festago.festago.repository.TicketRepository import com.festago.festago.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import javax.inject.Inject -class MyPageViewModel( +@HiltViewModel +class MyPageViewModel @Inject constructor( private val userRepository: UserRepository, private val ticketRepository: TicketRepository, private val authRepository: AuthRepository, private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _uiState = MutableLiveData(MyPageUiState.Loading) - val uiState: LiveData = _uiState + private val _uiState = MutableStateFlow(MyPageUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() - private val _event = MutableSingleLiveData() - val event: SingleLiveData = _event + private val _event = MutableSharedFlow() + val event: SharedFlow = _event.asSharedFlow() fun loadUserInfo() { if (!authRepository.isSigned) { - _event.setValue(MyPageEvent.ShowSignIn) - _uiState.value = MyPageUiState.Error + viewModelScope.launch { + _event.emit(MyPageEvent.ShowSignIn) + _uiState.value = MyPageUiState.Error + } return } viewModelScope.launch { - loadUserProfile() - loadFirstTicket() - } - } - - private suspend fun loadUserProfile() { - userRepository.loadUserProfile() - .onSuccess { - when (val current = uiState.value) { - is MyPageUiState.Error, - is MyPageUiState.Loading, - null, - -> _uiState.value = MyPageUiState.Success(userProfile = it.toPresentation()) + val deferredUserProfile = async { userRepository.loadUserProfile() } + val deferredHistoryTicket = async { ticketRepository.loadHistoryTickets(size = 1) } - is MyPageUiState.Success -> - _uiState.value = current.copy(userProfile = it.toPresentation()) - } - }.onFailure { - _uiState.value = MyPageUiState.Error - analyticsHelper.logNetworkFailure( - key = KEY_LOAD_USER_INFO, - value = it.message.toString(), + runCatching { + _uiState.value = MyPageUiState.Success( + userProfile = deferredUserProfile.await().getOrThrow(), + ticket = deferredHistoryTicket.await().getOrThrow().firstOrNull(), ) - } - } - - private suspend fun loadFirstTicket() { - ticketRepository.loadHistoryTickets(size = 1) - .onSuccess { - val ticket = it.firstOrNull()?.toPresentation() ?: TicketUiModel() - when (val current = uiState.value) { - is MyPageUiState.Error, null -> Unit - - is MyPageUiState.Loading -> - _uiState.value = MyPageUiState.Success(ticket = ticket) - - is MyPageUiState.Success -> _uiState.value = current.copy(ticket = ticket) - } }.onFailure { _uiState.value = MyPageUiState.Error analyticsHelper.logNetworkFailure( @@ -80,16 +56,16 @@ class MyPageViewModel( value = it.message.toString(), ) } + } } fun signOut() { viewModelScope.launch { authRepository.signOut() .onSuccess { - _event.setValue(MyPageEvent.SignOutSuccess) + _event.emit(MyPageEvent.SignOutSuccess) _uiState.value = MyPageUiState.Error }.onFailure { - _uiState.value = MyPageUiState.Error analyticsHelper.logNetworkFailure( key = KEY_SIGN_OUT, value = it.message.toString(), @@ -99,17 +75,18 @@ class MyPageViewModel( } fun showConfirmDelete() { - _event.setValue(MyPageEvent.ShowConfirmDelete) + viewModelScope.launch { + _event.emit(MyPageEvent.ShowConfirmDelete) + } } fun deleteAccount() { viewModelScope.launch { authRepository.deleteAccount() .onSuccess { - _event.setValue(MyPageEvent.DeleteAccountSuccess) + _event.emit(MyPageEvent.DeleteAccountSuccess) _uiState.value = MyPageUiState.Error }.onFailure { - _uiState.value = MyPageUiState.Error analyticsHelper.logNetworkFailure( key = KEY_DELETE_ACCOUNT, value = it.message.toString(), @@ -119,7 +96,9 @@ class MyPageViewModel( } fun showTicketHistory() { - _event.setValue(MyPageEvent.ShowTicketHistory) + viewModelScope.launch { + _event.emit(MyPageEvent.ShowTicketHistory) + } } companion object { diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListFragment.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListFragment.kt index 76bf53220..b3fa56d5a 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListFragment.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListFragment.kt @@ -11,9 +11,11 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.festago.festago.R import com.festago.festago.databinding.FragmentTicketListBinding -import com.festago.festago.presentation.ui.FestagoViewModelFactory import com.festago.festago.presentation.ui.ticketentry.TicketEntryActivity +import com.festago.festago.presentation.util.repeatOnStarted +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class TicketListFragment : Fragment(R.layout.fragment_ticket_list) { private var _binding: FragmentTicketListBinding? = null @@ -23,7 +25,7 @@ class TicketListFragment : Fragment(R.layout.fragment_ticket_list) { private lateinit var resultLauncher: ActivityResultLauncher - private val vm: TicketListViewModel by viewModels { FestagoViewModelFactory } + private val vm: TicketListViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, @@ -43,12 +45,16 @@ class TicketListFragment : Fragment(R.layout.fragment_ticket_list) { } private fun initObserve() { - vm.uiState.observe(viewLifecycleOwner) { - binding.uiState = it - updateUi(it) + repeatOnStarted(viewLifecycleOwner) { + vm.uiState.collect { + binding.uiState = it + updateUi(it) + } } - vm.event.observe(viewLifecycleOwner) { event -> - handleEvent(event) + repeatOnStarted(viewLifecycleOwner) { + vm.event.collect { event -> + handleEvent(event) + } } } @@ -56,11 +62,10 @@ class TicketListFragment : Fragment(R.layout.fragment_ticket_list) { when (uiState) { is TicketListUiState.Loading, is TicketListUiState.Error, - -> binding.srlTicketList.isRefreshing = false + -> Unit is TicketListUiState.Success -> { adapter.submitList(uiState.tickets) - binding.srlTicketList.isRefreshing = false } } } @@ -100,6 +105,7 @@ class TicketListFragment : Fragment(R.layout.fragment_ticket_list) { private fun initRefresh() { binding.srlTicketList.setOnRefreshListener { vm.loadCurrentTickets() + binding.srlTicketList.isRefreshing = false } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListItemUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListItemUiState.kt index d5c03ca6e..01cd67ecd 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListItemUiState.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListItemUiState.kt @@ -1,19 +1,36 @@ package com.festago.festago.presentation.ui.home.ticketlist -import com.festago.festago.presentation.model.StageUiModel -import com.festago.festago.presentation.model.TicketConditionUiModel +import com.festago.festago.model.Stage +import com.festago.festago.model.Ticket +import com.festago.festago.model.TicketCondition import java.time.LocalDateTime data class TicketListItemUiState( - val id: Long = -1, - val number: Int = -1, - val entryTime: LocalDateTime = LocalDateTime.MIN, - val reserveAt: LocalDateTime = LocalDateTime.MIN, - val condition: TicketConditionUiModel = TicketConditionUiModel.BEFORE_ENTRY, - val stage: StageUiModel = StageUiModel(), - val festivalId: Int = -1, - val festivalName: String = "", - val festivalThumbnail: String = "", + val id: Long, + val number: Int, + val entryTime: LocalDateTime, + val reserveAt: LocalDateTime, + val condition: TicketCondition, + val stage: Stage, + val festivalId: Int, + val festivalName: String, + val festivalThumbnail: String, val canEntry: Boolean, val onTicketEntry: (ticketId: Long) -> Unit, -) +) { + companion object { + fun of(ticket: Ticket, onTicketEntry: (ticketId: Long) -> Unit) = TicketListItemUiState( + id = ticket.id, + number = ticket.number, + entryTime = ticket.entryTime, + reserveAt = ticket.reserveAt, + condition = ticket.condition, + stage = ticket.stage, + festivalId = ticket.festivalTicket.id, + festivalName = ticket.festivalTicket.name, + festivalThumbnail = ticket.festivalTicket.thumbnail, + canEntry = LocalDateTime.now().isAfter(ticket.entryTime), + onTicketEntry = onTicketEntry, + ) + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListItemViewHolder.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListItemViewHolder.kt index 26f34bd45..ebd8045c4 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListItemViewHolder.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListItemViewHolder.kt @@ -6,6 +6,7 @@ import android.widget.Button import androidx.recyclerview.widget.RecyclerView import com.festago.festago.R import com.festago.festago.databinding.ItemTicketListBinding +import com.festago.festago.model.TicketCondition import java.time.format.DateTimeFormatter class TicketListItemViewHolder( @@ -14,13 +15,22 @@ class TicketListItemViewHolder( fun bind(item: TicketListItemUiState) { binding.ticket = item + setTicketConditionText(item) setTicketEntryBtn(item) } + private fun setTicketConditionText(item: TicketListItemUiState) { + val ticketConditionResId = when (item.condition) { + TicketCondition.BEFORE_ENTRY -> R.string.all_ticket_state_before_entry + TicketCondition.AFTER_ENTRY -> R.string.all_ticket_state_after_entry + TicketCondition.AWAY -> R.string.all_ticket_state_away + } + binding.tvTicketCondition.setText(ticketConditionResId) + } + private fun setTicketEntryBtn(item: TicketListItemUiState) { val btn = binding.btnTicketEntry btn.isEnabled = item.canEntry - setTicketEntryBtnText(isAfterEntryTime = item.canEntry, btn = btn, ticket = item) } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModel.kt index 8a7788194..ce15bfd2b 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModel.kt @@ -1,35 +1,39 @@ package com.festago.festago.presentation.ui.home.ticketlist -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.model.Ticket -import com.festago.festago.presentation.mapper.toPresentation -import com.festago.festago.presentation.util.MutableSingleLiveData -import com.festago.festago.presentation.util.SingleLiveData import com.festago.festago.repository.TicketRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import java.time.LocalDateTime +import javax.inject.Inject -class TicketListViewModel( +@HiltViewModel +class TicketListViewModel @Inject constructor( private val ticketRepository: TicketRepository, private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _uiState = MutableLiveData(TicketListUiState.Loading) - val uiState: LiveData = _uiState + private val _uiState = MutableStateFlow(TicketListUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() - private val _event = MutableSingleLiveData() - val event: SingleLiveData = _event + private val _event = MutableSharedFlow() + val event: SharedFlow = _event.asSharedFlow() fun loadCurrentTickets() { viewModelScope.launch { ticketRepository.loadCurrentTickets() .onSuccess { tickets -> - _uiState.value = TicketListUiState.Success(tickets.map { it.toUiState() }) + _uiState.value = TicketListUiState.Success( + tickets.map { TicketListItemUiState.of(it, ::showTicketEntry) }, + ) }.onFailure { _uiState.value = TicketListUiState.Error analyticsHelper.logNetworkFailure(KEY_LOAD_TICKETS_LOG, it.message.toString()) @@ -38,23 +42,11 @@ class TicketListViewModel( } fun showTicketEntry(ticketId: Long) { - _event.setValue(TicketListEvent.ShowTicketEntry(ticketId)) + viewModelScope.launch { + _event.emit(TicketListEvent.ShowTicketEntry(ticketId)) + } } - private fun Ticket.toUiState() = TicketListItemUiState( - id = id, - number = number, - entryTime = entryTime, - reserveAt = reserveAt, - condition = condition.toPresentation(), - stage = stage.toPresentation(), - festivalId = festivalTicket.id, - festivalName = festivalTicket.name, - festivalThumbnail = festivalTicket.thumbnail, - canEntry = LocalDateTime.now().isAfter(entryTime), - onTicketEntry = ::showTicketEntry, - ) - companion object { private const val KEY_LOAD_TICKETS_LOG = "load_tickets" } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/reservationcomplete/ReservationCompleteActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/reservationcomplete/ReservationCompleteActivity.kt index 0f3aa4c75..145bbb44d 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/reservationcomplete/ReservationCompleteActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/reservationcomplete/ReservationCompleteActivity.kt @@ -5,9 +5,10 @@ import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.festago.festago.databinding.ActivityReservationCompleteBinding -import com.festago.festago.presentation.model.ReservedTicketUiModel import com.festago.festago.presentation.util.getParcelableExtraCompat +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class ReservationCompleteActivity : AppCompatActivity() { private lateinit var binding: ActivityReservationCompleteBinding @@ -24,17 +25,17 @@ class ReservationCompleteActivity : AppCompatActivity() { } private fun initView() { - val reservationComplete = - intent.getParcelableExtraCompat( + val reservedTicket = + intent.getParcelableExtraCompat( KEY_RESERVATION_COMPLETE, ) - binding.reservationComplete = reservationComplete + binding.reservedTicket = reservedTicket } companion object { private const val KEY_RESERVATION_COMPLETE = "KEY_RESERVATION_COMPLETE" - fun getIntent(context: Context, reservationComplete: ReservedTicketUiModel): Intent { + fun getIntent(context: Context, reservationComplete: ReservedTicketArg): Intent { return Intent(context, ReservationCompleteActivity::class.java).apply { putExtra(KEY_RESERVATION_COMPLETE, reservationComplete) } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservedTicketUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/reservationcomplete/ReservedTicketArg.kt similarity index 68% rename from android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservedTicketUiModel.kt rename to android/festago/app/src/main/java/com/festago/festago/presentation/ui/reservationcomplete/ReservedTicketArg.kt index cab1d21be..bc202b487 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservedTicketUiModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/reservationcomplete/ReservedTicketArg.kt @@ -1,11 +1,11 @@ -package com.festago.festago.presentation.model +package com.festago.festago.presentation.ui.reservationcomplete import android.os.Parcelable import kotlinx.parcelize.Parcelize import java.time.LocalDateTime @Parcelize -data class ReservedTicketUiModel( +data class ReservedTicketArg( val ticketId: Long, val number: Int, val entryTime: LocalDateTime, diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolActivity.kt new file mode 100644 index 000000000..595016076 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolActivity.kt @@ -0,0 +1,87 @@ +package com.festago.festago.presentation.ui.selectschool + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.widget.ArrayAdapter +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.festago.festago.R +import com.festago.festago.databinding.ActivitySelectSchoolBinding +import com.festago.festago.presentation.ui.studentverification.StudentVerificationActivity +import com.festago.festago.presentation.util.repeatOnStarted +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SelectSchoolActivity : AppCompatActivity() { + + private val binding: ActivitySelectSchoolBinding by lazy { + ActivitySelectSchoolBinding.inflate(layoutInflater) + } + + private val vm: SelectSchoolViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initBinding() + initObserve() + initView() + } + + private fun initBinding() { + setContentView(binding.root) + binding.lifecycleOwner = this + binding.vm = vm + } + + private fun initObserve() { + repeatOnStarted(this) { + vm.uiState.collect { uiState -> + handleUiState(uiState) + } + } + repeatOnStarted(this) { + vm.event.collect { event -> + handleEvent(event) + } + } + } + + private fun initView() { + vm.loadSchools() + } + + private fun handleUiState(uiState: SelectSchoolUiState) { + binding.uiState = uiState + when (uiState) { + is SelectSchoolUiState.Loading, is SelectSchoolUiState.Error -> Unit + is SelectSchoolUiState.Success -> handleSuccess(uiState) + } + } + + private fun handleSuccess(uiState: SelectSchoolUiState.Success) { + val adapter = + ArrayAdapter(this, R.layout.item_select_school, uiState.schools.map { it.name }) + binding.actvSelectSchool.setAdapter(adapter) + binding.actvSelectSchool.setOnItemClickListener { _, _, position, _ -> + val selectedSchool = uiState.schools.firstOrNull { + it.name == adapter.getItem(position) + } + selectedSchool?.let { vm.selectSchool(it.id) } + } + } + + private fun handleEvent(event: SelectSchoolEvent) { + when (event) { + is SelectSchoolEvent.ShowStudentVerification -> { + startActivity(StudentVerificationActivity.getIntent(this, event.schoolId)) + } + } + } + + companion object { + fun getIntent(context: Context): Intent { + return Intent(context, SelectSchoolActivity::class.java) + } + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolEvent.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolEvent.kt new file mode 100644 index 000000000..3464fe3e4 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolEvent.kt @@ -0,0 +1,5 @@ +package com.festago.festago.presentation.ui.selectschool + +interface SelectSchoolEvent { + class ShowStudentVerification(val schoolId: Long) : SelectSchoolEvent +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolUiState.kt new file mode 100644 index 000000000..e71f6c70b --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolUiState.kt @@ -0,0 +1,21 @@ +package com.festago.festago.presentation.ui.selectschool + +import com.festago.festago.model.School + +interface SelectSchoolUiState { + object Loading : SelectSchoolUiState + + data class Success( + val schools: List, + val selectedSchoolId: Long? = null + ) : SelectSchoolUiState { + val schoolSelected = selectedSchoolId != null + } + + object Error : SelectSchoolUiState + + val enableNext get() = (this is Success) && schoolSelected + val shouldShowSuccess get() = this is Success + val shouldShowLoading get() = this is Loading + val shouldShowError get() = this is Error +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModel.kt new file mode 100644 index 000000000..5a7bd892e --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModel.kt @@ -0,0 +1,69 @@ +package com.festago.festago.presentation.ui.selectschool + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.festago.festago.analytics.AnalyticsHelper +import com.festago.festago.analytics.logNetworkFailure +import com.festago.festago.repository.SchoolRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SelectSchoolViewModel @Inject constructor( + private val schoolRepository: SchoolRepository, + private val analyticsHelper: AnalyticsHelper +) : ViewModel() { + + private val _uiState = MutableStateFlow(SelectSchoolUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _event = MutableSharedFlow() + val event: SharedFlow = _event.asSharedFlow() + + fun loadSchools() { + viewModelScope.launch { + schoolRepository.loadSchools() + .onSuccess { schools -> + val state = uiState.value + if (state is SelectSchoolUiState.Success) { + _uiState.value = state.copy(schools = schools) + } else { + _uiState.value = SelectSchoolUiState.Success(schools) + } + } + .onFailure { + _uiState.value = SelectSchoolUiState.Error + analyticsHelper.logNetworkFailure(KEY_LOAD_SCHOOLS_LOG, it.message.toString()) + } + } + } + + fun selectSchool(schoolId: Long) { + val state = uiState.value + if (state is SelectSchoolUiState.Success) { + _uiState.value = state.copy(selectedSchoolId = schoolId) + } + } + + fun showStudentVerification() { + viewModelScope.launch { + val state = uiState.value + if (state is SelectSchoolUiState.Success) { + state.selectedSchoolId?.let { schoolId -> + _event.emit(SelectSchoolEvent.ShowStudentVerification(schoolId)) + } + } + } + } + + companion object { + private const val KEY_LOAD_SCHOOLS_LOG = "load_schools" + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInActivity.kt index 974494da1..dcfa93055 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInActivity.kt @@ -6,23 +6,22 @@ import android.os.Bundle import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.ForegroundColorSpan +import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope import com.festago.festago.R import com.festago.festago.databinding.ActivitySignInBinding -import com.festago.festago.presentation.ui.FestagoViewModelFactory import com.festago.festago.presentation.ui.customview.OkDialogFragment import com.festago.festago.presentation.ui.home.HomeActivity -import com.festago.festago.presentation.util.loginWithKakao -import com.kakao.sdk.user.UserApiClient -import kotlinx.coroutines.launch +import com.festago.festago.presentation.util.repeatOnStarted +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class SignInActivity : AppCompatActivity() { private lateinit var binding: ActivitySignInBinding - private val vm: SignInViewModel by viewModels { FestagoViewModelFactory } + private val vm: SignInViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -40,14 +39,26 @@ class SignInActivity : AppCompatActivity() { binding.lifecycleOwner = this binding.vm = vm initComment() + initBackPressed() + } + + private fun initBackPressed() { + val callback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + setResult(RESULT_NOT_SIGN_IN, intent) + finish() + } + } + this.onBackPressedDispatcher.addCallback(this, callback) } private fun initObserve() { - vm.event.observe(this) { event -> - when (event) { - SignInEvent.ShowSignInPage -> handleSignInEvent() - SignInEvent.SignInSuccess -> handleSuccessEvent() - SignInEvent.SignInFailure -> handleFailureEvent() + repeatOnStarted(this) { + vm.event.collect { event -> + when (event) { + is SignInEvent.SignInSuccess -> handleSuccessEvent() + is SignInEvent.SignInFailure -> handleFailureEvent() + } } } } @@ -66,13 +77,6 @@ class SignInActivity : AppCompatActivity() { binding.tvLoginDescription.text = spannableStringBuilder } - private fun handleSignInEvent() { - lifecycleScope.launch { - val oauthToken = UserApiClient.loginWithKakao(this@SignInActivity) - vm.signIn(oauthToken.accessToken) - } - } - private fun handleSuccessEvent() { showHomeWithFinish() } @@ -99,7 +103,7 @@ class SignInActivity : AppCompatActivity() { private const val COLOR_SPAN_END_INDEX = 4 private const val FAILURE_SIGN_IN = "로그인에 실패했습니다." - + const val RESULT_NOT_SIGN_IN = 1 fun getIntent(context: Context): Intent { return Intent(context, SignInActivity::class.java) } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInEvent.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInEvent.kt index 1c9968218..5ca96de93 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInEvent.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInEvent.kt @@ -1,7 +1,6 @@ package com.festago.festago.presentation.ui.signin sealed interface SignInEvent { - object ShowSignInPage : SignInEvent object SignInSuccess : SignInEvent object SignInFailure : SignInEvent } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInViewModel.kt index ce770e3d4..f9054058e 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInViewModel.kt @@ -4,37 +4,34 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.presentation.util.MutableSingleLiveData -import com.festago.festago.presentation.util.SingleLiveData import com.festago.festago.repository.AuthRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch +import javax.inject.Inject -class SignInViewModel( +@HiltViewModel +class SignInViewModel @Inject constructor( private val authRepository: AuthRepository, private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _event = MutableSingleLiveData() - val event: SingleLiveData = _event + private val _event = MutableSharedFlow() + val event: SharedFlow = _event - fun signInKakao() { - _event.setValue(SignInEvent.ShowSignInPage) - } - - fun signIn(token: String) { + fun signIn() { viewModelScope.launch { - authRepository.signIn(SOCIAL_TYPE_KAKAO, token) - .onSuccess { - _event.setValue(SignInEvent.SignInSuccess) - }.onFailure { - _event.setValue(SignInEvent.SignInFailure) - analyticsHelper.logNetworkFailure(KEY_SIGN_IN_LOG, it.message.toString()) - } + authRepository.signIn().onSuccess { + _event.emit(SignInEvent.SignInSuccess) + }.onFailure { + _event.emit(SignInEvent.SignInFailure) + analyticsHelper.logNetworkFailure(KEY_SIGN_IN_LOG, it.message.toString()) + } } } companion object { - private const val SOCIAL_TYPE_KAKAO = "KAKAO" private const val KEY_SIGN_IN_LOG = "KEY_SIGN_IN_LOG" } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/splash/SplashActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/splash/SplashActivity.kt new file mode 100644 index 000000000..71606d264 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/splash/SplashActivity.kt @@ -0,0 +1,92 @@ +package com.festago.festago.presentation.ui.splash + +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.appcompat.app.AlertDialog +import androidx.core.splashscreen.SplashScreen +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import com.festago.festago.R +import com.festago.festago.databinding.ActivitySplashBinding +import com.festago.festago.presentation.ui.home.HomeActivity +import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.install.model.UpdateAvailability +import dagger.hilt.android.AndroidEntryPoint + +@SuppressLint("CustomSplashScreen") +@AndroidEntryPoint +class SplashActivity : ComponentActivity() { + + val binding by lazy { + ActivitySplashBinding.inflate(layoutInflater) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val splashScreen = installSplashScreen() + splashScreen.setKeepOnScreenCondition { true } + checkAppUpdate(splashScreen) + setContentView(binding.root) + } + + private fun checkAppUpdate(splashScreen: SplashScreen) { + val appUpdateManager = AppUpdateManagerFactory.create(this) + appUpdateManager.appUpdateInfo + .addOnSuccessListener { appUpdateInfo -> + handleOnSuccess(appUpdateInfo, splashScreen) + }.addOnFailureListener { + showHome() + } + } + + private fun handleOnSuccess(appUpdateInfo: AppUpdateInfo, splashScreen: SplashScreen) { + if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) { + splashScreen.setKeepOnScreenCondition { false } + requestUpdate() + } else { + showHome() + } + } + + private fun showHome() { + startActivity(HomeActivity.getIntent(this)) + finish() + } + + private fun requestUpdate() { + AlertDialog.Builder(this).apply { + setTitle(getString(R.string.splash_app_update_request_dialog_title)) + setMessage(getString(R.string.splash_app_update_request_dialog_message)) + setNegativeButton(R.string.ok_dialog_btn_cancel) { _, _ -> + handleCancelUpdate() + } + setPositiveButton(R.string.ok_dialog_btn_ok) { _, _ -> + handleOkUpdate() + } + setCancelable(false) + }.show() + } + + private fun handleCancelUpdate() { + Toast.makeText( + this@SplashActivity, + getString(R.string.splash_app_update_denied), + Toast.LENGTH_SHORT, + ).show() + finish() + } + + private fun handleOkUpdate() { + navigateToAppStore() + finish() + } + + private fun navigateToAppStore() { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName"))) + finish() + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationActivity.kt new file mode 100644 index 000000000..349ec05d4 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationActivity.kt @@ -0,0 +1,119 @@ +package com.festago.festago.presentation.ui.studentverification + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.festago.festago.R +import com.festago.festago.databinding.ActivityStudentVerificationBinding +import com.festago.festago.presentation.ui.customview.OkDialogFragment +import com.festago.festago.presentation.ui.home.HomeActivity +import com.festago.festago.presentation.util.repeatOnStarted +import dagger.hilt.android.AndroidEntryPoint +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +@AndroidEntryPoint +class StudentVerificationActivity : AppCompatActivity() { + + private val binding: ActivityStudentVerificationBinding by lazy { + ActivityStudentVerificationBinding.inflate(layoutInflater) + } + + private val vm: StudentVerificationViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initBinding() + initView() + initObserve() + } + + private fun initBinding() { + setContentView(binding.root) + binding.lifecycleOwner = this + binding.vm = vm + } + + private fun initView() { + val schoolId = intent.getLongExtra(KEY_SCHOOL_ID, -1L) + vm.loadSchoolEmail(schoolId) + initRequestVerificationCodeBtn(schoolId) + } + + private fun initRequestVerificationCodeBtn(schoolId: Long) { + binding.btnRequestVerificationCode.setOnClickListener { + vm.sendVerificationCode(binding.tieUserName.text.toString(), schoolId) + } + } + + private fun initObserve() { + repeatOnStarted(this) { + vm.uiState.collect { uiState -> + handleUiState(uiState) + } + } + repeatOnStarted(this) { + vm.event.collect { event -> + handleEvent(event) + } + } + } + + private fun handleUiState(uiState: StudentVerificationUiState) { + binding.uiState = uiState + when (uiState) { + is StudentVerificationUiState.Loading -> Unit + is StudentVerificationUiState.Success -> handleSuccess(uiState) + is StudentVerificationUiState.Error -> Unit + } + } + + private fun handleSuccess(uiState: StudentVerificationUiState.Success) { + binding.tvSchoolEmail.text = + getString(R.string.student_verification_tv_email_format, uiState.schoolEmail) + + val format = + DateTimeFormatter.ofPattern(getString(R.string.student_verification_tv_timer_format)) + binding.tvTimerVerificationCode.text = LocalTime.ofSecondOfDay(uiState.remainTime.toLong()) + .format(format) + + binding.btnVerificationConfirm.isEnabled = uiState.isValidateCode + } + + private fun handleEvent(event: StudentVerificationEvent) { + when (event) { + is StudentVerificationEvent.VerificationSuccess -> handleVerificationSuccess() + is StudentVerificationEvent.VerificationFailure -> showDialog(FAILURE_VERIFICATION) + is StudentVerificationEvent.VerificationTimeOut -> showDialog(TIME_OUT_VERIFICATION) + is StudentVerificationEvent.SendingEmailFailure -> showDialog(FAILURE_SENDING_EMAIL) + } + } + + private fun handleVerificationSuccess() { + val intent = HomeActivity.getIntent(this).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + finishAffinity() + startActivity(intent) + } + + private fun showDialog(message: String) { + val dialog = OkDialogFragment.newInstance(message) + dialog.show(supportFragmentManager, OkDialogFragment::class.java.name) + } + + companion object { + private const val KEY_SCHOOL_ID = "KEY_SCHOOL_ID" + private const val FAILURE_VERIFICATION = "인증에 실패하였습니다" + private const val TIME_OUT_VERIFICATION = "인증 시간이 만료되었습니다" + private const val FAILURE_SENDING_EMAIL = "이메일 입력을 확인해주세요" + + fun getIntent(context: Context, schoolId: Long): Intent { + return Intent(context, StudentVerificationActivity::class.java).apply { + putExtra(KEY_SCHOOL_ID, schoolId) + } + } + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationEvent.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationEvent.kt new file mode 100644 index 000000000..07fccff05 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationEvent.kt @@ -0,0 +1,8 @@ +package com.festago.festago.presentation.ui.studentverification + +sealed interface StudentVerificationEvent { + object VerificationTimeOut : StudentVerificationEvent + object VerificationFailure : StudentVerificationEvent + object VerificationSuccess : StudentVerificationEvent + object SendingEmailFailure : StudentVerificationEvent +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationUiState.kt new file mode 100644 index 000000000..ca650fa02 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationUiState.kt @@ -0,0 +1,17 @@ +package com.festago.festago.presentation.ui.studentverification + +sealed interface StudentVerificationUiState { + object Loading : StudentVerificationUiState + + data class Success( + val schoolEmail: String, + val remainTime: Int, + val isValidateCode: Boolean = false, + ) : StudentVerificationUiState + + object Error : StudentVerificationUiState + + val shouldShowSuccess get() = this is Success + val shouldShowLoading get() = this is Loading + val shouldShowError get() = this is Error +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModel.kt new file mode 100644 index 000000000..39f0ca38b --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModel.kt @@ -0,0 +1,153 @@ +package com.festago.festago.presentation.ui.studentverification + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.festago.festago.analytics.AnalyticsHelper +import com.festago.festago.analytics.logNetworkFailure +import com.festago.festago.model.StudentVerificationCode +import com.festago.festago.model.timer.Timer +import com.festago.festago.model.timer.TimerListener +import com.festago.festago.repository.SchoolRepository +import com.festago.festago.repository.StudentVerificationRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class StudentVerificationViewModel @Inject constructor( + private val schoolRepository: SchoolRepository, + private val studentVerificationRepository: StudentVerificationRepository, + private val analyticsHelper: AnalyticsHelper, +) : ViewModel() { + + private val _uiState = + MutableStateFlow(StudentVerificationUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _event = MutableSharedFlow() + val event: SharedFlow = _event.asSharedFlow() + + val verificationCode = MutableStateFlow("") + + private val timer: Timer = Timer() + + init { + initObserveVerificationCode() + } + + private fun initObserveVerificationCode() { + viewModelScope.launch { + verificationCode.collect { code -> + handleOnUiStateSuccess { + validateCode(code, it) + } + } + } + } + + private inline fun handleOnUiStateSuccess(action: (state: StudentVerificationUiState.Success) -> Unit) { + val state = uiState.value as? StudentVerificationUiState.Success ?: return + action(state) + } + + private fun validateCode(verificationCode: String, state: StudentVerificationUiState.Success) { + runCatching { + StudentVerificationCode(verificationCode) + }.onSuccess { + _uiState.value = state.copy(isValidateCode = true) + }.onFailure { + _uiState.value = state.copy(isValidateCode = false) + } + } + + fun loadSchoolEmail(schoolId: Long) { + if (uiState.value is StudentVerificationUiState.Success) return + + viewModelScope.launch { + schoolRepository.loadSchoolEmail(schoolId) + .onSuccess { email -> + _uiState.value = StudentVerificationUiState.Success( + schoolEmail = email, + remainTime = MIN_REMAIN_TIME, + ) + } + .onFailure { + _uiState.value = StudentVerificationUiState.Error + analyticsHelper.logNetworkFailure( + KEY_LOAD_SCHOOL_EMAIL_LOG, + it.message.toString(), + ) + } + } + } + + fun sendVerificationCode(userName: String, schoolId: Long) { + viewModelScope.launch { + studentVerificationRepository.sendVerificationCode(userName, schoolId) + .onSuccess { + handleOnUiStateSuccess { state -> + _uiState.value = state.copy(remainTime = TIMER_PERIOD) + } + setTimer() + } + .onFailure { + _event.emit(StudentVerificationEvent.SendingEmailFailure) + analyticsHelper.logNetworkFailure( + KEY_SEND_VERIFICATION_CODE_LOG, + it.message.toString(), + ) + } + } + } + + private suspend fun setTimer() { + timer.timerListener = createTimerListener() + timer.start(TIMER_PERIOD) + } + + private fun createTimerListener(): TimerListener = object : TimerListener { + override fun onTick(current: Int) { + handleOnUiStateSuccess { state -> + _uiState.value = state.copy(remainTime = current) + } + } + + override fun onFinish() { + handleOnUiStateSuccess { state -> + _uiState.value = state.copy(remainTime = MIN_REMAIN_TIME) + } + } + } + + fun confirmVerificationCode() { + viewModelScope.launch { + val state = uiState.value as? StudentVerificationUiState.Success ?: return@launch + + if (state.remainTime == MIN_REMAIN_TIME) { + _event.emit(StudentVerificationEvent.VerificationTimeOut) + return@launch + } + + studentVerificationRepository.requestVerificationCodeConfirm( + StudentVerificationCode(verificationCode.value), + ).onSuccess { + _event.emit(StudentVerificationEvent.VerificationSuccess) + }.onFailure { + _event.emit(StudentVerificationEvent.VerificationFailure) + } + } + } + + companion object { + private const val KEY_LOAD_SCHOOL_EMAIL_LOG = "load_school_email" + private const val KEY_SEND_VERIFICATION_CODE_LOG = "send_verification_code" + private const val MIN_REMAIN_TIME = 0 + private const val TIMER_PERIOD = 300 + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryActivity.kt index 6d7a3184f..74094c06b 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryActivity.kt @@ -3,21 +3,26 @@ package com.festago.festago.presentation.ui.ticketentry import android.content.Context import android.content.Intent import android.os.Bundle +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.content.res.ResourcesCompat import com.festago.festago.databinding.ActivityTicketEntryBinding -import com.festago.festago.presentation.mapper.toPresentation -import com.festago.festago.presentation.ui.FestagoViewModelFactory +import com.festago.festago.presentation.fcm.TicketEntryService +import com.festago.festago.presentation.util.repeatOnStarted import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.BarcodeEncoder +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +@AndroidEntryPoint class TicketEntryActivity : AppCompatActivity() { private lateinit var binding: ActivityTicketEntryBinding - private val vm: TicketEntryViewModel by viewModels { FestagoViewModelFactory } + private val vm: TicketEntryViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -49,24 +54,35 @@ class TicketEntryActivity : AppCompatActivity() { } private fun initObserve() { - vm.uiState.observe(this) { uiState -> - binding.uiState = uiState - when (uiState) { - is TicketEntryUiState.Loading, is TicketEntryUiState.Error -> Unit - is TicketEntryUiState.Success -> { - handleSuccess(uiState) + repeatOnStarted(this) { + vm.uiState.collectLatest { uiState -> + binding.uiState = uiState + when (uiState) { + is TicketEntryUiState.Loading, is TicketEntryUiState.Error -> Unit + is TicketEntryUiState.Success -> { + handleSuccess(uiState) + } } } } + repeatOnStarted(this) { + TicketEntryService.ticketStateChangeEvent.first { + Toast.makeText(this, "티켓이 스캔되었습니다.", Toast.LENGTH_SHORT).show() + setResult(RESULT_OK, intent) + finish() + true + } + } } private fun initView(currentTicketId: Long) { vm.loadTicket(currentTicketId) + vm.loadTicketCode(currentTicketId) } private fun handleSuccess(uiState: TicketEntryUiState.Success) { binding.successState = uiState - val ticketCode = uiState.ticketCode.toPresentation() + val ticketCode = uiState.ticketCode val bitmap = BarcodeEncoder().encodeBitmap( ticketCode.code, diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryUiState.kt index 55762d2a2..1ab505157 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryUiState.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryUiState.kt @@ -1,17 +1,17 @@ package com.festago.festago.presentation.ui.ticketentry import com.festago.festago.R +import com.festago.festago.model.Ticket import com.festago.festago.model.TicketCode -import com.festago.festago.presentation.model.TicketConditionUiModel.AFTER_ENTRY -import com.festago.festago.presentation.model.TicketConditionUiModel.AWAY -import com.festago.festago.presentation.model.TicketConditionUiModel.BEFORE_ENTRY -import com.festago.festago.presentation.model.TicketUiModel +import com.festago.festago.model.TicketCondition.AFTER_ENTRY +import com.festago.festago.model.TicketCondition.AWAY +import com.festago.festago.model.TicketCondition.BEFORE_ENTRY -interface TicketEntryUiState { +sealed interface TicketEntryUiState { object Loading : TicketEntryUiState data class Success( - val ticket: TicketUiModel, + val ticket: Ticket, val ticketCode: TicketCode, val remainTime: Int, ) : TicketEntryUiState { diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModel.kt index 043ce7b3a..ae14081a2 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModel.kt @@ -1,101 +1,107 @@ package com.festago.festago.presentation.ui.ticketentry -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure +import com.festago.festago.model.Ticket import com.festago.festago.model.TicketCode import com.festago.festago.model.timer.Timer import com.festago.festago.model.timer.TimerListener -import com.festago.festago.presentation.mapper.toPresentation import com.festago.festago.repository.TicketRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch +import javax.inject.Inject -class TicketEntryViewModel( +@HiltViewModel +class TicketEntryViewModel @Inject constructor( private val ticketRepository: TicketRepository, private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _uiState = MutableLiveData() - val uiState: LiveData = _uiState + private val ticketFlow = MutableSharedFlow>() + + private val ticketCodeFlow = MutableSharedFlow>() + + private val _uiState: MutableStateFlow = + MutableStateFlow(TicketEntryUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() private val timer: Timer = Timer() - fun loadTicketCode(ticketId: Long) { + init { viewModelScope.launch { - ticketRepository.loadTicketCode(ticketId) - .onSuccess { - val state = uiState.value - if (state is TicketEntryUiState.Success) { - _uiState.value = state.copy(ticketCode = it, remainTime = it.period) - setTimer(ticketId, it) - } - }.onFailure { - _uiState.value = TicketEntryUiState.Error - analyticsHelper.logNetworkFailure( - key = KEY_LOAD_CODE_LOG, - value = it.message.toString(), + combine(ticketFlow, ticketCodeFlow) { ticketResult, ticketCodeResult -> + runCatching { + val ticket = ticketResult.getOrThrowWithLog() + val ticketCode = ticketCodeResult.getOrThrowWithLog() + + setTimer(ticket.id, ticketCode) + + TicketEntryUiState.Success( + ticket = ticket, + ticketCode = ticketCode, + remainTime = ticketCode.period, ) - } + }.getOrElse { TicketEntryUiState.Error } + }.collectLatest { _uiState.value = it } + } + } + + fun loadTicketCode(ticketId: Long) { + viewModelScope.launch { + ticketCodeFlow.emit(ticketRepository.loadTicketCode(ticketId)) } } fun loadTicket(ticketId: Long) { viewModelScope.launch { - _uiState.value = TicketEntryUiState.Loading - ticketRepository.loadTicket(ticketId) - .onSuccess { ticket -> - ticketRepository.loadTicketCode(ticketId) - .onSuccess { ticketCode -> - _uiState.value = TicketEntryUiState.Success( - ticket = ticket.toPresentation(), - ticketCode = ticketCode, - remainTime = ticketCode.period, - ) - setTimer(ticketId, ticketCode) - }.onFailure { - _uiState.value = TicketEntryUiState.Error - analyticsHelper.logNetworkFailure( - key = KEY_LOAD_CODE_LOG, - value = it.message.toString(), - ) - } - }.onFailure { - _uiState.value = TicketEntryUiState.Error - analyticsHelper.logNetworkFailure( - key = KEY_LOAD_Ticket_LOG, - value = it.message.toString(), - ) - } + ticketFlow.emit(ticketRepository.loadTicket(ticketId)) } } - private suspend fun setTimer(ticketId: Long, ticketCode: TicketCode) { - timer.timerListener = createTimerListener( - ticketId = ticketId, - period = ticketCode.period, - ) - timer.start(ticketCode.period) + private fun setTimer(ticketId: Long, ticketCode: TicketCode) { + viewModelScope.launch { + timer.timerListener = createTimerListener(ticketId) + timer.start(ticketCode.period) + } } - private fun createTimerListener(ticketId: Long, period: Int): TimerListener = - object : TimerListener { - override fun onTick(current: Int) { - val state = uiState.value - if (state is TicketEntryUiState.Success) { - _uiState.value = state.copy(remainTime = current) - } + private fun createTimerListener(ticketId: Long): TimerListener = object : TimerListener { + override fun onTick(current: Int) { + val state = uiState.value + if (state is TicketEntryUiState.Success) { + _uiState.value = state.copy(remainTime = current) } + } - override fun onFinish() { - viewModelScope.launch { - timer.start(period) - loadTicketCode(ticketId) - } - } + override fun onFinish() { + loadTicketCode(ticketId) } + } + + private fun Result.getOrThrowWithLog(): Ticket = getOrElse { throwable -> + analyticsHelper.logNetworkFailure( + key = KEY_LOAD_Ticket_LOG, + value = throwable.message.toString(), + ) + + throw throwable + } + + private fun Result.getOrThrowWithLog(): TicketCode = getOrElse { throwable -> + analyticsHelper.logNetworkFailure( + key = KEY_LOAD_CODE_LOG, + value = throwable.message.toString(), + ) + throw throwable + } companion object { private const val KEY_LOAD_Ticket_LOG = "load_ticket" diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryActivity.kt index ed95ff13a..46019fce1 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryActivity.kt @@ -6,12 +6,14 @@ import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.festago.festago.databinding.ActivityTicketHistoryBinding -import com.festago.festago.presentation.ui.FestagoViewModelFactory +import com.festago.festago.presentation.util.repeatOnStarted +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class TicketHistoryActivity : AppCompatActivity() { private lateinit var binding: ActivityTicketHistoryBinding - private val vm: TicketHistoryViewModel by viewModels { FestagoViewModelFactory } + private val vm: TicketHistoryViewModel by viewModels() private var adapter: TicketHistoryAdapter = TicketHistoryAdapter() @@ -29,17 +31,19 @@ class TicketHistoryActivity : AppCompatActivity() { } private fun initObserve() { - vm.uiState.observe(this) { uiState -> - when (uiState) { - is TicketHistoryUiState.Loading, - is TicketHistoryUiState.Error, - -> Unit - - is TicketHistoryUiState.Success -> { - adapter.submitList(uiState.tickets) + repeatOnStarted(this) { + vm.uiState.collect { uiState -> + when (uiState) { + is TicketHistoryUiState.Loading, + is TicketHistoryUiState.Error, + -> Unit + + is TicketHistoryUiState.Success -> { + adapter.submitList(uiState.tickets) + } } + binding.uiState = uiState } - binding.uiState = uiState } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryAdapter.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryAdapter.kt index fcdcfac66..0e80827d7 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryAdapter.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryAdapter.kt @@ -3,9 +3,8 @@ package com.festago.festago.presentation.ui.tickethistory import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import com.festago.festago.presentation.model.TicketUiModel -class TicketHistoryAdapter : ListAdapter(ticketDiffUtil) { +class TicketHistoryAdapter : ListAdapter(ticketDiffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TicketHistoryViewHolder { return TicketHistoryViewHolder.from(parent) @@ -16,11 +15,11 @@ class TicketHistoryAdapter : ListAdapter } companion object { - private val ticketDiffUtil = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: TicketUiModel, newItem: TicketUiModel) = + private val ticketDiffUtil = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: TicketHistoryItemUiState, newItem: TicketHistoryItemUiState) = oldItem.id == newItem.id - override fun areContentsTheSame(oldItem: TicketUiModel, newItem: TicketUiModel) = + override fun areContentsTheSame(oldItem: TicketHistoryItemUiState, newItem: TicketHistoryItemUiState) = oldItem == newItem } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryItemUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryItemUiState.kt new file mode 100644 index 000000000..1cd2dc81a --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryItemUiState.kt @@ -0,0 +1,29 @@ +package com.festago.festago.presentation.ui.tickethistory + +import com.festago.festago.model.Stage +import com.festago.festago.model.Ticket +import java.time.LocalDateTime + +data class TicketHistoryItemUiState( + val id: Long, + val number: Int, + val entryTime: LocalDateTime, + val reserveAt: LocalDateTime, + val stage: Stage, + val festivalId: Int, + val festivalName: String, + val festivalThumbnail: String, +) { + companion object { + fun from(ticket: Ticket) = TicketHistoryItemUiState( + id = ticket.id, + number = ticket.number, + entryTime = ticket.entryTime, + reserveAt = ticket.reserveAt, + stage = ticket.stage, + festivalId = ticket.festivalTicket.id, + festivalName = ticket.festivalTicket.name, + festivalThumbnail = ticket.festivalTicket.thumbnail, + ) + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryUiState.kt index 79908db9b..e8c0d8666 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryUiState.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryUiState.kt @@ -1,11 +1,9 @@ package com.festago.festago.presentation.ui.tickethistory -import com.festago.festago.presentation.model.TicketUiModel - sealed interface TicketHistoryUiState { object Loading : TicketHistoryUiState - data class Success(val tickets: List) : TicketHistoryUiState { + data class Success(val tickets: List) : TicketHistoryUiState { val hasTicket get() = tickets.isNotEmpty() val hasNotTicket get() = tickets.isEmpty() } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewHolder.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewHolder.kt index 1e4cff371..0e4c66658 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewHolder.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewHolder.kt @@ -4,13 +4,12 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.festago.festago.databinding.ItemTicketHistoryBinding -import com.festago.festago.presentation.model.TicketUiModel class TicketHistoryViewHolder( val binding: ItemTicketHistoryBinding, ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: TicketUiModel) { + fun bind(item: TicketHistoryItemUiState) { binding.ticket = item } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModel.kt index f30406ab0..f1cf12e61 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModel.kt @@ -1,31 +1,35 @@ package com.festago.festago.presentation.ui.tickethistory -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.presentation.mapper.toPresentation import com.festago.festago.repository.TicketRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import javax.inject.Inject -class TicketHistoryViewModel( +@HiltViewModel +class TicketHistoryViewModel @Inject constructor( private val ticketRepository: TicketRepository, private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _uiState = MutableLiveData() - val uiState: LiveData = _uiState + private val _uiState = MutableStateFlow(TicketHistoryUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() - fun loadTicketHistories(size: Int = 100, refresh: Boolean = false) { + fun loadTicketHistories(size: Int = SIZE_TICKET_HISTORY, refresh: Boolean = false) { if (!refresh && uiState.value is TicketHistoryUiState.Success) return viewModelScope.launch { - _uiState.value = TicketHistoryUiState.Loading ticketRepository.loadHistoryTickets(size) .onSuccess { tickets -> - _uiState.value = TicketHistoryUiState.Success(tickets.toPresentation()) + _uiState.value = TicketHistoryUiState.Success( + tickets.map { TicketHistoryItemUiState.from(it) }, + ) }.onFailure { _uiState.value = TicketHistoryUiState.Error analyticsHelper.logNetworkFailure( @@ -38,5 +42,6 @@ class TicketHistoryViewModel( companion object { private const val KEY_LOAD_TICKET_HISTORIES_LOG = "ticket_histories" + private const val SIZE_TICKET_HISTORY = 100 } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveActivity.kt index dad5aade1..529e364dd 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveActivity.kt @@ -8,12 +8,11 @@ import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.ConcatAdapter import com.festago.festago.R import com.festago.festago.databinding.ActivityTicketReserveBinding +import com.festago.festago.model.ReservationTicket import com.festago.festago.model.ReservedTicket -import com.festago.festago.presentation.mapper.toPresentation -import com.festago.festago.presentation.model.ReservationTicketUiModel -import com.festago.festago.presentation.ui.FestagoViewModelFactory import com.festago.festago.presentation.ui.customview.OkDialogFragment import com.festago.festago.presentation.ui.reservationcomplete.ReservationCompleteActivity +import com.festago.festago.presentation.ui.reservationcomplete.ReservedTicketArg import com.festago.festago.presentation.ui.signin.SignInActivity import com.festago.festago.presentation.ui.ticketreserve.TicketReserveEvent.ReserveTicketFailed import com.festago.festago.presentation.ui.ticketreserve.TicketReserveEvent.ReserveTicketSuccess @@ -21,15 +20,20 @@ import com.festago.festago.presentation.ui.ticketreserve.TicketReserveEvent.Show import com.festago.festago.presentation.ui.ticketreserve.TicketReserveEvent.ShowTicketTypes import com.festago.festago.presentation.ui.ticketreserve.adapter.TicketReserveAdapter import com.festago.festago.presentation.ui.ticketreserve.adapter.TicketReserveHeaderAdapter +import com.festago.festago.presentation.ui.ticketreserve.bottomsheet.BottomSheetReservationTicketArg +import com.festago.festago.presentation.ui.ticketreserve.bottomsheet.BottomSheetTicketTypeArg import com.festago.festago.presentation.ui.ticketreserve.bottomsheet.TicketReserveBottomSheetFragment +import com.festago.festago.presentation.util.repeatOnStarted +import dagger.hilt.android.AndroidEntryPoint import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.Locale +@AndroidEntryPoint class TicketReserveActivity : AppCompatActivity() { private lateinit var binding: ActivityTicketReserveBinding - private val vm: TicketReserveViewModel by viewModels { FestagoViewModelFactory } + private val vm: TicketReserveViewModel by viewModels() private val contentsAdapter by lazy { TicketReserveAdapter() } private val headerAdapter by lazy { TicketReserveHeaderAdapter() } @@ -51,19 +55,23 @@ class TicketReserveActivity : AppCompatActivity() { } private fun initObserve() { - vm.uiState.observe(this) { uiState -> - updateUi(uiState) - binding.uiState = uiState + repeatOnStarted(this) { + vm.uiState.collect { uiState -> + updateUi(uiState) + binding.uiState = uiState + } } - vm.event.observe(this) { event -> - handleEvent(event) + repeatOnStarted(this) { + vm.event.collect { event -> + handleEvent(event) + } } } private fun handleEvent(event: TicketReserveEvent) = when (event) { is ShowTicketTypes -> handleShowTicketTypes( stageStartTime = event.stageStartTime, - tickets = event.tickets, + reservationTickets = event.tickets, ) is ReserveTicketSuccess -> handleReserveTicketSuccess(event.reservedTicket) @@ -73,7 +81,7 @@ class TicketReserveActivity : AppCompatActivity() { private fun handleShowTicketTypes( stageStartTime: LocalDateTime, - tickets: List, + reservationTickets: List, ) { TicketReserveBottomSheetFragment.newInstance( stageStartTime.format( @@ -82,12 +90,24 @@ class TicketReserveActivity : AppCompatActivity() { Locale.KOREA, ), ), - tickets, + reservationTickets.map { + BottomSheetReservationTicketArg( + id = it.id, + remainAmount = it.remainAmount, + ticketType = BottomSheetTicketTypeArg.from(it.ticketType), + totalAmount = it.totalAmount, + ) + }, ).show(supportFragmentManager, TicketReserveBottomSheetFragment::class.java.name) } private fun handleReserveTicketSuccess(reservedTicket: ReservedTicket) { - val intent = ReservationCompleteActivity.getIntent(this, reservedTicket.toPresentation()) + val reservedTicketArg = ReservedTicketArg( + ticketId = reservedTicket.id, + number = reservedTicket.number, + entryTime = reservedTicket.entryTime, + ) + val intent = ReservationCompleteActivity.getIntent(this, reservedTicketArg) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK startActivity(intent) finish() @@ -112,13 +132,14 @@ class TicketReserveActivity : AppCompatActivity() { festivalId = intent.getLongExtra(KEY_FESTIVAL_ID, -1), refresh = true, ) + binding.srlTicketReserve.isRefreshing = false } } private fun updateUi(uiState: TicketReserveUiState) = when (uiState) { is TicketReserveUiState.Loading, is TicketReserveUiState.Error, - -> binding.srlTicketReserve.isRefreshing = false + -> Unit is TicketReserveUiState.Success -> updateSuccess(uiState) } @@ -126,7 +147,6 @@ class TicketReserveActivity : AppCompatActivity() { private fun updateSuccess(successState: TicketReserveUiState.Success) { headerAdapter.submitList(listOf(successState.festival)) contentsAdapter.submitList(successState.stages) - binding.srlTicketReserve.isRefreshing = false } companion object { diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveEvent.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveEvent.kt index 04ef9faf5..5cc0f3cc7 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveEvent.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveEvent.kt @@ -1,13 +1,13 @@ package com.festago.festago.presentation.ui.ticketreserve +import com.festago.festago.model.ReservationTicket import com.festago.festago.model.ReservedTicket -import com.festago.festago.presentation.model.ReservationTicketUiModel import java.time.LocalDateTime sealed interface TicketReserveEvent { class ShowTicketTypes( val stageStartTime: LocalDateTime, - val tickets: List, + val tickets: List, ) : TicketReserveEvent class ReserveTicketSuccess(val reservedTicket: ReservedTicket) : TicketReserveEvent diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveItemUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveItemUiState.kt index f3da26b8e..d199c7a15 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveItemUiState.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveItemUiState.kt @@ -1,18 +1,15 @@ package com.festago.festago.presentation.ui.ticketreserve -import android.os.Parcelable -import com.festago.festago.presentation.model.ReservationTicketUiModel -import kotlinx.parcelize.Parcelize +import com.festago.festago.model.ReservationTicket import java.time.LocalDateTime -@Parcelize data class TicketReserveItemUiState( val id: Int, val lineUp: String, val startTime: LocalDateTime, val ticketOpenTime: LocalDateTime, - val reservationTickets: List, + val reservationTickets: List, val canReserve: Boolean, val isSigned: Boolean, val onShowStageTickets: (stageId: Int, stageStartTime: LocalDateTime) -> Unit, -) : Parcelable +) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModel.kt index 110270e00..05c056faf 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModel.kt @@ -1,51 +1,53 @@ package com.festago.festago.presentation.ui.ticketreserve -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure import com.festago.festago.model.ReservationStage -import com.festago.festago.presentation.mapper.toPresentation -import com.festago.festago.presentation.util.MutableSingleLiveData -import com.festago.festago.presentation.util.SingleLiveData import com.festago.festago.repository.AuthRepository import com.festago.festago.repository.FestivalRepository import com.festago.festago.repository.ReservationTicketRepository import com.festago.festago.repository.TicketRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.time.LocalDateTime +import javax.inject.Inject -class TicketReserveViewModel( +@HiltViewModel +class TicketReserveViewModel @Inject constructor( private val reservationTicketRepository: ReservationTicketRepository, private val festivalRepository: FestivalRepository, private val ticketRepository: TicketRepository, private val authRepository: AuthRepository, private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _uiState = MutableLiveData(TicketReserveUiState.Loading) - val uiState: LiveData = _uiState + private val _uiState = MutableStateFlow(TicketReserveUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() - private val _event = MutableSingleLiveData() - val event: SingleLiveData = _event + private val _event = MutableSharedFlow() + val event: SharedFlow = _event.asSharedFlow() fun loadReservation(festivalId: Long = 0, refresh: Boolean = false) { if (!refresh && uiState.value is TicketReserveUiState.Success) return viewModelScope.launch { festivalRepository.loadFestivalDetail(festivalId) .onSuccess { - _uiState.setValue( - TicketReserveUiState.Success( - festival = ReservationFestivalUiState( - id = it.id, - name = it.name, - thumbnail = it.thumbnail, - endDate = it.endDate, - startDate = it.startDate, - ), - stages = it.reservationStages.toTicketReserveItems(), + _uiState.value = TicketReserveUiState.Success( + festival = ReservationFestivalUiState( + id = it.id, + name = it.name, + thumbnail = it.thumbnail, + endDate = it.endDate, + startDate = it.startDate, ), + stages = it.reservationStages.toTicketReserveItems(), ) }.onFailure { _uiState.value = TicketReserveUiState.Error @@ -61,18 +63,18 @@ class TicketReserveViewModel( viewModelScope.launch { if (authRepository.isSigned) { reservationTicketRepository.loadTicketTypes(stageId) - .onSuccess { tickets -> - _event.setValue( + .onSuccess { reservationTickets -> + _event.emit( TicketReserveEvent.ShowTicketTypes( stageStartTime, - tickets.map { it.toPresentation() }, + reservationTickets.sortedByTicketTypes(), ), ) }.onFailure { - _uiState.setValue(TicketReserveUiState.Error) + _uiState.value = TicketReserveUiState.Error } } else { - _event.setValue(TicketReserveEvent.ShowSignIn) + _event.emit(TicketReserveEvent.ShowSignIn) } } } @@ -81,9 +83,9 @@ class TicketReserveViewModel( viewModelScope.launch { ticketRepository.reserveTicket(ticketId) .onSuccess { - _event.setValue(TicketReserveEvent.ReserveTicketSuccess(it)) + _event.emit(TicketReserveEvent.ReserveTicketSuccess(it)) }.onFailure { - _event.setValue(TicketReserveEvent.ReserveTicketFailed) + _event.emit(TicketReserveEvent.ReserveTicketFailed) } } } @@ -93,7 +95,7 @@ class TicketReserveViewModel( lineUp = lineUp, startTime = startTime, ticketOpenTime = ticketOpenTime, - reservationTickets = reservationTickets.map { it.toPresentation() }, + reservationTickets = reservationTickets.sortedByTicketTypes(), canReserve = LocalDateTime.now().isAfter(ticketOpenTime), isSigned = authRepository.isSigned, onShowStageTickets = ::showTicketTypes, diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservationTicketUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/BottomSheetReservationTicketArg.kt similarity index 50% rename from android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservationTicketUiModel.kt rename to android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/BottomSheetReservationTicketArg.kt index add00010f..b04969ca7 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservationTicketUiModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/BottomSheetReservationTicketArg.kt @@ -1,12 +1,12 @@ -package com.festago.festago.presentation.model +package com.festago.festago.presentation.ui.ticketreserve.bottomsheet import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class ReservationTicketUiModel( +data class BottomSheetReservationTicketArg( val id: Int, val remainAmount: Int, - val ticketType: String, + val ticketType: BottomSheetTicketTypeArg, val totalAmount: Int, ) : Parcelable diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/BottomSheetTicketTypeArg.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/BottomSheetTicketTypeArg.kt new file mode 100644 index 000000000..b64ae3412 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/BottomSheetTicketTypeArg.kt @@ -0,0 +1,19 @@ +package com.festago.festago.presentation.ui.ticketreserve.bottomsheet + +import android.os.Parcelable +import com.festago.festago.model.TicketType +import kotlinx.parcelize.Parcelize + +@Parcelize +enum class BottomSheetTicketTypeArg : Parcelable { + STUDENT, VISITOR, OTHER + ; + + companion object { + fun from(ticketType: TicketType): BottomSheetTicketTypeArg = when (ticketType) { + TicketType.STUDENT -> STUDENT + TicketType.VISITOR -> VISITOR + TicketType.OTHER -> OTHER + } + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomItem.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomItem.kt index b52495f5b..745447aa5 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomItem.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomItem.kt @@ -1,8 +1,6 @@ package com.festago.festago.presentation.ui.ticketreserve.bottomsheet -import com.festago.festago.presentation.model.ReservationTicketUiModel - data class TicketReserveBottomItem( - val ticket: ReservationTicketUiModel, + val ticket: BottomSheetReservationTicketArg, val isSelected: Boolean = false, ) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomSheetFragment.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomSheetFragment.kt index 810924c90..d665e2b94 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomSheetFragment.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomSheetFragment.kt @@ -6,11 +6,12 @@ import android.view.View import android.view.ViewGroup import androidx.lifecycle.ViewModelProvider import com.festago.festago.databinding.FragmentTicketReserveBottomSheetBinding -import com.festago.festago.presentation.model.ReservationTicketUiModel import com.festago.festago.presentation.ui.ticketreserve.TicketReserveViewModel import com.festago.festago.presentation.util.getParcelableArrayListCompat import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { private var _binding: FragmentTicketReserveBottomSheetBinding? = null @@ -20,6 +21,7 @@ class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { private val ticketTypeAdapter = TicketReserveBottomSheetAdapter { ticketId -> binding.selectedTicketTypeId = ticketId + binding.btnReserveTicket.isEnabled = true } override fun onCreate(savedInstanceState: Bundle?) { @@ -44,7 +46,7 @@ class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { getString(KEY_STAGE_START_TIME)?.let { startTime -> binding.stageStartTime = startTime } - getParcelableArrayListCompat(KEY_ITEMS)?.let { + getParcelableArrayListCompat(KEY_ITEMS)?.let { ticketTypeAdapter.submitList(it.map(::TicketReserveBottomItem)) } } @@ -56,6 +58,7 @@ class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { binding.rvTicketTypes.adapter = ticketTypeAdapter val onReserve: (Int) -> Unit = { id -> vm.reserveTicket(id) } binding.onReserve = onReserve + binding.btnReserveTicket.isEnabled = false } override fun onDestroyView() { @@ -69,7 +72,7 @@ class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { fun newInstance( stageStartTime: String, - items: List, + items: List, ) = TicketReserveBottomSheetFragment().apply { arguments = Bundle().apply { putString(KEY_STAGE_START_TIME, stageStartTime) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomViewHolder.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomViewHolder.kt index 8d359bee4..a4995ec59 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomViewHolder.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomViewHolder.kt @@ -3,6 +3,7 @@ package com.festago.festago.presentation.ui.ticketreserve.bottomsheet import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import com.festago.festago.R import com.festago.festago.databinding.ItemTicketReserveBottomSheetBinding class TicketReserveBottomViewHolder( @@ -17,6 +18,16 @@ class TicketReserveBottomViewHolder( fun bind(item: TicketReserveBottomItem) { binding.item = item binding.clLayout.isSelected = item.isSelected + binding.tvTicketType.text = item.ticket.ticketType.getString() + } + + private fun BottomSheetTicketTypeArg.getString(): String { + val resId: Int = when (this) { + BottomSheetTicketTypeArg.STUDENT -> R.string.all_ticket_type_student + BottomSheetTicketTypeArg.VISITOR -> R.string.all_ticket_type_visitor + BottomSheetTicketTypeArg.OTHER -> R.string.all_ticket_type_other + } + return binding.root.context.getString(resId) } companion object { diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/viewHolder/TicketReserveViewHolder.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/viewHolder/TicketReserveViewHolder.kt index 10f252fc5..09fe4df95 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/viewHolder/TicketReserveViewHolder.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/viewHolder/TicketReserveViewHolder.kt @@ -5,6 +5,7 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.festago.festago.R import com.festago.festago.databinding.ItemTicketReserveBinding +import com.festago.festago.model.TicketType import com.festago.festago.presentation.ui.ticketreserve.TicketReserveItemUiState import java.time.format.DateTimeFormatter @@ -39,13 +40,22 @@ class TicketReserveViewHolder( item.reservationTickets.joinToString(binding.root.context.getString(R.string.ticket_reserve_tv_ticket_count_separator)) { binding.root.context.getString( R.string.ticket_reserve_tv_ticket_count, - it.ticketType, + it.ticketType.getString(), it.remainAmount.toString(), it.totalAmount.toString(), ) } } + private fun TicketType.getString(): String { + val resId: Int = when (this) { + TicketType.STUDENT -> R.string.all_ticket_type_student + TicketType.VISITOR -> R.string.all_ticket_type_visitor + TicketType.OTHER -> R.string.all_ticket_type_other + } + return binding.root.context.getString(resId) + } + companion object { fun from(parent: ViewGroup): TicketReserveViewHolder { val binding = ItemTicketReserveBinding.inflate( diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/util/LifecycleOwnerUtil.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/util/LifecycleOwnerUtil.kt new file mode 100644 index 000000000..4d5363478 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/util/LifecycleOwnerUtil.kt @@ -0,0 +1,15 @@ +package com.festago.festago.presentation.util + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.launch + +fun repeatOnStarted(lifecycleOwner: LifecycleOwner, action: suspend () -> Unit) { + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + action() + } + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/util/PermissionUtil.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/util/PermissionUtil.kt new file mode 100644 index 000000000..d8f257894 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/util/PermissionUtil.kt @@ -0,0 +1,42 @@ +package com.festago.festago.presentation.util + +import android.Manifest.permission.POST_NOTIFICATIONS +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.os.Build +import androidx.activity.result.ActivityResultLauncher +import androidx.core.content.ContextCompat + +fun Activity.requestNotificationPermission(resultLauncher: ActivityResultLauncher) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + checkSelfPermission( + this, + POST_NOTIFICATIONS, + onNotGranted = { + resultLauncher.launch(POST_NOTIFICATIONS) + } + ) + } +} + +fun checkNotificationPermission(context: Context, block: () -> Unit) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + checkSelfPermission(context, POST_NOTIFICATIONS, onGranted = block) + } else { + block() + } +} + +fun checkSelfPermission( + context: Context, + permission: String, + onGranted: () -> Unit = {}, + onNotGranted: () -> Unit = {} +) { + if (ContextCompat.checkSelfPermission(context, permission) == PERMISSION_GRANTED) { + onGranted() + } else { + onNotGranted() + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/util/UserApiClientExt.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/util/UserApiClientExt.kt deleted file mode 100644 index 956572740..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/util/UserApiClientExt.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.festago.festago.presentation.util - -import android.content.Context -import com.kakao.sdk.auth.model.OAuthToken -import com.kakao.sdk.common.model.ClientError -import com.kakao.sdk.common.model.ClientErrorCause -import com.kakao.sdk.user.UserApiClient -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -/** - * 카카오톡 설치 여부에 따라서 설치 되어있으면 카카오톡 로그인을 시도한다. - * 미설치 시 카카오 계정 로그인을 시도한다. - * - * 카카오톡 로그인에 실패하면 사용자가 의도적으로 로그인 취소한 경우를 제외하고는 카카오 계정 로그인을 서브로 실행한다. - */ -suspend fun UserApiClient.Companion.loginWithKakao(context: Context): OAuthToken { - return if (instance.isKakaoTalkLoginAvailable(context)) { - try { - UserApiClient.loginWithKakaoTalk(context) - } catch (error: Throwable) { - // 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우, - // 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기) - // 그냥 에러를 올린다. - if (error is ClientError && error.reason == ClientErrorCause.Cancelled) throw error - - // 그렇지 않다면, 카카오 계정 로그인을 시도한다. - UserApiClient.loginWithKakaoAccount(context) - } - } else { - UserApiClient.loginWithKakaoAccount(context) - } -} - -/** - * 카카오톡으로 로그인 시도 - */ -private suspend fun UserApiClient.Companion.loginWithKakaoTalk(context: Context): OAuthToken { - return suspendCoroutine { continuation -> - instance.loginWithKakaoTalk(context) { token, error -> - if (error != null) { - continuation.resumeWithException(error) - } else if (token != null) { - continuation.resume(token) - } else { - continuation.resumeWithException(RuntimeException("Failure get kakao access token")) - } - } - } -} - -/** - * 카카오 계정으로 로그인 시도 - */ -private suspend fun UserApiClient.Companion.loginWithKakaoAccount(context: Context): OAuthToken { - return suspendCoroutine { continuation -> - instance.loginWithKakaoAccount(context) { token, error -> - if (error != null) { - continuation.resumeWithException(error) - } else if (token != null) { - continuation.resume(token) - } else { - continuation.resumeWithException(RuntimeException("Failure get kakao access token")) - } - } - } -} diff --git a/android/festago/app/src/main/res/drawable/ic_festago_coupon.xml b/android/festago/app/src/main/res/drawable/ic_festago_coupon.xml index e92084cd8..1a161d0ab 100644 --- a/android/festago/app/src/main/res/drawable/ic_festago_coupon.xml +++ b/android/festago/app/src/main/res/drawable/ic_festago_coupon.xml @@ -11,6 +11,6 @@ android:fillType="evenOdd"/> + android:fillColor="?attr/colorOnSurface"/> diff --git a/android/festago/app/src/main/res/drawable/menu_selector_color.xml b/android/festago/app/src/main/res/drawable/menu_selector_color.xml index 224f3e3be..dee0ba366 100644 --- a/android/festago/app/src/main/res/drawable/menu_selector_color.xml +++ b/android/festago/app/src/main/res/drawable/menu_selector_color.xml @@ -2,5 +2,5 @@ - + diff --git a/android/festago/app/src/main/res/drawable/menu_selector_color_inverse.xml b/android/festago/app/src/main/res/drawable/menu_selector_color_inverse.xml new file mode 100644 index 000000000..81734c3e0 --- /dev/null +++ b/android/festago/app/src/main/res/drawable/menu_selector_color_inverse.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/festago/app/src/main/res/layout-land/activity_home.xml b/android/festago/app/src/main/res/layout-land/activity_home.xml new file mode 100644 index 000000000..2727770b4 --- /dev/null +++ b/android/festago/app/src/main/res/layout-land/activity_home.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + diff --git a/android/festago/app/src/main/res/layout/activity_home.xml b/android/festago/app/src/main/res/layout/activity_home.xml index a8ab5eb76..4839154f8 100644 --- a/android/festago/app/src/main/res/layout/activity_home.xml +++ b/android/festago/app/src/main/res/layout/activity_home.xml @@ -20,19 +20,20 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" + android:backgroundTint="?attr/colorSurfaceBright" app:backgroundTint="?attr/colorSurface" app:contentInsetStart="0dp" app:fabAlignmentMode="center"> @@ -42,12 +43,12 @@ style="@style/Widget.MaterialComponents.FloatingActionButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@android:color/transparent" android:backgroundTint="@drawable/menu_selector_color" android:src="@drawable/ic_festago_coupon" + app:borderWidth="1dp" app:fabCustomSize="72dp" - app:tint="@color/white" app:layout_anchor="@id/baNavigation" - app:maxImageSize="72dp" /> + app:maxImageSize="72dp" + app:tint="@null" /> diff --git a/android/festago/app/src/main/res/layout/activity_reservation_complete.xml b/android/festago/app/src/main/res/layout/activity_reservation_complete.xml index 4970e1629..39dbc9277 100644 --- a/android/festago/app/src/main/res/layout/activity_reservation_complete.xml +++ b/android/festago/app/src/main/res/layout/activity_reservation_complete.xml @@ -8,8 +8,8 @@ + name="reservedTicket" + type="com.festago.festago.presentation.ui.reservationcomplete.ReservedTicketArg" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/festago/app/src/main/res/layout/activity_sign_in.xml b/android/festago/app/src/main/res/layout/activity_sign_in.xml index 9273101a1..a35d7badc 100644 --- a/android/festago/app/src/main/res/layout/activity_sign_in.xml +++ b/android/festago/app/src/main/res/layout/activity_sign_in.xml @@ -58,7 +58,7 @@ android:layout_width="0dp" android:layout_height="48dp" android:background="@drawable/bg_mypage_kakao_login" - android:onClick="@{() -> vm.signInKakao()}" + android:onClick="@{() -> vm.signIn()}" android:text="@string/mypage_btn_kakao" android:textColor="@color/kakao_label" android:textSize="16sp" diff --git a/android/festago/app/src/main/res/layout/activity_splash.xml b/android/festago/app/src/main/res/layout/activity_splash.xml new file mode 100644 index 000000000..290a1e03c --- /dev/null +++ b/android/festago/app/src/main/res/layout/activity_splash.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/android/festago/app/src/main/res/layout/activity_student_verification.xml b/android/festago/app/src/main/res/layout/activity_student_verification.xml new file mode 100644 index 000000000..38566eb9a --- /dev/null +++ b/android/festago/app/src/main/res/layout/activity_student_verification.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +