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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/festago/app/src/main/res/layout/activity_ticket_entry.xml b/android/festago/app/src/main/res/layout/activity_ticket_entry.xml
index 35e23675e..715ec927a 100644
--- a/android/festago/app/src/main/res/layout/activity_ticket_entry.xml
+++ b/android/festago/app/src/main/res/layout/activity_ticket_entry.xml
@@ -5,7 +5,7 @@
-
+
@@ -47,7 +47,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_llg"
- android:text="@{successState.ticket.festivalName}"
+ android:text="@{successState.ticket.festivalTicket.name}"
android:textColor="@color/md_theme_light_onSurface"
android:textSize="@dimen/title_medium"
android:textStyle="bold"
diff --git a/android/festago/app/src/main/res/layout/fragment_my_page.xml b/android/festago/app/src/main/res/layout/fragment_my_page.xml
index 3a52b5fda..c3fd24548 100644
--- a/android/festago/app/src/main/res/layout/fragment_my_page.xml
+++ b/android/festago/app/src/main/res/layout/fragment_my_page.xml
@@ -22,189 +22,315 @@
-
-
+
+
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_marginStart="16dp"
+ android:text="@{successState.userProfile.nickName}"
+ android:textSize="16sp"
+ android:textStyle="bold"
+ app:layout_constraintBottom_toBottomOf="@id/sivProfile"
+ app:layout_constraintStart_toEndOf="@id/sivProfile"
+ app:layout_constraintTop_toTopOf="@id/sivProfile"
+ tools:text="홍길동" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_height="120dp">
+
+
+
+
-
+ android:layout_margin="16dp">
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-
+
-
+
diff --git a/android/festago/app/src/main/res/layout/item_select_school.xml b/android/festago/app/src/main/res/layout/item_select_school.xml
new file mode 100644
index 000000000..ba5b39c2e
--- /dev/null
+++ b/android/festago/app/src/main/res/layout/item_select_school.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/android/festago/app/src/main/res/layout/item_ticket_history.xml b/android/festago/app/src/main/res/layout/item_ticket_history.xml
index 33cfae275..62bccb978 100644
--- a/android/festago/app/src/main/res/layout/item_ticket_history.xml
+++ b/android/festago/app/src/main/res/layout/item_ticket_history.xml
@@ -7,13 +7,9 @@
-
-
+ type="com.festago.festago.presentation.ui.tickethistory.TicketHistoryItemUiState" />
@color/md_theme_dark_onSurfaceVariant
- @color/md_theme_dark_inversePrimary
+
+
diff --git a/android/festago/app/src/main/res/values/strings.xml b/android/festago/app/src/main/res/values/strings.xml
index 9ccbda540..35daf2b7f 100644
--- a/android/festago/app/src/main/res/values/strings.xml
+++ b/android/festago/app/src/main/res/values/strings.xml
@@ -10,6 +10,17 @@
입장완료
외출중
+
+ 재학생용
+ 방문객용
+ 기타
+
+
+ 업데이트 알림
+ 새로운 페스타고를 사용하기 위해 업데이트 해주세요.
+ 업데이트 후 정상 사용가능합니다.
+ 페스타고 실행 중 문제가 발생했습니다. 페스타고로 문의해주세요.
+
HH:mm 티켓 활성화
티켓 제시
@@ -27,6 +38,7 @@
축제 목록
티켓 목록
마이페이지
+ 알림 권한을 거부했습니다 :(
%1s ~ %1s
@@ -37,14 +49,13 @@
%1$s(%2$s/%3$s)
", "
⚠️ 재학생용 티켓예매를 위해 사전에 학교 인증이 필요합니다.
- 예매 가능한 티켓이 없습니다.
+ 축제 조회에 실패했습니다.
티켓 예매
(%1$s/%2$s)
예매 하기
MM월 dd일 HH:mm 오픈예정
로그인 후 예매
-
yyyy.MM.dd
진행중
@@ -66,6 +77,7 @@
확인
+ 취소
카카오로 로그인
@@ -109,4 +121,23 @@
탈퇴
취소
+
+
+ 학교 이메일
+ 인증 코드
+ 인증 번호 받기
+ 인증 번호 확인
+ \@%s
+ mm:ss
+ 학교 정보 받아오기에 실패했습니다.
+
+
+ 다음
+ 학교 선택
+ 학교 목록 불러오기에 실패했습니다.
+ 학교 선택
+
+
+ 공연 입장 알림
+
diff --git a/android/festago/app/src/main/res/values/style.xml b/android/festago/app/src/main/res/values/style.xml
index 6e137c406..004ea7295 100644
--- a/android/festago/app/src/main/res/values/style.xml
+++ b/android/festago/app/src/main/res/values/style.xml
@@ -4,7 +4,7 @@
diff --git a/android/festago/app/src/main/res/values/themes.xml b/android/festago/app/src/main/res/values/themes.xml
index 55a94eb39..2ec47a8fc 100644
--- a/android/festago/app/src/main/res/values/themes.xml
+++ b/android/festago/app/src/main/res/values/themes.xml
@@ -30,4 +30,15 @@
+
+
+
+
diff --git a/android/festago/app/src/test/java/com/festago/festago/data/repository/ReservationTicketRetrofitServiceTest.kt b/android/festago/app/src/test/java/com/festago/festago/data/repository/ReservationTicketRetrofitServiceTest.kt
index 12902f5e0..8863bb511 100644
--- a/android/festago/app/src/test/java/com/festago/festago/data/repository/ReservationTicketRetrofitServiceTest.kt
+++ b/android/festago/app/src/test/java/com/festago/festago/data/repository/ReservationTicketRetrofitServiceTest.kt
@@ -187,6 +187,7 @@ class ReservationTicketRetrofitServiceTest {
private fun getFakeFestival(): ReservationFestivalResponse {
return ReservationFestivalResponse(
id = 1,
+ schoolId = 1,
name = "테코대학교",
startDate = "2023-07-03",
endDate = "2023-07-09",
@@ -197,7 +198,8 @@ class ReservationTicketRetrofitServiceTest {
startTime = "2023-07-09T16:00:00",
ticketOpenTime = "2023-07-08T14:00:00",
lineUp = "르세라핌,아이브,뉴진스",
- tickets = listOf(
+ tickets =
+ listOf(
ReservationTicketResponse(
id = 1,
ticketType = "STUDENT",
@@ -210,6 +212,7 @@ class ReservationTicketRetrofitServiceTest {
totalAmount = 300,
remainAmount = 212,
),
+
),
),
ReservationStageResponse(
@@ -217,7 +220,8 @@ class ReservationTicketRetrofitServiceTest {
startTime = "2023-07-09T16:00:00",
ticketOpenTime = "2023-07-08T14:00:00",
lineUp = "르세라핌,아이브,뉴진스",
- tickets = listOf(
+ tickets =
+ listOf(
ReservationTicketResponse(
id = 3,
ticketType = "STUDENT",
@@ -240,6 +244,7 @@ class ReservationTicketRetrofitServiceTest {
return """
{
"id": 1,
+ "schoolId": 1,
"name": "테코대학교",
"startDate": "2023-07-03",
"endDate": "2023-07-09",
diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/HomeViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/HomeViewModelTest.kt
index 5e7b174f7..6a7c5616c 100644
--- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/HomeViewModelTest.kt
+++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/HomeViewModelTest.kt
@@ -1,6 +1,6 @@
package com.festago.festago.presentation.ui.home
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import app.cash.turbine.test
import com.festago.festago.repository.AuthRepository
import io.mockk.every
import io.mockk.mockk
@@ -8,20 +8,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
class HomeViewModelTest {
private lateinit var vm: HomeViewModel
private lateinit var authRepository: AuthRepository
- @get:Rule
- val instantExecutorRule = InstantTaskExecutorRule()
-
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
@@ -36,75 +33,109 @@ class HomeViewModelTest {
Dispatchers.resetMain()
}
+ private fun `사용자 인증 유무가 다음과 같을 때`(isSigned: Boolean) {
+ every { authRepository.isSigned } returns isSigned
+ }
+
@Test
- fun `축제 목록을 요청했을 때 토큰이 있으면 축제 목록이 보인다`() {
+ fun `축제 목록을 요청했을 때 토큰이 있으면 축제 목록이 보이고 이벤트가 발생하지 않는다`() = runTest {
// given
- every { authRepository.isSigned } returns true
+ `사용자 인증 유무가 다음과 같을 때`(true)
- // when
- vm.loadHomeItem(HomeItemType.FESTIVAL_LIST)
+ vm.event.test {
+ // when
+ vm.selectItem(HomeItemType.FESTIVAL_LIST)
- // then
- assertThat(vm.event.getValue() is HomeEvent.ShowFestivalList).isTrue
+ // then
+ assertThat(vm.selectedItem.value).isEqualTo(HomeItemType.FESTIVAL_LIST)
+
+ // and
+ expectNoEvents()
+ }
}
@Test
- fun `축제 목록을 요청했을 때 토큰이 없어도 축제 목록이 보인다`() {
+ fun `축제 목록을 요청했을 때 토큰이 없어도 축제 목록이 보이고 이벤트가 발생하지 않는다`() = runTest {
// given
- every { authRepository.isSigned } returns false
+ `사용자 인증 유무가 다음과 같을 때`(false)
+
+ vm.event.test {
+ // when
+ vm.selectItem(HomeItemType.FESTIVAL_LIST)
- // when
- vm.loadHomeItem(HomeItemType.FESTIVAL_LIST)
+ // then
+ assertThat(vm.selectedItem.value).isEqualTo(HomeItemType.FESTIVAL_LIST)
- // then
- assertThat(vm.event.getValue() is HomeEvent.ShowFestivalList).isTrue
+ // and
+ expectNoEvents()
+ }
}
@Test
- fun `티켓 목록을 요청했을 때 토큰이 있으면 티켓 목록 보기 이벤트가 발생한다`() {
+ fun `티켓 목록을 요청했을 때 토큰이 있으면 티켓 목록이 보이고 이벤트가 발생하지 않는다`() = runTest {
// given
- every { authRepository.isSigned } returns true
+ `사용자 인증 유무가 다음과 같을 때`(true)
- // when
- vm.loadHomeItem(HomeItemType.TICKET_LIST)
+ vm.event.test {
+ // when
+ vm.selectItem(HomeItemType.TICKET_LIST)
- // then
- assertThat(vm.event.getValue() is HomeEvent.ShowTicketList).isTrue
+ // then
+ assertThat(vm.selectedItem.value).isEqualTo(HomeItemType.TICKET_LIST)
+
+ // and
+ expectNoEvents()
+ }
}
@Test
- fun `티켓 목록을 요청했을 때 토큰이 있으면 로그인 보기 이벤트가 발생한다`() {
+ fun `티켓 목록을 요청했을 때 토큰이 있으면 로그인 이벤트가 발생하고 선택된 화면은 축제 목록 그대로이다`() = runTest {
// given
- every { authRepository.isSigned } returns false
+ `사용자 인증 유무가 다음과 같을 때`(false)
+
+ vm.event.test {
+ // when
+ vm.selectItem(HomeItemType.TICKET_LIST)
- // when
- vm.loadHomeItem(HomeItemType.TICKET_LIST)
+ // then
+ assertThat(awaitItem()).isExactlyInstanceOf(HomeEvent.ShowSignIn::class.java)
- // then
- assertThat(vm.event.getValue() is HomeEvent.ShowSignIn).isTrue
+ // and
+ assertThat(vm.selectedItem.value).isEqualTo(HomeItemType.FESTIVAL_LIST)
+ }
}
@Test
- fun `마이페이지를 요청했을 때 토큰이 있으면 마이페이지 보기 이벤트가 발생한다`() {
+ fun `마이페이지를 요청했을 때 토큰이 있으면 마이페이지가 보이고 이벤트가 발생하지 않는다`() = runTest {
// given
- every { authRepository.isSigned } returns true
+ `사용자 인증 유무가 다음과 같을 때`(true)
- // when
- vm.loadHomeItem(HomeItemType.MY_PAGE)
+ vm.event.test {
+ // when
+ vm.selectItem(HomeItemType.MY_PAGE)
- // then
- assertThat(vm.event.getValue() is HomeEvent.ShowMyPage).isTrue
+ // then
+ assertThat(vm.selectedItem.value).isEqualTo(HomeItemType.MY_PAGE)
+
+ // and
+ expectNoEvents()
+ }
}
@Test
- fun `마이페이즈를 요청했을 때 토큰이 없으면 로그인 보기 이벤트가 발생한다`() {
+ fun `마이페이즈를 요청했을 때 토큰이 없으면 로그인 보기 이벤트가 발생하고 선택된 화면은 축제 목록 그대로이다`() = runTest {
// given
- every { authRepository.isSigned } returns false
+ `사용자 인증 유무가 다음과 같을 때`(false)
+
+ vm.event.test {
+ // when
+ vm.selectItem(HomeItemType.MY_PAGE)
- // when
- vm.loadHomeItem(HomeItemType.MY_PAGE)
+ // then
+ assertThat(awaitItem()).isExactlyInstanceOf(HomeEvent.ShowSignIn::class.java)
- // then
- assertThat(vm.event.getValue() is HomeEvent.ShowSignIn).isTrue
+ // and
+ assertThat(vm.selectedItem.value).isEqualTo(HomeItemType.FESTIVAL_LIST)
+ }
}
}
diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModelTest.kt
index 95d5e5344..56b5b060b 100644
--- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModelTest.kt
+++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModelTest.kt
@@ -1,6 +1,6 @@
package com.festago.festago.presentation.ui.home.festivallist
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import app.cash.turbine.test
import com.festago.festago.analytics.AnalyticsHelper
import com.festago.festago.model.Festival
import com.festago.festago.repository.FestivalRepository
@@ -11,16 +11,17 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.junit.After
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import java.time.LocalDate
class FestivalListViewModelTest {
+
private lateinit var vm: FestivalListViewModel
private lateinit var festivalRepository: FestivalRepository
private lateinit var analyticsHelper: AnalyticsHelper
@@ -35,9 +36,6 @@ class FestivalListViewModelTest {
)
}
- @get:Rule
- val instantExecutorRule = InstantTaskExecutorRule()
-
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
@@ -53,14 +51,18 @@ class FestivalListViewModelTest {
Dispatchers.resetMain()
}
- @Test
- fun `축제 목록 받아오기에 성공하면 성공 상태이고 축제 목록을 반환한다`() {
- // given
+ private fun `축제 목록 요청 결과가 다음과 같을 때`(result: Result>) {
coEvery {
festivalRepository.loadFestivals()
} answers {
- Result.success(fakeFestivals)
+ result
}
+ }
+
+ @Test
+ fun `축제 목록 받아오기에 성공하면 성공 상태이고 축제 목록을 반환한다`() {
+ // given
+ `축제 목록 요청 결과가 다음과 같을 때`(Result.success(fakeFestivals))
// when
vm.loadFestivals()
@@ -70,9 +72,9 @@ class FestivalListViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(FestivalListUiState.Success::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(true)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
// and
val actual = (vm.uiState.value as FestivalListUiState.Success).festivals
@@ -85,11 +87,7 @@ class FestivalListViewModelTest {
@Test
fun `축제 목록 받아오기에 실패하면 에러 상태다`() {
// given
- coEvery {
- festivalRepository.loadFestivals()
- } answers {
- Result.failure(Exception())
- }
+ `축제 목록 요청 결과가 다음과 같을 때`(Result.failure(Exception()))
// when
vm.loadFestivals()
@@ -99,9 +97,9 @@ class FestivalListViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(FestivalListUiState.Error::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(true)
}
softly.assertAll()
}
@@ -124,21 +122,24 @@ class FestivalListViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(FestivalListUiState.Loading::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(true)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
}
softly.assertAll()
}
@Test
- fun `티켓 예매를 열면 티켓 예매 열기 이벤트가 발생한다`() {
- // when
- val fakeFestivalId = 1L
- vm.showTicketReserve(fakeFestivalId)
+ fun `티켓 예매를 열면 티켓 예매 열기 이벤트가 발생한다`() = runTest {
- // then
- assertThat(vm.event.getValue()).isInstanceOf(FestivalListEvent.ShowTicketReserve::class.java)
+ vm.event.test {
+ // when
+ val fakeFestivalId = 1L
+ vm.showTicketReserve(fakeFestivalId)
+
+ // then
+ assertThat(awaitItem()).isExactlyInstanceOf(FestivalListEvent.ShowTicketReserve::class.java)
+ }
}
private fun Festival.toUiState() = FestivalItemUiState(
diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/mypage/MyPageViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/mypage/MyPageViewModelTest.kt
index 305a1e883..2eb87bdae 100644
--- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/mypage/MyPageViewModelTest.kt
+++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/mypage/MyPageViewModelTest.kt
@@ -1,13 +1,12 @@
package com.festago.festago.presentation.ui.home.mypage
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import app.cash.turbine.test
import com.festago.festago.analytics.AnalyticsHelper
import com.festago.festago.model.MemberTicketFestival
import com.festago.festago.model.Stage
import com.festago.festago.model.Ticket
import com.festago.festago.model.TicketCondition
import com.festago.festago.model.UserProfile
-import com.festago.festago.presentation.mapper.toPresentation
import com.festago.festago.repository.AuthRepository
import com.festago.festago.repository.TicketRepository
import com.festago.festago.repository.UserRepository
@@ -18,12 +17,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.junit.After
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import java.time.LocalDateTime
@@ -56,9 +55,6 @@ class MyPageViewModelTest {
),
)
- @get:Rule
- val instantExecutorRule = InstantTaskExecutorRule()
-
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
@@ -76,42 +72,52 @@ class MyPageViewModelTest {
Dispatchers.resetMain()
}
- @Test
- fun `로그인 되지 않았다면 로그인 이벤트가 발생한다`() {
- // given
+ private fun `로그인 상태가 다음과 같다`(result: Boolean) {
coEvery {
authRepository.isSigned
} answers {
- false
+ result
}
-
- // when
- vm.loadUserInfo()
-
- // then
- assertThat(vm.event.getValue() is MyPageEvent.ShowSignIn).isTrue
}
- @Test
- fun `유저 프로필, 첫번째 티켓 받아오기에 성공하면 성공 상태다`() {
- // given
+ private fun `유저 프로필 요청의 결과가 다음과 같다`(result: Result) {
coEvery {
userRepository.loadUserProfile()
} answers {
- Result.success(fakeUserProfile)
+ result
}
+ }
+ private fun `과거 예매 내역 요청의 결과가 다음과 같다`(result: Result>) {
coEvery {
- ticketRepository.loadHistoryTickets(1)
+ ticketRepository.loadHistoryTickets(any())
} answers {
- Result.success(fakeTickets)
+ result
}
+ }
- coEvery {
- authRepository.isSigned
- } answers {
- true
+ @Test
+ fun `로그인 되지 않았다면 로그인 이벤트가 발생한다`() = runTest {
+ // given
+ `로그인 상태가 다음과 같다`(false)
+
+ vm.event.test {
+ // when
+ vm.loadUserInfo()
+
+ // then
+ assertThat(awaitItem()).isExactlyInstanceOf(MyPageEvent.ShowSignIn::class.java)
}
+ }
+
+ @Test
+ fun `유저 프로필, 과거 예매 내역 받아오기에 성공하면 성공 상태다`() {
+ // given
+ `유저 프로필 요청의 결과가 다음과 같다`(Result.success(fakeUserProfile))
+
+ `과거 예매 내역 요청의 결과가 다음과 같다`(Result.success(fakeTickets))
+
+ `로그인 상태가 다음과 같다`(true)
// when
vm.loadUserInfo()
@@ -121,29 +127,25 @@ class MyPageViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(MyPageUiState.Success::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(true)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
// and
val actualUserProfile = (vm.uiState.value as MyPageUiState.Success).userProfile
- assertThat(actualUserProfile).isEqualTo(fakeUserProfile.toPresentation())
+ assertThat(actualUserProfile).isEqualTo(fakeUserProfile)
// and
val actualTicket = (vm.uiState.value as MyPageUiState.Success).ticket
- assertThat(actualTicket).isEqualTo(fakeTickets.first().toPresentation())
+ assertThat(actualTicket).isEqualTo(fakeTickets.first())
}
softly.assertAll()
}
@Test
- fun `유저 프로필 받아오기에 성공하고, 첫번째 티켓을 받아오는 중이면 성공 상태이고, 티켓은 없다`() {
+ fun `유저 프로필 받아오기에 성공하고, 과거 예매 내역을 받아오는 중이면 로딩 상태이다`() {
// given
- coEvery {
- userRepository.loadUserProfile()
- } answers {
- Result.success(fakeUserProfile)
- }
+ `유저 프로필 요청의 결과가 다음과 같다`(Result.success(fakeUserProfile))
coEvery {
ticketRepository.loadHistoryTickets(1)
@@ -152,30 +154,19 @@ class MyPageViewModelTest {
Result.success(fakeTickets)
}
- coEvery {
- authRepository.isSigned
- } answers {
- true
- }
+ `로그인 상태가 다음과 같다`(true)
// when
vm.loadUserInfo()
// then
val softly = SoftAssertions().apply {
- assertThat(vm.uiState.value).isInstanceOf(MyPageUiState.Success::class.java)
-
- // and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(true)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false)
-
- // and
- assertThat((vm.uiState.value as MyPageUiState.Success).hasTicket).isFalse
+ assertThat(vm.uiState.value).isInstanceOf(MyPageUiState.Loading::class.java)
// and
- val actualTicket = (vm.uiState.value as MyPageUiState.Success).ticket
- assertThat(actualTicket).isNotEqualTo(fakeTickets.first().toPresentation())
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
}
softly.assertAll()
}
@@ -183,23 +174,11 @@ class MyPageViewModelTest {
@Test
fun `유저 프로필 받아오기 실패하면 에러 상태다`() {
// given
- coEvery {
- userRepository.loadUserProfile()
- } answers {
- Result.failure(Exception())
- }
+ `유저 프로필 요청의 결과가 다음과 같다`(Result.failure(Exception()))
- coEvery {
- ticketRepository.loadHistoryTickets(1)
- } answers {
- Result.success(fakeTickets)
- }
+ `과거 예매 내역 요청의 결과가 다음과 같다`(Result.success(fakeTickets))
- coEvery {
- authRepository.isSigned
- } answers {
- true
- }
+ `로그인 상태가 다음과 같다`(true)
// when
vm.loadUserInfo()
@@ -209,33 +188,21 @@ class MyPageViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(MyPageUiState.Error::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(true)
}
softly.assertAll()
}
@Test
- fun `첫번째 티켓 받아오기에 실패하면 에러 상태다`() {
+ fun `과거 예매 내역 받아오기에 실패하면 에러 상태다`() {
// given
- coEvery {
- userRepository.loadUserProfile()
- } answers {
- Result.success(fakeUserProfile)
- }
+ `유저 프로필 요청의 결과가 다음과 같다`(Result.success(fakeUserProfile))
- coEvery {
- ticketRepository.loadHistoryTickets(1)
- } answers {
- Result.failure(Exception())
- }
+ `과거 예매 내역 요청의 결과가 다음과 같다`(Result.failure(Exception()))
- coEvery {
- authRepository.isSigned
- } answers {
- true
- }
+ `로그인 상태가 다음과 같다`(true)
// when
vm.loadUserInfo()
@@ -245,15 +212,15 @@ class MyPageViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(MyPageUiState.Error::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(true)
}
softly.assertAll()
}
@Test
- fun `로그아웃에 성공하면 SignOutSuccess 이벤트가 발생하고 에러 상태이다`() {
+ fun `로그아웃에 성공하면 SignOutSuccess 이벤트가 발생하고 에러 상태이다`() = runTest {
// given
coEvery {
authRepository.signOut()
@@ -261,33 +228,37 @@ class MyPageViewModelTest {
Result.success(Unit)
}
- // when
- vm.signOut()
-
- // then
- val softly = SoftAssertions().apply {
- assertThat(vm.event.getValue() is MyPageEvent.SignOutSuccess).isTrue
- assertThat(vm.uiState.value is MyPageUiState.Error).isTrue
-
- // and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true)
+ vm.event.test {
+ // when
+ vm.signOut()
+
+ // then
+ val softly = SoftAssertions().apply {
+ assertThat(awaitItem()).isExactlyInstanceOf(MyPageEvent.SignOutSuccess::class.java)
+ assertThat(vm.uiState.value is MyPageUiState.Error).isTrue
+
+ // and
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(true)
+ }
+ softly.assertAll()
}
- softly.assertAll()
}
@Test
- fun `회원탈퇴 확인 이벤트가 발생한다`() {
- // when
- vm.showConfirmDelete()
+ fun `회원탈퇴 확인 이벤트가 발생한다`() = runTest {
+ vm.event.test {
+ // when
+ vm.showConfirmDelete()
- // then
- assertThat(vm.event.getValue() is MyPageEvent.ShowConfirmDelete).isTrue
+ // then
+ assertThat(awaitItem()).isExactlyInstanceOf(MyPageEvent.ShowConfirmDelete::class.java)
+ }
}
@Test
- fun `회원 탈퇴에 성공하면 DeleteAccountSuccess 이벤트가 발생하고 에러상태다`() {
+ fun `회원 탈퇴에 성공하면 DeleteAccountSuccess 이벤트가 발생하고 에러상태다`() = runTest {
// given
coEvery {
authRepository.deleteAccount()
@@ -295,19 +266,21 @@ class MyPageViewModelTest {
Result.success(Unit)
}
- // when
- vm.deleteAccount()
-
- // then
- val softly = SoftAssertions().apply {
- assertThat(vm.event.getValue() is MyPageEvent.DeleteAccountSuccess).isTrue
- assertThat(vm.uiState.value is MyPageUiState.Error).isTrue
-
- // and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true)
+ vm.event.test {
+ // when
+ vm.deleteAccount()
+
+ // then
+ val softly = SoftAssertions().apply {
+ assertThat(awaitItem()).isExactlyInstanceOf(MyPageEvent.DeleteAccountSuccess::class.java)
+ assertThat(vm.uiState.value is MyPageUiState.Error).isTrue
+
+ // and
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(true)
+ }
+ softly.assertAll()
}
- softly.assertAll()
}
}
diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModelTest.kt
index e4698197d..050fe50fd 100644
--- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModelTest.kt
+++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModelTest.kt
@@ -1,10 +1,9 @@
package com.festago.festago.presentation.ui.home.ticketlist
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import app.cash.turbine.test
import com.festago.festago.analytics.AnalyticsHelper
import com.festago.festago.model.Ticket
import com.festago.festago.presentation.fixture.TicketFixture
-import com.festago.festago.presentation.mapper.toPresentation
import com.festago.festago.repository.TicketRepository
import io.mockk.coEvery
import io.mockk.mockk
@@ -13,23 +12,18 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
-import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.junit.After
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
-import java.time.LocalDateTime
class TicketListViewModelTest {
private lateinit var vm: TicketListViewModel
private lateinit var ticketRepository: TicketRepository
private lateinit var analyticsHelper: AnalyticsHelper
- @get:Rule
- val instantExecutorRule = InstantTaskExecutorRule()
-
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
@@ -45,12 +39,16 @@ class TicketListViewModelTest {
Dispatchers.resetMain()
}
+ private fun `현재 티켓 요청 결과가 다음과 같을 때`(result: Result>) {
+ coEvery { ticketRepository.loadCurrentTickets() } returns result
+ }
+
@Test
fun `티켓을 받아왔을 때 티켓이 있으면 성공이고 티켓도 존재하는 상태이다`() {
// given
val tickets = TicketFixture.getMemberTickets((1L..10L).toList())
- coEvery { ticketRepository.loadCurrentTickets() } returns Result.success(tickets)
+ `현재 티켓 요청 결과가 다음과 같을 때`(Result.success(tickets))
// when
vm.loadCurrentTickets()
@@ -60,14 +58,14 @@ class TicketListViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(TicketListUiState.Success::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isEqualTo(true)
- assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
// and
val actual = (vm.uiState.value as TicketListUiState.Success).tickets
- val expected = tickets.map { it.toUiState() }
+ val expected = tickets.map { TicketListItemUiState.of(it, vm::showTicketEntry) }
assertThat(actual).isEqualTo(expected)
}
softly.assertAll()
@@ -77,7 +75,8 @@ class TicketListViewModelTest {
fun `티켓을 받아왔을 때 티켓이 없으면 성공이지만 티켓은 없는 상태이다`() {
// given
val fakeEmptyTickets = emptyList()
- coEvery { ticketRepository.loadCurrentTickets() } returns Result.success(fakeEmptyTickets)
+
+ `현재 티켓 요청 결과가 다음과 같을 때`(Result.success(fakeEmptyTickets))
// when
vm.loadCurrentTickets()
@@ -87,14 +86,15 @@ class TicketListViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(TicketListUiState.Success::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isEqualTo(true)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
// and
val actual = (vm.uiState.value as TicketListUiState.Success).tickets
- val expected = fakeEmptyTickets.map { it.toUiState() }
+ val expected =
+ fakeEmptyTickets.map { TicketListItemUiState.of(it, vm::showTicketEntry) }
assertThat(actual).isEqualTo(expected)
}
softly.assertAll()
@@ -103,9 +103,7 @@ class TicketListViewModelTest {
@Test
fun `티켓 목록 받아오기를 실패하면 에러 상태이다`() {
// given
- coEvery { ticketRepository.loadCurrentTickets() } answers {
- Result.failure(Exception())
- }
+ `현재 티켓 요청 결과가 다음과 같을 때`(Result.failure(Exception()))
// when
vm.loadCurrentTickets()
@@ -115,10 +113,10 @@ class TicketListViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(TicketListUiState.Error::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(true)
}
softly.assertAll()
}
@@ -141,41 +139,32 @@ class TicketListViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(TicketListUiState.Loading::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(true)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
}
softly.assertAll()
}
@Test
- fun `티켓 제시를 요청하면 이벤트가 발생한다`() {
+ fun `1번 티켓 제시를 요청하면 1번 티켓 제시 화면 보여주기 이벤트가 발생한다`() = runTest {
// given
- // when
- vm.showTicketEntry(1L)
+ vm.event.test {
+ // when
+ vm.showTicketEntry(1L)
- // then
- assertThat(vm.event.getValue()).isInstanceOf(TicketListEvent.ShowTicketEntry::class.java)
+ // then
+ val softly = SoftAssertions().apply {
+ val event = awaitItem()
+ assertThat(event).isExactlyInstanceOf(TicketListEvent.ShowTicketEntry::class.java)
- // and
- val actual = (vm.event.getValue() as TicketListEvent.ShowTicketEntry).ticketId
- val expected = 1L
- assertThat(actual).isEqualTo(expected)
+ // and
+ val ticketId = (event as? TicketListEvent.ShowTicketEntry)?.ticketId
+ assertThat(ticketId).isEqualTo(1L)
+ }
+ softly.assertAll()
+ }
}
-
- 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 = vm::showTicketEntry,
- )
}
diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModelTest.kt
new file mode 100644
index 000000000..d6708dc56
--- /dev/null
+++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModelTest.kt
@@ -0,0 +1,133 @@
+package com.festago.festago.presentation.ui.selectschool
+
+import app.cash.turbine.test
+import com.festago.festago.analytics.AnalyticsHelper
+import com.festago.festago.model.School
+import com.festago.festago.repository.SchoolRepository
+import io.mockk.coEvery
+import io.mockk.mockk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.SoftAssertions.assertSoftly
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class SelectSchoolViewModelTest {
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private lateinit var vm: SelectSchoolViewModel
+ private lateinit var schoolRepository: SchoolRepository
+ private lateinit var analyticsHelper: AnalyticsHelper
+
+ @Before
+ fun setup() {
+ Dispatchers.setMain(testDispatcher)
+ schoolRepository = mockk()
+ analyticsHelper = mockk(relaxed = true)
+ vm = SelectSchoolViewModel(
+ schoolRepository = schoolRepository,
+ analyticsHelper = analyticsHelper,
+ )
+ }
+
+ @After
+ fun finish() {
+ Dispatchers.resetMain()
+ }
+
+ private fun `학교 목록 불러오기 요청 결과가 다음과 같을 때 `(result: Result>) {
+ coEvery {
+ schoolRepository.loadSchools()
+ } returns result
+ }
+
+ @Test
+ fun `학교 목록 불러오기에 성공하면 성공 상태다`() {
+ // given
+ `학교 목록 불러오기 요청 결과가 다음과 같을 때 `(Result.success(fakeSchools))
+
+ // when
+ vm.loadSchools()
+
+ // then
+ assertSoftly { softly ->
+ softly.assertThat(vm.uiState.value)
+ .isExactlyInstanceOf(SelectSchoolUiState.Success::class.java)
+
+ val successState = vm.uiState.value as? SelectSchoolUiState.Success
+ successState?.let {
+ softly.assertThat(it.enableNext).isEqualTo(false)
+ }
+ }
+ }
+
+ @Test
+ fun `학교 목록 불러오기에 실패하면 에러 상태다`() {
+ // given
+ `학교 목록 불러오기 요청 결과가 다음과 같을 때 `(Result.failure(Exception()))
+
+ // when
+ vm.loadSchools()
+
+ // then
+ assertThat(vm.uiState.value).isExactlyInstanceOf(SelectSchoolUiState.Error::class.java)
+ }
+
+ @Test
+ fun `성공 상태에서 학교를 선택하면 선택한 학교 ID가 uistate에 설정된다`() {
+ // given
+ `학교 목록 불러오기 요청 결과가 다음과 같을 때 `(Result.success(fakeSchools))
+ vm.loadSchools()
+
+ // when
+ vm.selectSchool(fakeSchoolId)
+
+ // then
+ val uiState = vm.uiState.value as SelectSchoolUiState.Success
+ assertThat(uiState.selectedSchoolId).isEqualTo(fakeSchoolId)
+ }
+
+ @Test
+ fun `에러 상태에서 학교를 선택하면 여전히 에러 상태다`() {
+ // given
+ `학교 목록 불러오기 요청 결과가 다음과 같을 때 `(Result.failure(Exception()))
+ vm.loadSchools()
+
+ // when
+ vm.selectSchool(fakeSchoolId)
+
+ // then
+ assertThat(vm.uiState.value).isExactlyInstanceOf(SelectSchoolUiState.Error::class.java)
+ }
+
+ @Test
+ fun `성공 상태이고 학교 선택하고 학생 인증 보여주기를 시도하면 학생 인증 보여주기 이벤트가 발생한다`() = runTest {
+ // given
+ `학교 목록 불러오기 요청 결과가 다음과 같을 때 `(Result.success(fakeSchools))
+ vm.loadSchools()
+ vm.selectSchool(fakeSchoolId)
+
+ // when
+ vm.event.test {
+ // when
+ vm.showStudentVerification()
+
+ // then
+ assertThat(awaitItem()).isExactlyInstanceOf(SelectSchoolEvent.ShowStudentVerification::class.java)
+ }
+ }
+
+ private val fakeSchoolId = 1L
+
+ private val fakeSchools = listOf(
+ School(id = fakeSchoolId, domain = "scripta", name = "Charley Sullivan"),
+ School(id = 8930, domain = "movet", name = "Juliette Fleming"),
+ )
+}
diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/signin/SignInViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/signin/SignInViewModelTest.kt
index fe3e8a73b..c8f209389 100644
--- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/signin/SignInViewModelTest.kt
+++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/signin/SignInViewModelTest.kt
@@ -1,6 +1,6 @@
package com.festago.festago.presentation.ui.signin
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import app.cash.turbine.test
import com.festago.festago.analytics.AnalyticsHelper
import com.festago.festago.repository.AuthRepository
import io.mockk.coEvery
@@ -9,11 +9,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
class SignInViewModelTest {
@@ -21,13 +21,11 @@ class SignInViewModelTest {
private lateinit var authRepository: AuthRepository
private lateinit var analyticsHelper: AnalyticsHelper
- @get:Rule
- val instantExecutorRule = InstantTaskExecutorRule()
-
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
Dispatchers.setMain(UnconfinedTestDispatcher())
+
authRepository = mockk(relaxed = true)
analyticsHelper = mockk(relaxed = true)
vm = SignInViewModel(authRepository, analyticsHelper)
@@ -39,45 +37,35 @@ class SignInViewModelTest {
Dispatchers.resetMain()
}
- @Test
- fun `로그인 성공하면 성공 이벤트가 발생한다`() {
- // given
- coEvery {
- authRepository.signIn(any(), any())
- } answers {
- Result.success(Unit)
- }
-
- // when
- vm.signIn("testToken")
-
- // then
- assertThat(vm.event.getValue() is SignInEvent.SignInSuccess).isTrue
+ private fun `로그인 결과가 다음과 같을 때`(result: Result) {
+ coEvery { authRepository.signIn() } returns result
}
@Test
- fun `로그인 실패하면 실패 이벤트가 발생한다`() {
+ fun `로그인 성공하면 성공 이벤트가 발생한다`() = runTest {
// given
- coEvery {
- authRepository.signIn(any(), any())
- } answers {
- Result.failure(Exception())
- }
+ `로그인 결과가 다음과 같을 때`(Result.success(Unit))
- // when
- vm.signIn("testToken")
+ vm.event.test {
+ // when
+ vm.signIn()
- // then
- assertThat(vm.event.getValue() is SignInEvent.SignInFailure).isTrue
+ // then
+ assertThat(awaitItem()).isExactlyInstanceOf(SignInEvent.SignInSuccess::class.java)
+ }
}
@Test
- fun `로그인을 요청하면 로그인 화면을 보여주는 이벤트가 발생한다`() {
+ fun `로그인 실패하면 실패 이벤트가 발생한다`() = runTest {
// given
- // when
- vm.signInKakao()
+ `로그인 결과가 다음과 같을 때`(Result.failure(Exception()))
- // then
- assertThat(vm.event.getValue() is SignInEvent.ShowSignInPage).isTrue
+ vm.event.test {
+ // when
+ vm.signIn()
+
+ // then
+ assertThat(awaitItem()).isExactlyInstanceOf(SignInEvent.SignInFailure::class.java)
+ }
}
}
diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModelTest.kt
new file mode 100644
index 000000000..672066619
--- /dev/null
+++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModelTest.kt
@@ -0,0 +1,273 @@
+package com.festago.festago.presentation.ui.studentverification
+
+import app.cash.turbine.test
+import com.festago.festago.analytics.AnalyticsHelper
+import com.festago.festago.model.StudentVerificationCode
+import com.festago.festago.repository.SchoolRepository
+import com.festago.festago.repository.StudentVerificationRepository
+import io.mockk.coEvery
+import io.mockk.mockk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.SoftAssertions.assertSoftly
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class StudentVerificationViewModelTest {
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private lateinit var vm: StudentVerificationViewModel
+ private lateinit var studentVerificationRepository: StudentVerificationRepository
+ private lateinit var schoolRepository: SchoolRepository
+ private lateinit var analyticsHelper: AnalyticsHelper
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(testDispatcher)
+ studentVerificationRepository = mockk()
+ schoolRepository = mockk()
+ analyticsHelper = mockk(relaxed = true)
+ vm = StudentVerificationViewModel(
+ schoolRepository = schoolRepository,
+ studentVerificationRepository = studentVerificationRepository,
+ analyticsHelper = analyticsHelper,
+ )
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @After
+ fun finish() {
+ Dispatchers.resetMain()
+ }
+
+ private fun `이메일 요청 결과가 다음과 같을 때`(result: Result) {
+ coEvery {
+ schoolRepository.loadSchoolEmail(any())
+ } returns result
+ }
+
+ private fun `이메일을 불러오면`(schoolId: Long = 1L) {
+ vm.loadSchoolEmail(schoolId)
+ }
+
+ private fun `인증 코드 전송 요청 결과가 다음과 같을 때`(result: Result) {
+ coEvery {
+ studentVerificationRepository.sendVerificationCode(any(), any())
+ } returns result
+ }
+
+ private fun `인증 코드를 전송하면`(userName: String = "test", schoolId: Long = 1) {
+ vm.sendVerificationCode(userName, schoolId)
+ }
+
+ private fun `인증 코드 확인 요청 결과가 다음과 같을 떄`(result: Result, fakeCode: String) {
+ coEvery {
+ studentVerificationRepository.requestVerificationCodeConfirm(
+ StudentVerificationCode(fakeCode),
+ )
+ } returns result
+ }
+
+ private fun `인증 코드를 확인하면`() {
+ vm.confirmVerificationCode()
+ }
+
+ private fun `인증 코드를 입력하면`(code: String) {
+ vm.verificationCode.value = code
+ }
+
+ @Test
+ fun `이메일 불러오기에 성공하면 성공 상태가 되고 이메일이 불러와진다`() {
+ // given
+ val fakeEmail = "test.com"
+
+ `이메일 요청 결과가 다음과 같을 때`(Result.success(fakeEmail))
+
+ // when
+ `이메일을 불러오면`()
+
+ // then
+ assertSoftly { softly ->
+ val uiState = vm.uiState.value as? StudentVerificationUiState.Success
+
+ softly.assertThat(vm.uiState.value)
+ .isExactlyInstanceOf(StudentVerificationUiState.Success::class.java)
+ softly.assertThat(uiState?.schoolEmail).isEqualTo(fakeEmail)
+ }
+ }
+
+ @Test
+ fun `이메일 불러오기에 실패하면 실패 상태가 된다`() {
+ // given
+ `이메일 요청 결과가 다음과 같을 때`(Result.failure(Exception()))
+
+ // when
+ `이메일을 불러오면`()
+
+ // then
+ assertThat(vm.uiState.value).isExactlyInstanceOf(StudentVerificationUiState.Error::class.java)
+ }
+
+ @Test
+ fun `이메일 불러오기에 성공한 상태에서 코드 전송에 성공하면 계속 성공 상태이고 남은 시간 초만 변경 된다`() {
+ // given
+ `이메일 요청 결과가 다음과 같을 때`(Result.success("test.com"))
+ `인증 코드 전송 요청 결과가 다음과 같을 때`(Result.success(Unit))
+
+ // when
+ `이메일을 불러오면`()
+ val beforeRemainTime = (vm.uiState.value as? StudentVerificationUiState.Success)?.remainTime
+
+ `인증 코드를 전송하면`()
+
+ // then
+ assertSoftly { softly ->
+ val uiState = vm.uiState.value as? StudentVerificationUiState.Success
+
+ softly.assertThat(vm.uiState.value)
+ .isExactlyInstanceOf(StudentVerificationUiState.Success::class.java)
+ softly.assertThat(uiState?.remainTime).isNotEqualTo(beforeRemainTime)
+ }
+ }
+
+ @Test
+ fun `이메일을 불러오지 않은 상태에서 코드 전송에 성공해도 불러오기 중이다`() {
+ // given
+ `인증 코드 전송 요청 결과가 다음과 같을 때`(Result.success(Unit))
+
+ // when
+ `인증 코드를 전송하면`()
+
+ // then
+ assertThat(vm.uiState.value).isExactlyInstanceOf(StudentVerificationUiState.Loading::class.java)
+ }
+
+ @Test
+ fun `이메일 불러오기 성공일 때 인증 번호가 유효하다면 인증 코드가 유효함 상태로 변경한다`() {
+ // given
+ `이메일 요청 결과가 다음과 같을 때`(Result.success("test.com"))
+
+ // when
+ `이메일을 불러오면`()
+ `인증 코드를 입력하면`("123456")
+
+ // then
+ assertSoftly { softly ->
+ val uiState = vm.uiState.value as? StudentVerificationUiState.Success
+
+ softly.assertThat(vm.uiState.value)
+ .isExactlyInstanceOf(StudentVerificationUiState.Success::class.java)
+ softly.assertThat(uiState?.isValidateCode).isTrue
+ }
+ }
+
+ @Test
+ fun `이메일 불러오기 성공 상태일 때 인증 번호가 유효하지 않다면 인증 코드가 유효하지 않음 상태로 변경한다`() {
+ // given
+ `이메일 요청 결과가 다음과 같을 때`(Result.success("test.com"))
+
+ // when
+ `이메일을 불러오면`()
+ `인증 코드를 입력하면`("1234")
+
+ // then
+ assertSoftly { softly ->
+ val uiState = vm.uiState.value as? StudentVerificationUiState.Success
+
+ softly.assertThat(vm.uiState.value)
+ .isExactlyInstanceOf(StudentVerificationUiState.Success::class.java)
+ softly.assertThat(uiState?.isValidateCode).isFalse
+ }
+ }
+
+ @Test
+ fun `이메일 불러오기와 인증 코드 전송이 성공일 때 인증 번호 확인이 성공하면 인증 성공 이벤트가 발생한다`() = runTest {
+ // given
+ val fakeCode = "123456"
+
+ `이메일 요청 결과가 다음과 같을 때`(Result.success("test.com"))
+ `인증 코드 전송 요청 결과가 다음과 같을 때`(Result.success(Unit))
+ `인증 코드 확인 요청 결과가 다음과 같을 떄`(result = Result.success(Unit), fakeCode = fakeCode)
+
+ // when
+ `이메일을 불러오면`()
+ `인증 코드를 입력하면`(fakeCode)
+ `인증 코드를 전송하면`()
+
+ vm.event.test {
+ // when
+ `인증 코드를 확인하면`()
+
+ // then
+ assertThat(awaitItem()).isEqualTo(StudentVerificationEvent.VerificationSuccess)
+ }
+ }
+
+ @Test
+ fun `이메일 불러오기와 인증 코드 전송이 성공일 때 인증 번호 확인이 실패하면 인증 실패 이벤트가 발생한다`() = runTest {
+ // given
+ val fakeCode = "123456"
+
+ `이메일 요청 결과가 다음과 같을 때`(Result.success("test.com"))
+ `인증 코드 전송 요청 결과가 다음과 같을 때`(Result.success(Unit))
+ `인증 코드 확인 요청 결과가 다음과 같을 떄`(result = Result.failure(Exception()), fakeCode = fakeCode)
+
+ // when
+ `이메일을 불러오면`()
+ `인증 코드를 입력하면`(fakeCode)
+ `인증 코드를 전송하면`()
+
+ vm.event.test {
+ // when
+ `인증 코드를 확인하면`()
+
+ // then
+ assertThat(awaitItem()).isExactlyInstanceOf(StudentVerificationEvent.VerificationFailure::class.java)
+ }
+ }
+
+ @Test
+ fun `이메일 불러오기 성공이고 인증 코드 전송을 하지 않았을 때 타임 아웃 이벤트가 발생한다1`() = runTest {
+ // given
+ val fakeCode = "123456"
+ `이메일 요청 결과가 다음과 같을 때`(Result.success("test.com"))
+ `인증 코드 확인 요청 결과가 다음과 같을 떄`(result = Result.failure(Exception()), fakeCode = fakeCode)
+
+ // when
+ `이메일을 불러오면`()
+ `인증 코드를 입력하면`(fakeCode)
+
+ vm.event.test {
+ // when
+ `인증 코드를 확인하면`()
+
+ // then
+ assertThat(awaitItem()).isEqualTo(StudentVerificationEvent.VerificationTimeOut)
+ }
+ }
+
+ @Test
+ fun `이메일 불러오기 전이면 인증 코드 확인하기 요청을 보내도 이벤트가 발생하지 않는다`() = runTest {
+ // given
+ val fakeCode = "123456"
+ `인증 코드 확인 요청 결과가 다음과 같을 떄`(result = Result.success(Unit), fakeCode = fakeCode)
+
+ // when
+ `인증 코드를 입력하면`(fakeCode)
+
+ vm.event.test {
+ // when
+ `인증 코드를 확인하면`()
+
+ // then
+ expectNoEvents()
+ }
+ }
+}
diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModelTest.kt
index a2e01d3d5..9369a2a87 100644
--- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModelTest.kt
+++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModelTest.kt
@@ -1,23 +1,21 @@
package com.festago.festago.presentation.ui.ticketentry
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.festago.festago.analytics.AnalyticsHelper
+import com.festago.festago.model.Ticket
import com.festago.festago.model.TicketCode
import com.festago.festago.presentation.fixture.TicketFixture
-import com.festago.festago.presentation.mapper.toPresentation
import com.festago.festago.repository.TicketRepository
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.delay
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.assertj.core.api.SoftAssertions
import org.junit.After
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
class TicketEntryViewModelTest {
@@ -25,9 +23,6 @@ class TicketEntryViewModelTest {
private lateinit var ticketRepository: TicketRepository
private lateinit var analyticsHelper: AnalyticsHelper
- @get:Rule
- val instantExecutorRule = InstantTaskExecutorRule()
-
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
@@ -43,36 +38,36 @@ class TicketEntryViewModelTest {
Dispatchers.resetMain()
}
+ private fun `티켓 요쳥 결과는 다음과 같을 때`(result: Result) {
+ coEvery { ticketRepository.loadTicket(any()) } returns result
+ }
+
+ private fun `티켓 코드 요청 결과는 다음과 같을 때`(result: Result) {
+ coEvery { ticketRepository.loadTicketCode(any()) } returns result
+ }
+
@Test
fun `티켓 받아오기에 성공하면 성공 상태이고 티켓 코드와 티켓을 가지고 있다`() {
// given
- coEvery {
- ticketRepository.loadTicket(any())
- } answers {
- Result.success(TicketFixture.getMemberTicket())
- }
-
- coEvery {
- ticketRepository.loadTicketCode(any())
- } answers {
- Result.success(getFakeTicketCode())
- }
+ `티켓 요쳥 결과는 다음과 같을 때`(Result.success(TicketFixture.getMemberTicket()))
+ `티켓 코드 요청 결과는 다음과 같을 때`(Result.success(getFakeTicketCode()))
// when
vm.loadTicket(1L)
+ vm.loadTicketCode(1L)
// then
val softly = SoftAssertions().apply {
assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Success::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(true)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
// and
val actualTicket = (vm.uiState.value as TicketEntryUiState.Success).ticket
- assertThat(actualTicket).isEqualTo(TicketFixture.getMemberTicket().toPresentation())
+ assertThat(actualTicket).isEqualTo(TicketFixture.getMemberTicket())
val actualTicketCode = (vm.uiState.value as TicketEntryUiState.Success).ticketCode
assertThat(actualTicketCode).isEqualTo(getFakeTicketCode())
}
@@ -82,36 +77,71 @@ class TicketEntryViewModelTest {
@Test
fun `티켓 받아오기에 실패하면 에러 상태다`() {
// given
- coEvery {
- ticketRepository.loadTicket(any())
- } answers {
- Result.failure(Exception())
+ `티켓 요쳥 결과는 다음과 같을 때`(Result.failure(Exception()))
+ `티켓 코드 요청 결과는 다음과 같을 때`(Result.success(getFakeTicketCode()))
+
+ // when
+ vm.loadTicket(1L)
+ vm.loadTicketCode(1L)
+
+ // then
+ val softly = SoftAssertions().apply {
+ assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Error::class.java)
+
+ // and
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(true)
}
+ softly.assertAll()
+ }
+
+ @Test
+ fun `티켓 코드 받아오기에 실패하면 에러 상태다`() {
+ // given
+ `티켓 요쳥 결과는 다음과 같을 때`(Result.success(TicketFixture.getMemberTicket()))
+ `티켓 코드 요청 결과는 다음과 같을 때`(Result.failure(Exception()))
// when
vm.loadTicket(1L)
+ vm.loadTicketCode(1L)
// then
val softly = SoftAssertions().apply {
assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Error::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(true)
}
softly.assertAll()
}
@Test
- fun `티켓 받아오는 중이면 로딩 상태다`() {
+ fun `티켓만 받아오면 성공해도 로딩 상태다`() = runTest {
// given
- coEvery {
- ticketRepository.loadTicket(any())
- } coAnswers {
- delay(1000)
- Result.success(TicketFixture.getMemberTicket())
+ `티켓 요쳥 결과는 다음과 같을 때`(Result.success(TicketFixture.getMemberTicket()))
+
+ // when
+ vm.loadTicket(1L)
+
+ // then
+ val softly = SoftAssertions().apply {
+ assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Loading::class.java)
+
+ // and
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
}
+ softly.assertAll()
+ }
+
+ @Test
+ fun `티켓만 받으면 실패해도 로딩 상태다`() {
+ // given
+ `티켓 요쳥 결과는 다음과 같을 때`(Result.failure(Exception()))
// when
vm.loadTicket(1L)
@@ -121,33 +151,49 @@ class TicketEntryViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Loading::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(true)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
}
softly.assertAll()
}
@Test
- fun `티켓 코드를 받아오기에 실패하면 에러 상태다`() {
+ fun `티켓코드만 받아오면 결과가 성공해도 로딩 상태다`() {
// given
- coEvery {
- ticketRepository.loadTicketCode(any())
- } answers {
- Result.failure(Exception())
+ `티켓 코드 요청 결과는 다음과 같을 때`(Result.success(getFakeTicketCode()))
+
+ // when
+ vm.loadTicketCode(1L)
+
+ // then
+ val softly = SoftAssertions().apply {
+ assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Loading::class.java)
+
+ // and
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
}
+ softly.assertAll()
+ }
+
+ @Test
+ fun `티켓 코드만 받으면 결과에 실패해도 로딩 상태다`() {
+ // given
+ `티켓 코드 요청 결과는 다음과 같을 때`(Result.failure(Exception()))
// when
vm.loadTicketCode(1L)
// then
val softly = SoftAssertions().apply {
- assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Error::class.java)
+ assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Loading::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
- assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
+ assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true)
+ assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
}
softly.assertAll()
}
diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModelTest.kt
index 4b0885db7..b9c8f8fec 100644
--- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModelTest.kt
+++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModelTest.kt
@@ -1,9 +1,8 @@
package com.festago.festago.presentation.ui.tickethistory
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.festago.festago.analytics.AnalyticsHelper
+import com.festago.festago.model.Ticket
import com.festago.festago.presentation.fixture.TicketFixture
-import com.festago.festago.presentation.mapper.toPresentation
import com.festago.festago.repository.TicketRepository
import io.mockk.coEvery
import io.mockk.mockk
@@ -16,7 +15,6 @@ import kotlinx.coroutines.test.setMain
import org.assertj.core.api.SoftAssertions
import org.junit.After
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
class TicketHistoryViewModelTest {
@@ -25,9 +23,6 @@ class TicketHistoryViewModelTest {
private lateinit var ticketRepository: TicketRepository
private lateinit var analyticsHelper: AnalyticsHelper
- @get:Rule
- val instantExecutorRule = InstantTaskExecutorRule()
-
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
@@ -47,16 +42,18 @@ class TicketHistoryViewModelTest {
Dispatchers.resetMain()
}
+ private fun `티켓 기록 요청 결과가 다음과 같을 때`(result: Result>) {
+ coEvery {
+ ticketRepository.loadHistoryTickets(any())
+ } returns result
+ }
+
@Test
fun `빈 리스트가 아닌 티켓들을 가져오면 성공 상태이다`() {
// given
val ids = listOf(1L, 2L, 3L, 4L, 5L)
- coEvery {
- ticketRepository.loadHistoryTickets(any())
- } answers {
- Result.success(TicketFixture.getMemberTickets(ids))
- }
+ `티켓 기록 요청 결과가 다음과 같을 때`(Result.success(TicketFixture.getMemberTickets(ids)))
// when
vm.loadTicketHistories()
@@ -68,7 +65,7 @@ class TicketHistoryViewModelTest {
// and
val successUiState = vm.uiState.value as TicketHistoryUiState.Success
assertThat(successUiState.tickets).isEqualTo(
- TicketFixture.getMemberTickets(ids).toPresentation(),
+ TicketFixture.getMemberTickets(ids).map { it.toUiState() },
)
// and
@@ -84,11 +81,7 @@ class TicketHistoryViewModelTest {
@Test
fun `빈 리스트인 티켓을 받아도 성공 상태이다`() {
// given
- coEvery {
- ticketRepository.loadHistoryTickets(any())
- } answers {
- Result.success(emptyList())
- }
+ `티켓 기록 요청 결과가 다음과 같을 때`(Result.success(listOf()))
// when
vm.loadTicketHistories()
@@ -129,10 +122,10 @@ class TicketHistoryViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(TicketHistoryUiState.Loading::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isFalse
- assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isFalse
- assertThat(vm.uiState.value?.shouldShowLoading).isTrue
- assertThat(vm.uiState.value?.shouldShowError).isFalse
+ assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isFalse
+ assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isFalse
+ assertThat(vm.uiState.value.shouldShowLoading).isTrue
+ assertThat(vm.uiState.value.shouldShowError).isFalse
}
softly.assertAll()
@@ -141,11 +134,7 @@ class TicketHistoryViewModelTest {
@Test
fun `티켓을 받아오기 실패하면 에러 상태이다`() {
// given
- coEvery {
- ticketRepository.loadHistoryTickets(any())
- } answers {
- Result.failure(Exception())
- }
+ `티켓 기록 요청 결과가 다음과 같을 때`(Result.failure(Exception()))
// when
vm.loadTicketHistories()
@@ -155,12 +144,22 @@ class TicketHistoryViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(TicketHistoryUiState.Error::class.java)
// and
- assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isFalse
- assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isFalse
- assertThat(vm.uiState.value?.shouldShowLoading).isFalse
- assertThat(vm.uiState.value?.shouldShowError).isTrue
+ assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isFalse
+ assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isFalse
+ assertThat(vm.uiState.value.shouldShowLoading).isFalse
+ assertThat(vm.uiState.value.shouldShowError).isTrue
}
-
softly.assertAll()
}
+
+ private fun Ticket.toUiState(): TicketHistoryItemUiState = TicketHistoryItemUiState(
+ id = id,
+ number = number,
+ entryTime = entryTime,
+ reserveAt = reserveAt,
+ stage = stage,
+ festivalId = festivalTicket.id,
+ festivalName = festivalTicket.name,
+ festivalThumbnail = festivalTicket.thumbnail,
+ )
}
diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModelTest.kt
index d1ac9905f..b6a5582c7 100644
--- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModelTest.kt
+++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModelTest.kt
@@ -1,12 +1,13 @@
package com.festago.festago.presentation.ui.ticketreserve
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import app.cash.turbine.test
import com.festago.festago.analytics.AnalyticsHelper
import com.festago.festago.model.Reservation
import com.festago.festago.model.ReservationStage
import com.festago.festago.model.ReservationTicket
+import com.festago.festago.model.ReservationTickets
import com.festago.festago.model.ReservedTicket
-import com.festago.festago.presentation.mapper.toPresentation
+import com.festago.festago.model.TicketType
import com.festago.festago.repository.AuthRepository
import com.festago.festago.repository.FestivalRepository
import com.festago.festago.repository.ReservationTicketRepository
@@ -17,10 +18,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.assertj.core.api.AssertionsForClassTypes.assertThat
+import org.assertj.core.api.SoftAssertions
+import org.junit.After
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import java.time.LocalDate
import java.time.LocalDateTime
@@ -33,9 +37,11 @@ class TicketReserveViewModelTest {
private lateinit var authRepository: AuthRepository
private lateinit var analyticsHelper: AnalyticsHelper
- private val fakeReservationTickets = listOf(
- ReservationTicket(1, "재학생용", 219, 500),
- ReservationTicket(1, "외부인용", 212, 300),
+ private val fakeReservationTickets = ReservationTickets(
+ listOf(
+ ReservationTicket(1, TicketType.STUDENT, 219, 500),
+ ReservationTicket(1, TicketType.VISITOR, 212, 300),
+ ),
)
private val fakeReservationStage = ReservationStage(
id = 1,
@@ -60,9 +66,6 @@ class TicketReserveViewModelTest {
number = 1,
)
- @get:Rule
- val instantExecutorRule = InstantTaskExecutorRule()
-
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
@@ -81,20 +84,33 @@ class TicketReserveViewModelTest {
)
}
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ private fun `예약 정보 요청 결과가 다음과 같을 때`(result: Result) {
+ coEvery { festivalRepository.loadFestivalDetail(any()) } returns result
+ }
+
+ private fun `인증 여부가 다음과 같을 때`(isSigned: Boolean) {
+ coEvery { authRepository.isSigned } answers { isSigned }
+ }
+
+ private fun `특정 공연의 티켓 타입 요청 결과가 다음과 같을 때`(result: Result) {
+ coEvery { reservationTicketRepository.loadTicketTypes(any()) } returns result
+ }
+
+ private fun `티켓 예약 요청 결과가 다음과 같을 때`(result: Result) {
+ coEvery { ticketRepository.reserveTicket(any()) } returns result
+ }
+
@Test
fun `예약 정보를 불러오면 성공 이벤트가 발생하고 리스트를 반환한다`() {
// given
- coEvery {
- festivalRepository.loadFestivalDetail(0)
- } answers {
- Result.success(fakeReservation)
- }
-
- coEvery {
- authRepository.isSigned
- } answers {
- true
- }
+ `예약 정보 요청 결과가 다음과 같을 때`(Result.success(fakeReservation))
+ `인증 여부가 다음과 같을 때`(true)
// when
vm.loadReservation()
@@ -117,7 +133,7 @@ class TicketReserveViewModelTest {
@Test
fun `예약 정보를 불러오는 것을 실패하면 에러 이벤트가 발생한다`() {
// given
- coEvery { festivalRepository.loadFestivalDetail(0) } returns Result.failure(Exception())
+ `예약 정보 요청 결과가 다음과 같을 때`(Result.failure(Exception()))
// when
vm.loadReservation(0)
@@ -144,41 +160,33 @@ class TicketReserveViewModelTest {
}
@Test
- fun `특정 공연의 티켓 타입을 보여주는 이벤트가 발생하면 해당 공연의 티켓 타입을 보여준다`() {
+ fun `특정 공연의 티켓 타입을 보여주는 이벤트가 발생하면 해당 공연의 티켓 타입을 보여준다`() = runTest {
// given
- coEvery {
- reservationTicketRepository.loadTicketTypes(1)
- } answers {
- Result.success(fakeReservationTickets)
- }
-
- coEvery {
- authRepository.isSigned
- } answers {
- true
+ `특정 공연의 티켓 타입 요청 결과가 다음과 같을 때`(Result.success(fakeReservationTickets))
+ `인증 여부가 다음과 같을 때`(true)
+
+ vm.event.test {
+ // when
+ vm.showTicketTypes(1, LocalDateTime.MIN)
+
+ // then
+ val softly = SoftAssertions().apply {
+ val event = awaitItem()
+ assertThat(event).isExactlyInstanceOf(TicketReserveEvent.ShowTicketTypes::class.java)
+
+ // and
+ val actual = (event as? TicketReserveEvent.ShowTicketTypes)?.tickets
+ assertThat(actual).isEqualTo(fakeReservationTickets.sortedByTicketTypes())
+ }
+ softly.assertAll()
}
-
- // when
- vm.showTicketTypes(1, LocalDateTime.MIN)
-
- // then
- assertThat(vm.event.getValue()).isInstanceOf(TicketReserveEvent.ShowTicketTypes::class.java)
-
- // and
- val event = vm.event.getValue() as TicketReserveEvent.ShowTicketTypes
- assertThat(event.tickets).isEqualTo(fakeReservationTickets.map { it.toPresentation() })
}
@Test
fun `특정 공연의 티켓 타입을 보여주는 것을 실패하면 에러 이벤트가 발생한다`() {
// given
- coEvery { reservationTicketRepository.loadTicketTypes(1) } returns Result.failure(Exception())
-
- coEvery {
- authRepository.isSigned
- } answers {
- true
- }
+ `특정 공연의 티켓 타입 요청 결과가 다음과 같을 때`(Result.failure(Exception()))
+ `인증 여부가 다음과 같을 때`(true)
// when
vm.showTicketTypes(1, LocalDateTime.MIN)
@@ -188,33 +196,34 @@ class TicketReserveViewModelTest {
}
@Test
- fun `티켓 유형을 선택하고 예약하면 예약 성공 이벤트가 발생한다`() {
+ fun `티켓 유형을 선택하고 예약하면 예약 성공 이벤트가 발생한다`() = runTest {
// given
coEvery {
ticketRepository.reserveTicket(any())
} answers {
Result.success(fakeReservedTicket)
}
- // when
- vm.reserveTicket(0)
- // then
- assertThat(vm.event.getValue()).isInstanceOf(TicketReserveEvent.ReserveTicketSuccess::class.java)
+ vm.event.test {
+ // when
+ vm.reserveTicket(0)
+
+ // then
+ assertThat(awaitItem()).isExactlyInstanceOf(TicketReserveEvent.ReserveTicketSuccess::class.java)
+ }
}
@Test
- fun `티켓 유형을 선택하고 예약하는 것을 실패하면 예약 실패 이벤트가 발생한다`() {
+ fun `티켓 유형을 선택하고 예약하는 것을 실패하면 예약 실패 이벤트가 발생한다`() = runTest {
// given
- coEvery {
- ticketRepository.reserveTicket(0)
- } answers {
- Result.failure(Exception())
- }
+ `티켓 예약 요청 결과가 다음과 같을 때`(Result.failure(Exception()))
- // when
- vm.reserveTicket(0)
+ vm.event.test {
+ // when
+ vm.reserveTicket(0)
- // then
- assertThat(vm.event.getValue()).isEqualTo(TicketReserveEvent.ReserveTicketFailed)
+ // then
+ assertThat(awaitItem()).isExactlyInstanceOf(TicketReserveEvent.ReserveTicketFailed::class.java)
+ }
}
}
diff --git a/android/festago/build.gradle.kts b/android/festago/build.gradle.kts
index 4ae1b27c0..649598d35 100644
--- a/android/festago/build.gradle.kts
+++ b/android/festago/build.gradle.kts
@@ -12,4 +12,6 @@ plugins {
id("com.google.gms.google-services") version "4.3.15" apply false
id("com.google.firebase.crashlytics") version "2.9.7" apply false
+
+ id("com.google.dagger.hilt.android") version "2.44" apply false
}
diff --git a/android/festago/domain/src/main/java/com/festago/festago/model/ReservationStage.kt b/android/festago/domain/src/main/java/com/festago/festago/model/ReservationStage.kt
index 4c5bce35f..5c691a009 100644
--- a/android/festago/domain/src/main/java/com/festago/festago/model/ReservationStage.kt
+++ b/android/festago/domain/src/main/java/com/festago/festago/model/ReservationStage.kt
@@ -7,5 +7,5 @@ data class ReservationStage(
val lineUp: String,
val startTime: LocalDateTime,
val ticketOpenTime: LocalDateTime,
- val reservationTickets: List,
+ val reservationTickets: ReservationTickets,
)
diff --git a/android/festago/domain/src/main/java/com/festago/festago/model/ReservationTicket.kt b/android/festago/domain/src/main/java/com/festago/festago/model/ReservationTicket.kt
index 694522079..47bef2d69 100644
--- a/android/festago/domain/src/main/java/com/festago/festago/model/ReservationTicket.kt
+++ b/android/festago/domain/src/main/java/com/festago/festago/model/ReservationTicket.kt
@@ -2,7 +2,7 @@ package com.festago.festago.model
data class ReservationTicket(
val id: Int,
- val ticketType: String,
+ val ticketType: TicketType,
val remainAmount: Int,
val totalAmount: Int,
)
diff --git a/android/festago/domain/src/main/java/com/festago/festago/model/ReservationTickets.kt b/android/festago/domain/src/main/java/com/festago/festago/model/ReservationTickets.kt
new file mode 100644
index 000000000..5afc6e7e3
--- /dev/null
+++ b/android/festago/domain/src/main/java/com/festago/festago/model/ReservationTickets.kt
@@ -0,0 +1,9 @@
+package com.festago.festago.model
+
+class ReservationTickets(private val tickets: List) {
+
+ fun sortedByTicketTypes(): List {
+ val ticketTypes = TicketType.values().toList()
+ return tickets.sortedBy { ticketTypes.indexOf(it.ticketType) }
+ }
+}
diff --git a/android/festago/domain/src/main/java/com/festago/festago/model/School.kt b/android/festago/domain/src/main/java/com/festago/festago/model/School.kt
new file mode 100644
index 000000000..9e467f12c
--- /dev/null
+++ b/android/festago/domain/src/main/java/com/festago/festago/model/School.kt
@@ -0,0 +1,7 @@
+package com.festago.festago.model
+
+data class School(
+ val id: Long,
+ val domain: String,
+ val name: String,
+)
diff --git a/android/festago/domain/src/main/java/com/festago/festago/model/StudentVerificationCode.kt b/android/festago/domain/src/main/java/com/festago/festago/model/StudentVerificationCode.kt
new file mode 100644
index 000000000..5820187b6
--- /dev/null
+++ b/android/festago/domain/src/main/java/com/festago/festago/model/StudentVerificationCode.kt
@@ -0,0 +1,17 @@
+package com.festago.festago.model
+
+@JvmInline
+value class StudentVerificationCode(val value: String) {
+
+ init {
+ require(
+ TextValidator.of(DIGITS, CODE_LENGTH).isValid(value),
+ ) { ERROR_CODE_VALIDATION }
+ }
+
+ companion object {
+ private const val ERROR_CODE_VALIDATION = "[ERROR]: StudentVerificationCode Validation"
+ private const val CODE_LENGTH = 6
+ private const val DIGITS = "0123456789"
+ }
+}
diff --git a/android/festago/domain/src/main/java/com/festago/festago/model/TextValidator.kt b/android/festago/domain/src/main/java/com/festago/festago/model/TextValidator.kt
new file mode 100644
index 000000000..6969e1473
--- /dev/null
+++ b/android/festago/domain/src/main/java/com/festago/festago/model/TextValidator.kt
@@ -0,0 +1,19 @@
+package com.festago.festago.model
+
+class TextValidator private constructor(
+ private val allowedChars: List,
+ private val expectedLength: Int,
+) {
+ fun isValid(text: String): Boolean = isExpectedLength(text) && hasAllowedChars(text)
+
+ private fun isExpectedLength(text: String) = text.length == expectedLength
+
+ private fun hasAllowedChars(text: String) = text.toList().all { it in allowedChars }
+
+ companion object {
+
+ fun of(allowedCharSequence: String, expectedLength: Int): TextValidator {
+ return TextValidator(allowedCharSequence.toList(), expectedLength)
+ }
+ }
+}
diff --git a/android/festago/domain/src/main/java/com/festago/festago/model/TicketType.kt b/android/festago/domain/src/main/java/com/festago/festago/model/TicketType.kt
new file mode 100644
index 000000000..0e61992a2
--- /dev/null
+++ b/android/festago/domain/src/main/java/com/festago/festago/model/TicketType.kt
@@ -0,0 +1,5 @@
+package com.festago.festago.model
+
+enum class TicketType {
+ STUDENT, VISITOR, OTHER
+}
diff --git a/android/festago/domain/src/main/java/com/festago/festago/repository/AuthRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/AuthRepository.kt
index 1ce24a7df..562bd0170 100644
--- a/android/festago/domain/src/main/java/com/festago/festago/repository/AuthRepository.kt
+++ b/android/festago/domain/src/main/java/com/festago/festago/repository/AuthRepository.kt
@@ -1,9 +1,9 @@
package com.festago.festago.repository
interface AuthRepository {
+ var token: String?
val isSigned: Boolean
- val token: String?
- suspend fun signIn(socialType: String, token: String): Result
+ suspend fun signIn(): Result
suspend fun signOut(): Result
suspend fun deleteAccount(): Result
}
diff --git a/android/festago/domain/src/main/java/com/festago/festago/repository/ReservationTicketRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/ReservationTicketRepository.kt
index bef4f3654..89b06568c 100644
--- a/android/festago/domain/src/main/java/com/festago/festago/repository/ReservationTicketRepository.kt
+++ b/android/festago/domain/src/main/java/com/festago/festago/repository/ReservationTicketRepository.kt
@@ -1,7 +1,7 @@
package com.festago.festago.repository
-import com.festago.festago.model.ReservationTicket
+import com.festago.festago.model.ReservationTickets
interface ReservationTicketRepository {
- suspend fun loadTicketTypes(stageId: Int): Result>
+ suspend fun loadTicketTypes(stageId: Int): Result
}
diff --git a/android/festago/domain/src/main/java/com/festago/festago/repository/SchoolRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/SchoolRepository.kt
new file mode 100644
index 000000000..52063fdb6
--- /dev/null
+++ b/android/festago/domain/src/main/java/com/festago/festago/repository/SchoolRepository.kt
@@ -0,0 +1,8 @@
+package com.festago.festago.repository
+
+import com.festago.festago.model.School
+
+interface SchoolRepository {
+ suspend fun loadSchools(): Result>
+ suspend fun loadSchoolEmail(schoolId: Long): Result
+}
diff --git a/android/festago/domain/src/main/java/com/festago/festago/repository/SocialAuthRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/SocialAuthRepository.kt
new file mode 100644
index 000000000..b8047e5f0
--- /dev/null
+++ b/android/festago/domain/src/main/java/com/festago/festago/repository/SocialAuthRepository.kt
@@ -0,0 +1,8 @@
+package com.festago.festago.repository
+
+interface SocialAuthRepository {
+ val socialType: String
+ suspend fun getSocialToken(): Result
+ suspend fun signOut(): Result
+ suspend fun deleteAccount(): Result
+}
diff --git a/android/festago/domain/src/main/java/com/festago/festago/repository/StudentVerificationRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/StudentVerificationRepository.kt
new file mode 100644
index 000000000..46acc9f06
--- /dev/null
+++ b/android/festago/domain/src/main/java/com/festago/festago/repository/StudentVerificationRepository.kt
@@ -0,0 +1,8 @@
+package com.festago.festago.repository
+
+import com.festago.festago.model.StudentVerificationCode
+
+interface StudentVerificationRepository {
+ suspend fun sendVerificationCode(userName: String, schoolId: Long): Result
+ suspend fun requestVerificationCodeConfirm(code: StudentVerificationCode): Result
+}
diff --git a/android/festago/domain/src/main/java/com/festago/festago/repository/TokenRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/TokenRepository.kt
deleted file mode 100644
index bf915f5be..000000000
--- a/android/festago/domain/src/main/java/com/festago/festago/repository/TokenRepository.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.festago.festago.repository
-
-interface TokenRepository {
- var token: String?
- fun refreshToken(token: String): Result
- suspend fun signIn(socialType: String, token: String): Result
-}
diff --git a/android/festago/domain/src/test/java/com/festago/festago/model/StudentVerificationCodeTest.kt b/android/festago/domain/src/test/java/com/festago/festago/model/StudentVerificationCodeTest.kt
new file mode 100644
index 000000000..7b45f7620
--- /dev/null
+++ b/android/festago/domain/src/test/java/com/festago/festago/model/StudentVerificationCodeTest.kt
@@ -0,0 +1,33 @@
+package com.festago.festago.model
+
+import org.junit.jupiter.api.assertDoesNotThrow
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+class StudentVerificationCodeTest {
+
+ @ParameterizedTest
+ @ValueSource(strings = ["000000", "123456", "664325", "194823", "999999"])
+ fun `학생 인증 코드는 6자리 숫자여야 한다`(code: String) {
+ assertDoesNotThrow { StudentVerificationCode(code) }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["1234567", "11", "45567", "0"])
+ fun `학생 인증 코드가 6자리가 아니면 예외가 발생한다 `(code: String) {
+ assertThrows { StudentVerificationCode(code) }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["123456a", "a123456", "asdfeq", "999999a"])
+ fun `학생 인증 코드에 숫자가 아닌 문자가 포함되면 예외가 발생한다 `(code: String) {
+ assertThrows { StudentVerificationCode(code) }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["12345&", "a1#345", "******"])
+ fun `학생 인증 코드가 특수 문자가 포함되면 예외가 발생한다 `(code: String) {
+ assertThrows { StudentVerificationCode(code) }
+ }
+}
diff --git a/android/festago/domain/src/test/java/com/festago/festago/model/TextValidatorTest.kt b/android/festago/domain/src/test/java/com/festago/festago/model/TextValidatorTest.kt
new file mode 100644
index 000000000..c644ed5d0
--- /dev/null
+++ b/android/festago/domain/src/test/java/com/festago/festago/model/TextValidatorTest.kt
@@ -0,0 +1,50 @@
+package com.festago.festago.model
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+class TextValidatorTest {
+
+ @ParameterizedTest
+ @ValueSource(strings = ["asdf", "fkwk", "test"])
+ fun `알파벳만 허용하고 기대 길이가 4 일 때 다음은 검증된 Text 이다`(text: String) {
+ // given
+ val textValidator = TextValidator.of(
+ allowedCharSequence = "abcdefghijklmnopqrstuvwxyz",
+ expectedLength = 4,
+ )
+
+ // when
+ val isValidText = textValidator.isValid(text)
+
+ // then
+ assertThat(isValidText).isTrue
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["12345", "23456", "20382"])
+ fun `숫자만 허용하고 기대 길이가 5 일 때 다음은 검증된 Text 이다`(text: String) {
+ // given
+ val textValidator = TextValidator.of(allowedCharSequence = "0123456789", expectedLength = 5)
+
+ // when
+ val isValidText = textValidator.isValid(text)
+
+ // then
+ assertThat(isValidText).isTrue
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["asd12", "145667", "11"])
+ fun `숫자만 허용하고 기대 길이가 4 일 때 다음은 검증된 Text 가 아니다`(text: String) {
+ // given
+ val textValidator = TextValidator.of(allowedCharSequence = "0123456789", expectedLength = 4)
+
+ // when
+ val isValidText = textValidator.isValid(text)
+
+ // then
+ assertThat(isValidText).isFalse
+ }
+}
diff --git a/backend/build.gradle b/backend/build.gradle
index fc90dff7a..b8d91cef4 100644
--- a/backend/build.gradle
+++ b/backend/build.gradle
@@ -1,6 +1,6 @@
plugins {
id 'java'
- id 'org.springframework.boot' version '3.0.8'
+ id 'org.springframework.boot' version '3.1.4'
id 'io.spring.dependency-management' version '1.1.0'
}
@@ -24,6 +24,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
+ implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
runtimeOnly 'com.h2database:h2'
@@ -40,8 +41,24 @@ dependencies {
// Logback Slack Alarm
implementation "com.github.maricn:logback-slack-appender:1.4.0"
- // Mockito
- testImplementation 'org.mockito:mockito-inline'
+ // Cucumber
+ testImplementation 'io.cucumber:cucumber-java:7.13.0'
+ testImplementation 'io.cucumber:cucumber-spring:7.13.0'
+ testImplementation 'io.cucumber:cucumber-junit-platform-engine:7.13.0'
+ testImplementation 'org.junit.platform:junit-platform-suite:1.8.2'
+
+ // Flyway
+ implementation 'org.flywaydb:flyway-core'
+ implementation 'org.flywaydb:flyway-mysql'
+
+ // Firebase
+ implementation 'com.google.firebase:firebase-admin:8.1.0'
+
+ // Lombok
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+ testCompileOnly 'org.projectlombok:lombok'
+ testAnnotationProcessor 'org.projectlombok:lombok'
}
tasks.named('test') {
diff --git a/backend/docker/docker-compose.yml b/backend/docker/docker-compose.yml
new file mode 100644
index 000000000..03d6579cf
--- /dev/null
+++ b/backend/docker/docker-compose.yml
@@ -0,0 +1,15 @@
+version: "3.8"
+services:
+ db:
+ image: mysql:8.0.33
+ container_name: festago-local-db
+ restart: always
+ ports:
+ - "13306:3306"
+ environment:
+ MYSQL_ROOT_PASSWORD: root
+ MYSQL_DATABASE: festago
+ MYSQL_USER: festago
+ MYSQL_PASSWORD: festago
+ TZ: Asia/Seoul
+ command: [ "mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_general_ci" ]
diff --git a/backend/src/main/java/com/festago/application/AdminService.java b/backend/src/main/java/com/festago/admin/application/AdminService.java
similarity index 68%
rename from backend/src/main/java/com/festago/application/AdminService.java
rename to backend/src/main/java/com/festago/admin/application/AdminService.java
index 20b337d79..6caf46f1a 100644
--- a/backend/src/main/java/com/festago/application/AdminService.java
+++ b/backend/src/main/java/com/festago/admin/application/AdminService.java
@@ -1,50 +1,57 @@
-package com.festago.application;
+package com.festago.admin.application;
-import com.festago.domain.Festival;
-import com.festago.domain.FestivalRepository;
-import com.festago.domain.Stage;
-import com.festago.domain.StageRepository;
-import com.festago.domain.Ticket;
-import com.festago.domain.TicketAmount;
-import com.festago.domain.TicketEntryTime;
-import com.festago.domain.TicketRepository;
-import com.festago.dto.AdminFestivalResponse;
-import com.festago.dto.AdminResponse;
-import com.festago.dto.AdminStageResponse;
-import com.festago.dto.AdminTicketResponse;
+import com.festago.admin.dto.AdminFestivalResponse;
+import com.festago.admin.dto.AdminResponse;
+import com.festago.admin.dto.AdminSchoolResponse;
+import com.festago.admin.dto.AdminStageResponse;
+import com.festago.admin.dto.AdminTicketResponse;
+import com.festago.festival.domain.Festival;
+import com.festago.festival.repository.FestivalRepository;
+import com.festago.school.domain.School;
+import com.festago.school.repository.SchoolRepository;
+import com.festago.stage.domain.Stage;
+import com.festago.stage.repository.StageRepository;
+import com.festago.ticket.domain.Ticket;
+import com.festago.ticket.domain.TicketAmount;
+import com.festago.ticket.domain.TicketEntryTime;
+import com.festago.ticket.repository.TicketRepository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Transactional
@Service
+@RequiredArgsConstructor
public class AdminService {
private final FestivalRepository festivalRepository;
private final StageRepository stageRepository;
private final TicketRepository ticketRepository;
-
- public AdminService(FestivalRepository festivalRepository, StageRepository stageRepository,
- TicketRepository ticketRepository) {
- this.festivalRepository = festivalRepository;
- this.stageRepository = stageRepository;
- this.ticketRepository = ticketRepository;
- }
+ private final SchoolRepository schoolRepository;
@Transactional(readOnly = true)
public AdminResponse getAdminResponse() {
+ List allSchool = schoolRepository.findAll();
List allTicket = ticketRepository.findAll();
List allStage = stageRepository.findAll();
List allFestival = festivalRepository.findAll();
return new AdminResponse(
+ schoolResponses(allSchool),
ticketResponses(allTicket),
stageResponses(allStage),
festivalResponses(allFestival));
}
+ private List schoolResponses(List schools) {
+ return schools.stream()
+ .map(AdminSchoolResponse::from)
+ .toList();
+ }
+
private List ticketResponses(List tickets) {
return tickets.stream()
.map(this::ticketResponse)
@@ -91,6 +98,7 @@ private List festivalResponses(List festivals)
return festivals.stream()
.map(festival -> new AdminFestivalResponse(
festival.getId(),
+ festival.getSchool().getId(),
festival.getName(),
festival.getStartDate(),
festival.getEndDate(),
diff --git a/backend/src/main/java/com/festago/auth/domain/Admin.java b/backend/src/main/java/com/festago/admin/domain/Admin.java
similarity index 61%
rename from backend/src/main/java/com/festago/auth/domain/Admin.java
rename to backend/src/main/java/com/festago/admin/domain/Admin.java
index abf4530c5..a7937444e 100644
--- a/backend/src/main/java/com/festago/auth/domain/Admin.java
+++ b/backend/src/main/java/com/festago/admin/domain/Admin.java
@@ -1,12 +1,26 @@
-package com.festago.auth.domain;
+package com.festago.admin.domain;
+import com.festago.common.domain.BaseTimeEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
@Entity
-public class Admin {
+@Table(
+ uniqueConstraints = {
+ @UniqueConstraint(
+ name = "UNIQUE_USERNAME",
+ columnNames = {"username"}
+ )
+ }
+)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Admin extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -16,9 +30,6 @@ public class Admin {
private String password;
- protected Admin() {
- }
-
public Admin(String username, String password) {
this(null, username, password);
}
diff --git a/backend/src/main/java/com/festago/dto/AdminFestivalResponse.java b/backend/src/main/java/com/festago/admin/dto/AdminFestivalResponse.java
similarity index 77%
rename from backend/src/main/java/com/festago/dto/AdminFestivalResponse.java
rename to backend/src/main/java/com/festago/admin/dto/AdminFestivalResponse.java
index 05b0b7e34..ea59b850e 100644
--- a/backend/src/main/java/com/festago/dto/AdminFestivalResponse.java
+++ b/backend/src/main/java/com/festago/admin/dto/AdminFestivalResponse.java
@@ -1,9 +1,10 @@
-package com.festago.dto;
+package com.festago.admin.dto;
import java.time.LocalDate;
public record AdminFestivalResponse(
Long id,
+ Long schoolId,
String name,
LocalDate startDate,
LocalDate endDate,
diff --git a/backend/src/main/java/com/festago/dto/AdminResponse.java b/backend/src/main/java/com/festago/admin/dto/AdminResponse.java
similarity index 73%
rename from backend/src/main/java/com/festago/dto/AdminResponse.java
rename to backend/src/main/java/com/festago/admin/dto/AdminResponse.java
index 1017c3a34..59c473a7f 100644
--- a/backend/src/main/java/com/festago/dto/AdminResponse.java
+++ b/backend/src/main/java/com/festago/admin/dto/AdminResponse.java
@@ -1,8 +1,9 @@
-package com.festago.dto;
+package com.festago.admin.dto;
import java.util.List;
public record AdminResponse(
+ List adminSchools,
List adminTickets,
List adminStageResponse,
List adminFestivalResponse) {
diff --git a/backend/src/main/java/com/festago/admin/dto/AdminSchoolResponse.java b/backend/src/main/java/com/festago/admin/dto/AdminSchoolResponse.java
new file mode 100644
index 000000000..6a898ba24
--- /dev/null
+++ b/backend/src/main/java/com/festago/admin/dto/AdminSchoolResponse.java
@@ -0,0 +1,17 @@
+package com.festago.admin.dto;
+
+import com.festago.school.domain.School;
+
+public record AdminSchoolResponse(
+ Long id,
+ String domain,
+ String name) {
+
+ public static AdminSchoolResponse from(School school) {
+ return new AdminSchoolResponse(
+ school.getId(),
+ school.getDomain(),
+ school.getName()
+ );
+ }
+}
diff --git a/backend/src/main/java/com/festago/dto/AdminStageResponse.java b/backend/src/main/java/com/festago/admin/dto/AdminStageResponse.java
similarity index 86%
rename from backend/src/main/java/com/festago/dto/AdminStageResponse.java
rename to backend/src/main/java/com/festago/admin/dto/AdminStageResponse.java
index 7b5d09742..ab568d4c2 100644
--- a/backend/src/main/java/com/festago/dto/AdminStageResponse.java
+++ b/backend/src/main/java/com/festago/admin/dto/AdminStageResponse.java
@@ -1,4 +1,4 @@
-package com.festago.dto;
+package com.festago.admin.dto;
import java.time.LocalDateTime;
import java.util.List;
diff --git a/backend/src/main/java/com/festago/dto/AdminTicketResponse.java b/backend/src/main/java/com/festago/admin/dto/AdminTicketResponse.java
similarity index 77%
rename from backend/src/main/java/com/festago/dto/AdminTicketResponse.java
rename to backend/src/main/java/com/festago/admin/dto/AdminTicketResponse.java
index 9f2cdb3c2..88c93754d 100644
--- a/backend/src/main/java/com/festago/dto/AdminTicketResponse.java
+++ b/backend/src/main/java/com/festago/admin/dto/AdminTicketResponse.java
@@ -1,6 +1,6 @@
-package com.festago.dto;
+package com.festago.admin.dto;
-import com.festago.domain.TicketType;
+import com.festago.ticket.domain.TicketType;
import java.time.LocalDateTime;
import java.util.Map;
diff --git a/backend/src/main/java/com/festago/auth/domain/AdminRepository.java b/backend/src/main/java/com/festago/admin/repository/AdminRepository.java
similarity index 77%
rename from backend/src/main/java/com/festago/auth/domain/AdminRepository.java
rename to backend/src/main/java/com/festago/admin/repository/AdminRepository.java
index 399ad36d9..6bb333397 100644
--- a/backend/src/main/java/com/festago/auth/domain/AdminRepository.java
+++ b/backend/src/main/java/com/festago/admin/repository/AdminRepository.java
@@ -1,5 +1,6 @@
-package com.festago.auth.domain;
+package com.festago.admin.repository;
+import com.festago.admin.domain.Admin;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
diff --git a/backend/src/main/java/com/festago/application/EntryService.java b/backend/src/main/java/com/festago/application/EntryService.java
deleted file mode 100644
index e71eeec3e..000000000
--- a/backend/src/main/java/com/festago/application/EntryService.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.festago.application;
-
-import com.festago.domain.EntryCode;
-import com.festago.domain.EntryCodeExtractor;
-import com.festago.domain.EntryCodePayload;
-import com.festago.domain.EntryCodeProvider;
-import com.festago.domain.MemberTicket;
-import com.festago.domain.MemberTicketRepository;
-import com.festago.dto.EntryCodeResponse;
-import com.festago.dto.TicketValidationRequest;
-import com.festago.dto.TicketValidationResponse;
-import com.festago.exception.BadRequestException;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.NotFoundException;
-import java.time.LocalDateTime;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-@Service
-@Transactional
-public class EntryService {
-
- private final EntryCodeProvider entryCodeProvider;
- private final EntryCodeExtractor entryCodeExtractor;
- private final MemberTicketRepository memberTicketRepository;
-
- public EntryService(EntryCodeProvider entryCodeProvider, EntryCodeExtractor entryCodeExtractor,
- MemberTicketRepository memberTicketRepository) {
- this.entryCodeProvider = entryCodeProvider;
- this.entryCodeExtractor = entryCodeExtractor;
- this.memberTicketRepository = memberTicketRepository;
- }
-
- public EntryCodeResponse createEntryCode(Long memberId, Long memberTicketId) {
- MemberTicket memberTicket = findMemberTicket(memberTicketId);
- if (!memberTicket.isOwner(memberId)) {
- throw new BadRequestException(ErrorCode.NOT_MEMBER_TICKET_OWNER);
- }
- if (!memberTicket.canEntry(LocalDateTime.now())) {
- throw new BadRequestException(ErrorCode.NOT_ENTRY_TIME);
- }
- EntryCode entryCode = EntryCode.create(entryCodeProvider, memberTicket);
- return EntryCodeResponse.of(entryCode);
- }
-
- private MemberTicket findMemberTicket(Long memberTicketId) {
- return memberTicketRepository.findById(memberTicketId)
- .orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_TICKET_NOT_FOUND));
- }
-
- public TicketValidationResponse validate(TicketValidationRequest request) {
- EntryCodePayload entryCodePayload = entryCodeExtractor.extract(request.code());
- MemberTicket memberTicket = findMemberTicket(entryCodePayload.getMemberTicketId());
- memberTicket.changeState(entryCodePayload.getEntryState());
- return TicketValidationResponse.from(memberTicket);
- }
-}
diff --git a/backend/src/main/java/com/festago/application/FestivalService.java b/backend/src/main/java/com/festago/application/FestivalService.java
deleted file mode 100644
index 6f2f8e855..000000000
--- a/backend/src/main/java/com/festago/application/FestivalService.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.festago.application;
-
-import static java.util.Comparator.comparing;
-
-import com.festago.domain.Festival;
-import com.festago.domain.FestivalRepository;
-import com.festago.domain.Stage;
-import com.festago.domain.StageRepository;
-import com.festago.dto.FestivalCreateRequest;
-import com.festago.dto.FestivalDetailResponse;
-import com.festago.dto.FestivalResponse;
-import com.festago.dto.FestivalsResponse;
-import com.festago.exception.BadRequestException;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.NotFoundException;
-import java.time.LocalDate;
-import java.util.List;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-@Service
-@Transactional
-public class FestivalService {
-
- private final FestivalRepository festivalRepository;
- private final StageRepository stageRepository;
-
- public FestivalService(FestivalRepository festivalRepository, StageRepository stageRepository) {
- this.festivalRepository = festivalRepository;
- this.stageRepository = stageRepository;
- }
-
- public FestivalResponse create(FestivalCreateRequest request) {
- Festival festival = request.toEntity();
- validate(festival);
- Festival newFestival = festivalRepository.save(festival);
- return FestivalResponse.from(newFestival);
- }
-
- private void validate(Festival festival) {
- if (!festival.canCreate(LocalDate.now())) {
- throw new BadRequestException(ErrorCode.INVALID_FESTIVAL_START_DATE);
- }
- }
-
- @Transactional(readOnly = true)
- public FestivalsResponse findAll() {
- List festivals = festivalRepository.findAll();
- return FestivalsResponse.from(festivals);
- }
-
- @Transactional(readOnly = true)
- public FestivalDetailResponse findDetail(Long festivalId) {
- Festival festival = festivalRepository.findById(festivalId)
- .orElseThrow(() -> new NotFoundException(ErrorCode.FESTIVAL_NOT_FOUND));
- List stages = stageRepository.findAllDetailByFestivalId(festivalId).stream()
- .sorted(comparing(Stage::getStartTime))
- .toList();
- return FestivalDetailResponse.of(festival, stages);
- }
-}
diff --git a/backend/src/main/java/com/festago/application/StageService.java b/backend/src/main/java/com/festago/application/StageService.java
deleted file mode 100644
index 98cdf3cae..000000000
--- a/backend/src/main/java/com/festago/application/StageService.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.festago.application;
-
-import com.festago.domain.Festival;
-import com.festago.domain.FestivalRepository;
-import com.festago.domain.Stage;
-import com.festago.domain.StageRepository;
-import com.festago.dto.StageCreateRequest;
-import com.festago.dto.StageResponse;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.NotFoundException;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-@Service
-@Transactional
-public class StageService {
-
- private final StageRepository stageRepository;
- private final FestivalRepository festivalRepository;
-
- public StageService(StageRepository stageRepository, FestivalRepository festivalRepository) {
- this.stageRepository = stageRepository;
- this.festivalRepository = festivalRepository;
- }
-
- public StageResponse create(StageCreateRequest request) {
- Festival festival = findFestivalById(request.festivalId());
- Stage newStage = stageRepository.save(new Stage(
- request.startTime(),
- request.lineUp(),
- request.ticketOpenTime(),
- festival));
-
- return StageResponse.from(newStage);
- }
-
- private Festival findFestivalById(Long festivalId) {
- return festivalRepository.findById(festivalId)
- .orElseThrow(() -> new NotFoundException(ErrorCode.FESTIVAL_NOT_FOUND));
- }
-}
diff --git a/backend/src/main/java/com/festago/auth/application/AdminAuthService.java b/backend/src/main/java/com/festago/auth/application/AdminAuthService.java
index a5e788238..ff21eea0e 100644
--- a/backend/src/main/java/com/festago/auth/application/AdminAuthService.java
+++ b/backend/src/main/java/com/festago/auth/application/AdminAuthService.java
@@ -1,23 +1,24 @@
package com.festago.auth.application;
-import com.festago.auth.domain.Admin;
-import com.festago.auth.domain.AdminRepository;
+import com.festago.admin.domain.Admin;
+import com.festago.admin.repository.AdminRepository;
import com.festago.auth.domain.AuthPayload;
-import com.festago.auth.domain.AuthProvider;
import com.festago.auth.domain.Role;
import com.festago.auth.dto.AdminLoginRequest;
import com.festago.auth.dto.AdminSignupRequest;
import com.festago.auth.dto.AdminSignupResponse;
-import com.festago.exception.BadRequestException;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.ForbiddenException;
-import com.festago.exception.UnauthorizedException;
+import com.festago.common.exception.BadRequestException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.ForbiddenException;
+import com.festago.common.exception.UnauthorizedException;
import java.util.Objects;
+import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
+@RequiredArgsConstructor
public class AdminAuthService {
private static final String ROOT_ADMIN = "admin";
@@ -25,11 +26,6 @@ public class AdminAuthService {
private final AuthProvider authProvider;
private final AdminRepository adminRepository;
- public AdminAuthService(AuthProvider authProvider, AdminRepository adminRepository) {
- this.authProvider = authProvider;
- this.adminRepository = adminRepository;
- }
-
@Transactional(readOnly = true)
public String login(AdminLoginRequest request) {
Admin admin = findAdmin(request);
diff --git a/backend/src/main/java/com/festago/auth/application/AuthExtractor.java b/backend/src/main/java/com/festago/auth/application/AuthExtractor.java
new file mode 100644
index 000000000..326ae5121
--- /dev/null
+++ b/backend/src/main/java/com/festago/auth/application/AuthExtractor.java
@@ -0,0 +1,8 @@
+package com.festago.auth.application;
+
+import com.festago.auth.domain.AuthPayload;
+
+public interface AuthExtractor {
+
+ AuthPayload extract(String token);
+}
diff --git a/backend/src/main/java/com/festago/auth/application/AuthFacadeService.java b/backend/src/main/java/com/festago/auth/application/AuthFacadeService.java
new file mode 100644
index 000000000..6642e956b
--- /dev/null
+++ b/backend/src/main/java/com/festago/auth/application/AuthFacadeService.java
@@ -0,0 +1,39 @@
+package com.festago.auth.application;
+
+import com.festago.auth.domain.AuthPayload;
+import com.festago.auth.domain.Role;
+import com.festago.auth.domain.SocialType;
+import com.festago.auth.domain.UserInfo;
+import com.festago.auth.dto.LoginMemberDto;
+import com.festago.auth.dto.LoginResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class AuthFacadeService {
+
+ private final AuthService authService;
+ private final OAuth2Clients oAuth2Clients;
+ private final AuthProvider authProvider;
+
+ public LoginResponse login(SocialType socialType, String oAuthToken) {
+ UserInfo userInfo = getUserInfo(socialType, oAuthToken);
+ LoginMemberDto loginMember = authService.login(userInfo);
+ String accessToken = getAccessToken(loginMember.memberId());
+ return LoginResponse.of(accessToken, loginMember);
+ }
+
+ private String getAccessToken(Long memberId) {
+ return authProvider.provide(new AuthPayload(memberId, Role.MEMBER));
+ }
+
+ private UserInfo getUserInfo(SocialType socialType, String oAuthToken) {
+ OAuth2Client oAuth2Client = oAuth2Clients.getClient(socialType);
+ return oAuth2Client.getUserInfo(oAuthToken);
+ }
+
+ public void deleteMember(Long memberId) {
+ authService.deleteMember(memberId);
+ }
+}
diff --git a/backend/src/main/java/com/festago/auth/application/AuthProvider.java b/backend/src/main/java/com/festago/auth/application/AuthProvider.java
new file mode 100644
index 000000000..f4a52c123
--- /dev/null
+++ b/backend/src/main/java/com/festago/auth/application/AuthProvider.java
@@ -0,0 +1,8 @@
+package com.festago.auth.application;
+
+import com.festago.auth.domain.AuthPayload;
+
+public interface AuthProvider {
+
+ String provide(AuthPayload authPayload);
+}
diff --git a/backend/src/main/java/com/festago/auth/application/AuthService.java b/backend/src/main/java/com/festago/auth/application/AuthService.java
index 8668782c8..50c3ae317 100644
--- a/backend/src/main/java/com/festago/auth/application/AuthService.java
+++ b/backend/src/main/java/com/festago/auth/application/AuthService.java
@@ -1,52 +1,34 @@
package com.festago.auth.application;
-import com.festago.auth.domain.AuthPayload;
-import com.festago.auth.domain.AuthProvider;
-import com.festago.auth.domain.OAuth2Client;
-import com.festago.auth.domain.OAuth2Clients;
-import com.festago.auth.domain.Role;
import com.festago.auth.domain.UserInfo;
-import com.festago.auth.dto.LoginRequest;
-import com.festago.auth.dto.LoginResponse;
-import com.festago.domain.Member;
-import com.festago.domain.MemberRepository;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.NotFoundException;
+import com.festago.auth.dto.LoginMemberDto;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.NotFoundException;
+import com.festago.member.domain.Member;
+import com.festago.member.repository.MemberRepository;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
+@RequiredArgsConstructor
+@Slf4j
public class AuthService {
private final MemberRepository memberRepository;
- private final OAuth2Clients oAuth2Clients;
- private final AuthProvider authProvider;
- public AuthService(MemberRepository memberRepository, OAuth2Clients oAuth2Clients,
- AuthProvider authProvider) {
- this.memberRepository = memberRepository;
- this.oAuth2Clients = oAuth2Clients;
- this.authProvider = authProvider;
- }
-
- public LoginResponse login(LoginRequest request) {
- UserInfo userInfo = getUserInfo(request);
- return memberRepository.findBySocialIdAndSocialType(userInfo.socialId(), userInfo.socialType())
- .map(member -> LoginResponse.isExists(getAccessToken(member), member.getNickname()))
- .orElseGet(() -> {
- Member member = signUp(userInfo);
- return LoginResponse.isNew(getAccessToken(member), member.getNickname());
- });
- }
-
- private UserInfo getUserInfo(LoginRequest request) {
- OAuth2Client oAuth2Client = oAuth2Clients.getClient(request.socialType());
- return oAuth2Client.getUserInfo(request.accessToken());
- }
-
- private String getAccessToken(Member member) {
- return authProvider.provide(new AuthPayload(member.getId(), Role.MEMBER));
+ public LoginMemberDto login(UserInfo userInfo) {
+ Optional originMember =
+ memberRepository.findBySocialIdAndSocialType(userInfo.socialId(), userInfo.socialType());
+ if (originMember.isPresent()) {
+ Member member = originMember.get();
+ return LoginMemberDto.isExists(member);
+ }
+ Member newMember = signUp(userInfo);
+ return LoginMemberDto.isNew(newMember);
}
private Member signUp(UserInfo userInfo) {
@@ -56,6 +38,12 @@ private Member signUp(UserInfo userInfo) {
public void deleteMember(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND));
+ logDeleteMember(member);
memberRepository.delete(member);
}
+
+ private void logDeleteMember(Member member) {
+ log.info("[DELETE MEMBER] memberId: {} / socialType: {} / socialId: {}",
+ member.getId(), member.getSocialType(), member.getSocialId());
+ }
}
diff --git a/backend/src/main/java/com/festago/auth/application/OAuth2Client.java b/backend/src/main/java/com/festago/auth/application/OAuth2Client.java
new file mode 100644
index 000000000..1e4e1d378
--- /dev/null
+++ b/backend/src/main/java/com/festago/auth/application/OAuth2Client.java
@@ -0,0 +1,11 @@
+package com.festago.auth.application;
+
+import com.festago.auth.domain.SocialType;
+import com.festago.auth.domain.UserInfo;
+
+public interface OAuth2Client {
+
+ UserInfo getUserInfo(String accessToken);
+
+ SocialType getSocialType();
+}
diff --git a/backend/src/main/java/com/festago/auth/domain/OAuth2Clients.java b/backend/src/main/java/com/festago/auth/application/OAuth2Clients.java
similarity index 86%
rename from backend/src/main/java/com/festago/auth/domain/OAuth2Clients.java
rename to backend/src/main/java/com/festago/auth/application/OAuth2Clients.java
index 59e3bc436..7805c57f0 100644
--- a/backend/src/main/java/com/festago/auth/domain/OAuth2Clients.java
+++ b/backend/src/main/java/com/festago/auth/application/OAuth2Clients.java
@@ -1,8 +1,9 @@
-package com.festago.auth.domain;
+package com.festago.auth.application;
-import com.festago.exception.BadRequestException;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.InternalServerException;
+import com.festago.auth.domain.SocialType;
+import com.festago.common.exception.BadRequestException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.InternalServerException;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
diff --git a/backend/src/main/java/com/festago/auth/domain/TokenExtractor.java b/backend/src/main/java/com/festago/auth/application/TokenExtractor.java
similarity index 81%
rename from backend/src/main/java/com/festago/auth/domain/TokenExtractor.java
rename to backend/src/main/java/com/festago/auth/application/TokenExtractor.java
index 004c60bb6..f9396bd9c 100644
--- a/backend/src/main/java/com/festago/auth/domain/TokenExtractor.java
+++ b/backend/src/main/java/com/festago/auth/application/TokenExtractor.java
@@ -1,4 +1,4 @@
-package com.festago.auth.domain;
+package com.festago.auth.application;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
diff --git a/backend/src/main/java/com/festago/auth/config/AuthConfig.java b/backend/src/main/java/com/festago/auth/config/AuthConfig.java
index ef7bd2daf..5912d12b3 100644
--- a/backend/src/main/java/com/festago/auth/config/AuthConfig.java
+++ b/backend/src/main/java/com/festago/auth/config/AuthConfig.java
@@ -1,9 +1,9 @@
package com.festago.auth.config;
-import com.festago.auth.domain.AuthExtractor;
-import com.festago.auth.domain.AuthProvider;
-import com.festago.auth.domain.OAuth2Client;
-import com.festago.auth.domain.OAuth2Clients;
+import com.festago.auth.application.AuthExtractor;
+import com.festago.auth.application.AuthProvider;
+import com.festago.auth.application.OAuth2Client;
+import com.festago.auth.application.OAuth2Clients;
import com.festago.auth.infrastructure.JwtAuthExtractor;
import com.festago.auth.infrastructure.JwtAuthProvider;
import java.util.List;
diff --git a/backend/src/main/java/com/festago/auth/config/LoginConfig.java b/backend/src/main/java/com/festago/auth/config/LoginConfig.java
index 72439a2eb..5d06fa199 100644
--- a/backend/src/main/java/com/festago/auth/config/LoginConfig.java
+++ b/backend/src/main/java/com/festago/auth/config/LoginConfig.java
@@ -1,14 +1,14 @@
package com.festago.auth.config;
-import com.festago.auth.domain.AuthExtractor;
+import com.festago.auth.application.AuthExtractor;
import com.festago.auth.domain.Role;
import com.festago.auth.infrastructure.CookieTokenExtractor;
import com.festago.auth.infrastructure.HeaderTokenExtractor;
-import com.festago.auth.presentation.AuthInterceptor;
-import com.festago.auth.presentation.AuthenticateContext;
-import com.festago.auth.presentation.RoleArgumentResolver;
-import com.festago.presentation.ErrorLogger;
+import com.festago.presentation.auth.AuthInterceptor;
+import com.festago.presentation.auth.AuthenticateContext;
+import com.festago.presentation.auth.RoleArgumentResolver;
import java.util.List;
+import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
@@ -16,16 +16,12 @@
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
+@RequiredArgsConstructor
public class LoginConfig implements WebMvcConfigurer {
private final AuthExtractor authExtractor;
private final AuthenticateContext authenticateContext;
- public LoginConfig(AuthExtractor authExtractor, AuthenticateContext context) {
- this.authExtractor = authExtractor;
- this.authenticateContext = context;
- }
-
@Override
public void addArgumentResolvers(List resolvers) {
resolvers.add(new RoleArgumentResolver(Role.MEMBER, authenticateContext));
@@ -36,9 +32,9 @@ public void addArgumentResolvers(List resolvers)
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(adminAuthInterceptor())
.addPathPatterns("/admin/**", "/js/admin/**")
- .excludePathPatterns("/admin/login", "/admin/initialize");
+ .excludePathPatterns("/admin/login", "/admin/api/login", "/admin/api/initialize");
registry.addInterceptor(memberAuthInterceptor())
- .addPathPatterns("/member-tickets/**", "/members/**", "/auth/**")
+ .addPathPatterns("/member-tickets/**", "/members/**", "/auth/**", "/students/**")
.excludePathPatterns("/auth/oauth2");
}
@@ -61,9 +57,4 @@ public AuthInterceptor memberAuthInterceptor() {
.role(Role.MEMBER)
.build();
}
-
- @Bean
- public ErrorLogger errorLogger() {
- return new ErrorLogger();
- }
}
diff --git a/backend/src/main/java/com/festago/auth/domain/AuthExtractor.java b/backend/src/main/java/com/festago/auth/domain/AuthExtractor.java
deleted file mode 100644
index a8b4bd2d5..000000000
--- a/backend/src/main/java/com/festago/auth/domain/AuthExtractor.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.festago.auth.domain;
-
-public interface AuthExtractor {
-
- AuthPayload extract(String token);
-}
diff --git a/backend/src/main/java/com/festago/auth/domain/AuthPayload.java b/backend/src/main/java/com/festago/auth/domain/AuthPayload.java
index 0c8f91b12..1b174f74c 100644
--- a/backend/src/main/java/com/festago/auth/domain/AuthPayload.java
+++ b/backend/src/main/java/com/festago/auth/domain/AuthPayload.java
@@ -1,7 +1,7 @@
package com.festago.auth.domain;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.InternalServerException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.InternalServerException;
public class AuthPayload {
diff --git a/backend/src/main/java/com/festago/auth/domain/AuthProvider.java b/backend/src/main/java/com/festago/auth/domain/AuthProvider.java
deleted file mode 100644
index 195dc78c2..000000000
--- a/backend/src/main/java/com/festago/auth/domain/AuthProvider.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.festago.auth.domain;
-
-public interface AuthProvider {
-
- String provide(AuthPayload authPayload);
-}
diff --git a/backend/src/main/java/com/festago/auth/domain/OAuth2Client.java b/backend/src/main/java/com/festago/auth/domain/OAuth2Client.java
deleted file mode 100644
index 39de5dd4f..000000000
--- a/backend/src/main/java/com/festago/auth/domain/OAuth2Client.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.festago.auth.domain;
-
-public interface OAuth2Client {
-
- UserInfo getUserInfo(String accessToken);
-
- SocialType getSocialType();
-}
diff --git a/backend/src/main/java/com/festago/auth/domain/Role.java b/backend/src/main/java/com/festago/auth/domain/Role.java
index 28e1924dd..69936580b 100644
--- a/backend/src/main/java/com/festago/auth/domain/Role.java
+++ b/backend/src/main/java/com/festago/auth/domain/Role.java
@@ -3,8 +3,8 @@
import com.festago.auth.annotation.Admin;
import com.festago.auth.annotation.Anonymous;
import com.festago.auth.annotation.Member;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.InternalServerException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.InternalServerException;
import java.lang.annotation.Annotation;
public enum Role {
diff --git a/backend/src/main/java/com/festago/auth/domain/UserInfo.java b/backend/src/main/java/com/festago/auth/domain/UserInfo.java
index 0d36f66f8..3df1d31be 100644
--- a/backend/src/main/java/com/festago/auth/domain/UserInfo.java
+++ b/backend/src/main/java/com/festago/auth/domain/UserInfo.java
@@ -1,6 +1,6 @@
package com.festago.auth.domain;
-import com.festago.domain.Member;
+import com.festago.member.domain.Member;
public record UserInfo(
String socialId,
diff --git a/backend/src/main/java/com/festago/auth/dto/AdminLoginRequest.java b/backend/src/main/java/com/festago/auth/dto/AdminLoginRequest.java
index 1b71a7e8e..313977f5c 100644
--- a/backend/src/main/java/com/festago/auth/dto/AdminLoginRequest.java
+++ b/backend/src/main/java/com/festago/auth/dto/AdminLoginRequest.java
@@ -1,7 +1,11 @@
package com.festago.auth.dto;
+import jakarta.validation.constraints.NotBlank;
+
public record AdminLoginRequest(
+ @NotBlank(message = "username은 공백일 수 없습니다.")
String username,
+ @NotBlank(message = "password는 공백일 수 없습니다.")
String password
) {
diff --git a/backend/src/main/java/com/festago/auth/dto/AdminSignupRequest.java b/backend/src/main/java/com/festago/auth/dto/AdminSignupRequest.java
index b35cf7337..02c689d9f 100644
--- a/backend/src/main/java/com/festago/auth/dto/AdminSignupRequest.java
+++ b/backend/src/main/java/com/festago/auth/dto/AdminSignupRequest.java
@@ -1,7 +1,11 @@
package com.festago.auth.dto;
+import jakarta.validation.constraints.NotBlank;
+
public record AdminSignupRequest(
+ @NotBlank(message = "username은 공백일 수 없습니다.")
String username,
+ @NotBlank(message = "password는 공백일 수 없습니다.")
String password
) {
diff --git a/backend/src/main/java/com/festago/auth/dto/LoginMemberDto.java b/backend/src/main/java/com/festago/auth/dto/LoginMemberDto.java
new file mode 100644
index 000000000..5a921bffb
--- /dev/null
+++ b/backend/src/main/java/com/festago/auth/dto/LoginMemberDto.java
@@ -0,0 +1,18 @@
+package com.festago.auth.dto;
+
+import com.festago.member.domain.Member;
+
+public record LoginMemberDto(
+ boolean isNew,
+ Long memberId,
+ String nickname
+) {
+
+ public static LoginMemberDto isNew(Member member) {
+ return new LoginMemberDto(true, member.getId(), member.getNickname());
+ }
+
+ public static LoginMemberDto isExists(Member member) {
+ return new LoginMemberDto(false, member.getId(), member.getNickname());
+ }
+}
diff --git a/backend/src/main/java/com/festago/auth/dto/LoginRequest.java b/backend/src/main/java/com/festago/auth/dto/LoginRequest.java
index f34f17bed..5872e77e1 100644
--- a/backend/src/main/java/com/festago/auth/dto/LoginRequest.java
+++ b/backend/src/main/java/com/festago/auth/dto/LoginRequest.java
@@ -1,7 +1,15 @@
package com.festago.auth.dto;
import com.festago.auth.domain.SocialType;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
-public record LoginRequest(SocialType socialType, String accessToken) {
+public record LoginRequest(
+ @NotNull(message = "socialType은 null 일 수 없습니다.")
+ SocialType socialType,
+ @NotBlank(message = "acessToken은 공백일 수 없습니다.")
+ String accessToken,
+ @NotBlank(message = "fcmToken은 공백일 수 없습니다.")
+ String fcmToken) {
}
diff --git a/backend/src/main/java/com/festago/auth/dto/LoginResponse.java b/backend/src/main/java/com/festago/auth/dto/LoginResponse.java
index 00acc78cf..7ae5a060a 100644
--- a/backend/src/main/java/com/festago/auth/dto/LoginResponse.java
+++ b/backend/src/main/java/com/festago/auth/dto/LoginResponse.java
@@ -6,11 +6,7 @@ public record LoginResponse(
boolean isNew
) {
- public static LoginResponse isNew(String accessToken, String nickname) {
- return new LoginResponse(accessToken, nickname, true);
- }
-
- public static LoginResponse isExists(String accessToken, String nickname) {
- return new LoginResponse(accessToken, nickname, false);
+ public static LoginResponse of(String accessToken, LoginMemberDto loginMember) {
+ return new LoginResponse(accessToken, loginMember.nickname(), loginMember.isNew());
}
}
diff --git a/backend/src/main/java/com/festago/auth/dto/RootAdminInitializeRequest.java b/backend/src/main/java/com/festago/auth/dto/RootAdminInitializeRequest.java
index e1d408d89..1f76a2921 100644
--- a/backend/src/main/java/com/festago/auth/dto/RootAdminInitializeRequest.java
+++ b/backend/src/main/java/com/festago/auth/dto/RootAdminInitializeRequest.java
@@ -1,6 +1,9 @@
package com.festago.auth.dto;
+import jakarta.validation.constraints.NotBlank;
+
public record RootAdminInitializeRequest(
+ @NotBlank(message = "password는 공백일 수 없습니다.")
String password
) {
diff --git a/backend/src/main/java/com/festago/auth/infrastructure/CookieTokenExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/CookieTokenExtractor.java
index 1e8f03a41..e3d490374 100644
--- a/backend/src/main/java/com/festago/auth/infrastructure/CookieTokenExtractor.java
+++ b/backend/src/main/java/com/festago/auth/infrastructure/CookieTokenExtractor.java
@@ -1,6 +1,6 @@
package com.festago.auth.infrastructure;
-import com.festago.auth.domain.TokenExtractor;
+import com.festago.auth.application.TokenExtractor;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Objects;
diff --git a/backend/src/main/java/com/festago/auth/infrastructure/FestagoOAuth2Client.java b/backend/src/main/java/com/festago/auth/infrastructure/FestagoOAuth2Client.java
index 2012371f4..b46f74a3a 100644
--- a/backend/src/main/java/com/festago/auth/infrastructure/FestagoOAuth2Client.java
+++ b/backend/src/main/java/com/festago/auth/infrastructure/FestagoOAuth2Client.java
@@ -1,10 +1,10 @@
package com.festago.auth.infrastructure;
-import com.festago.auth.domain.OAuth2Client;
+import com.festago.auth.application.OAuth2Client;
import com.festago.auth.domain.SocialType;
import com.festago.auth.domain.UserInfo;
-import com.festago.exception.BadRequestException;
-import com.festago.exception.ErrorCode;
+import com.festago.common.exception.BadRequestException;
+import com.festago.common.exception.ErrorCode;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
diff --git a/backend/src/main/java/com/festago/auth/infrastructure/HeaderTokenExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/HeaderTokenExtractor.java
index f0e4f6b09..f0eab8f78 100644
--- a/backend/src/main/java/com/festago/auth/infrastructure/HeaderTokenExtractor.java
+++ b/backend/src/main/java/com/festago/auth/infrastructure/HeaderTokenExtractor.java
@@ -1,8 +1,8 @@
package com.festago.auth.infrastructure;
-import com.festago.auth.domain.TokenExtractor;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.UnauthorizedException;
+import com.festago.auth.application.TokenExtractor;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.UnauthorizedException;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.springframework.http.HttpHeaders;
diff --git a/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthExtractor.java
index 1ee63953d..5eef34745 100644
--- a/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthExtractor.java
+++ b/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthExtractor.java
@@ -1,10 +1,10 @@
package com.festago.auth.infrastructure;
-import com.festago.auth.domain.AuthExtractor;
+import com.festago.auth.application.AuthExtractor;
import com.festago.auth.domain.AuthPayload;
import com.festago.auth.domain.Role;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.UnauthorizedException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.UnauthorizedException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
diff --git a/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthProvider.java b/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthProvider.java
index 7e06132c3..77ce6bda1 100644
--- a/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthProvider.java
+++ b/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthProvider.java
@@ -1,7 +1,7 @@
package com.festago.auth.infrastructure;
+import com.festago.auth.application.AuthProvider;
import com.festago.auth.domain.AuthPayload;
-import com.festago.auth.domain.AuthProvider;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
diff --git a/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2Client.java b/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2Client.java
index 8a6436b2b..b89eda3c0 100644
--- a/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2Client.java
+++ b/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2Client.java
@@ -1,6 +1,6 @@
package com.festago.auth.infrastructure;
-import com.festago.auth.domain.OAuth2Client;
+import com.festago.auth.application.OAuth2Client;
import com.festago.auth.domain.SocialType;
import com.festago.auth.domain.UserInfo;
import org.springframework.stereotype.Component;
diff --git a/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2UserInfoErrorHandler.java b/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2UserInfoErrorHandler.java
index 4860bc89c..044f4d408 100644
--- a/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2UserInfoErrorHandler.java
+++ b/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2UserInfoErrorHandler.java
@@ -1,8 +1,8 @@
package com.festago.auth.infrastructure;
-import com.festago.exception.BadRequestException;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.InternalServerException;
+import com.festago.common.exception.BadRequestException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.InternalServerException;
import java.io.IOException;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpResponse;
diff --git a/backend/src/main/java/com/festago/aop/LogRequestBody.java b/backend/src/main/java/com/festago/common/aop/LogRequestBody.java
similarity index 92%
rename from backend/src/main/java/com/festago/aop/LogRequestBody.java
rename to backend/src/main/java/com/festago/common/aop/LogRequestBody.java
index 005c4d2ae..372190b58 100644
--- a/backend/src/main/java/com/festago/aop/LogRequestBody.java
+++ b/backend/src/main/java/com/festago/common/aop/LogRequestBody.java
@@ -1,4 +1,4 @@
-package com.festago.aop;
+package com.festago.common.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
diff --git a/backend/src/main/java/com/festago/aop/LogRequestBodyAspect.java b/backend/src/main/java/com/festago/common/aop/LogRequestBodyAspect.java
similarity index 76%
rename from backend/src/main/java/com/festago/aop/LogRequestBodyAspect.java
rename to backend/src/main/java/com/festago/common/aop/LogRequestBodyAspect.java
index c596d21cd..e00ad0e9e 100644
--- a/backend/src/main/java/com/festago/aop/LogRequestBodyAspect.java
+++ b/backend/src/main/java/com/festago/common/aop/LogRequestBodyAspect.java
@@ -1,17 +1,20 @@
-package com.festago.aop;
+package com.festago.common.aop;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.InternalServerException;
-import com.festago.presentation.ErrorLogger;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.InternalServerException;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Method;
+import java.util.EnumMap;
+import java.util.Map;
import java.util.Objects;
+import java.util.function.BiConsumer;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
import org.slf4j.event.Level;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
@@ -24,14 +27,18 @@
public class LogRequestBodyAspect {
private static final long MAX_CONTENT_LENGTH = 1024;
- private static final String LOG_FORMAT = "[REQUEST BODY]\n{}";
+ private static final String LOG_FORMAT = "\n[REQUEST BODY]\n{}";
- private final ErrorLogger errorLogger;
+ private final Map> loggerMap = new EnumMap<>(Level.class);
private final ObjectMapper objectMapper;
+ private final Logger errorLogger;
- public LogRequestBodyAspect(ErrorLogger errorLogger, ObjectMapper objectMapper) {
- this.errorLogger = errorLogger;
+ public LogRequestBodyAspect(ObjectMapper objectMapper, Logger errorLogger) {
this.objectMapper = objectMapper;
+ this.errorLogger = errorLogger;
+ loggerMap.put(Level.INFO, this.errorLogger::info);
+ loggerMap.put(Level.WARN, this.errorLogger::warn);
+ loggerMap.put(Level.ERROR, this.errorLogger::error);
}
@Around("@annotation(LogRequestBody)")
@@ -69,8 +76,9 @@ private boolean validateRequest(HttpServletRequest request) {
}
private void log(Level level, HttpServletRequest request) {
- errorLogger.get(level)
- .log(LOG_FORMAT, getRequestPayload(request));
+ loggerMap.getOrDefault(level, (ignore1, ignore2) -> {
+ })
+ .accept(LOG_FORMAT, getRequestPayload(request));
}
private String getRequestPayload(HttpServletRequest request) {
diff --git a/backend/src/main/java/com/festago/domain/BaseTimeEntity.java b/backend/src/main/java/com/festago/common/domain/BaseTimeEntity.java
similarity index 95%
rename from backend/src/main/java/com/festago/domain/BaseTimeEntity.java
rename to backend/src/main/java/com/festago/common/domain/BaseTimeEntity.java
index 82c446193..7a8621f42 100644
--- a/backend/src/main/java/com/festago/domain/BaseTimeEntity.java
+++ b/backend/src/main/java/com/festago/common/domain/BaseTimeEntity.java
@@ -1,4 +1,4 @@
-package com.festago.domain;
+package com.festago.common.domain;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
diff --git a/backend/src/main/java/com/festago/exception/BadRequestException.java b/backend/src/main/java/com/festago/common/exception/BadRequestException.java
similarity index 79%
rename from backend/src/main/java/com/festago/exception/BadRequestException.java
rename to backend/src/main/java/com/festago/common/exception/BadRequestException.java
index 13ac36288..cd4372162 100644
--- a/backend/src/main/java/com/festago/exception/BadRequestException.java
+++ b/backend/src/main/java/com/festago/common/exception/BadRequestException.java
@@ -1,4 +1,4 @@
-package com.festago.exception;
+package com.festago.common.exception;
public class BadRequestException extends FestaGoException {
diff --git a/backend/src/main/java/com/festago/exception/ErrorCode.java b/backend/src/main/java/com/festago/common/exception/ErrorCode.java
similarity index 67%
rename from backend/src/main/java/com/festago/exception/ErrorCode.java
rename to backend/src/main/java/com/festago/common/exception/ErrorCode.java
index cefd692af..9c57b4cab 100644
--- a/backend/src/main/java/com/festago/exception/ErrorCode.java
+++ b/backend/src/main/java/com/festago/common/exception/ErrorCode.java
@@ -1,25 +1,35 @@
-package com.festago.exception;
+package com.festago.common.exception;
public enum ErrorCode {
// 400
+ INVALID_REQUEST_ARGUMENT("잘못된 요청입니다."),
NOT_MEMBER_TICKET_OWNER("해당 예매 티켓의 주인이 아닙니다."),
NOT_ENTRY_TIME("입장 가능한 시간이 아닙니다."),
EXPIRED_ENTRY_CODE("만료된 입장 코드입니다."),
INVALID_ENTRY_CODE("올바르지 않은 입장코드입니다."),
- INVALID_TICKET_OPEN_TIME("티켓은 공연 시작 전에 오픈되어야 합니다."),
+ INVALID_TICKET_OPEN_TIME("티켓 오픈 시간은 공연 시작 이전 이어야 합니다."),
INVALID_STAGE_START_TIME("공연은 축제 기간 중에만 진행될 수 있습니다."),
INVALID_MIN_TICKET_AMOUNT("티켓은 적어도 한장 이상 발급해야합니다."),
LATE_TICKET_ENTRY_TIME("입장 시간은 공연 시간보다 빨라야합니다."),
EARLY_TICKET_ENTRY_TIME("입장 시간은 공연 시작 12시간 이내여야 합니다."),
EARLY_TICKET_ENTRY_THAN_OPEN("입장 시간은 티켓 오픈 시간 이후여야합니다."),
TICKET_SOLD_OUT("매진된 티켓입니다."),
- INVALID_FESTIVAL_START_DATE("축제 시작 일자는 과거일 수 없습니다."),
- INVALID_FESTIVAL_DURATION("축제 시작 일자는 종료일자 이전이어야합니다."),
- INVALID_TICKET_CREATE_TIME("티켓 예매 시작 후 새롭게 티켓을 발급할 수 없습니다."),
+ INVALID_FESTIVAL_DURATION("축제 시작 일은 종료일 이전이어야 합니다."),
+ INVALID_FESTIVAL_START_DATE("축제 시작 일은 과거일 수 없습니다."),
+ INVALID_TICKET_CREATE_TIME("티켓 오픈 시간 이후 새롭게 티켓을 발급할 수 없습니다."),
OAUTH2_NOT_SUPPORTED_SOCIAL_TYPE("해당 OAuth2 제공자는 지원되지 않습니다."),
RESERVE_TICKET_OVER_AMOUNT("예매 가능한 수량을 초과했습니다."),
+ NEED_STUDENT_VERIFICATION("학생 인증이 필요합니다."),
OAUTH2_INVALID_TOKEN("잘못된 OAuth2 토큰입니다."),
+ ALREADY_STUDENT_VERIFIED("이미 학교 인증이 완료된 사용자입니다."),
+ DUPLICATE_STUDENT_EMAIL("이미 인증된 이메일입니다."),
TICKET_CANNOT_RESERVE_STAGE_START("공연의 시작 시간 이후로 예매할 수 없습니다."),
+ INVALID_STUDENT_VERIFICATION_CODE("올바르지 않은 학생 인증 코드입니다."),
+ DELETE_CONSTRAINT_FESTIVAL("공연이 등록된 축제는 삭제할 수 없습니다."),
+ DELETE_CONSTRAINT_STAGE("티켓이 등록된 공연은 삭제할 수 없습니다."),
+ DELETE_CONSTRAINT_SCHOOL("학생 또는 축제에 등록된 학교는 삭제할 수 없습니다."),
+ DUPLICATE_SCHOOL("이미 존재하는 학교 정보입니다."),
+
// 401
EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."),
@@ -38,6 +48,10 @@ public enum ErrorCode {
STAGE_NOT_FOUND("존재하지 않은 공연입니다."),
FESTIVAL_NOT_FOUND("존재하지 않는 축제입니다."),
TICKET_NOT_FOUND("존재하지 않는 티켓입니다."),
+ SCHOOL_NOT_FOUND("존재하지 않는 학교입니다."),
+
+ // 429
+ TOO_FREQUENT_REQUESTS("너무 잦은 요청입니다. 잠시 후 다시 시도해주세요."),
// 500
INTERNAL_SERVER_ERROR("서버 내부에 문제가 발생했습니다."),
@@ -51,7 +65,8 @@ public enum ErrorCode {
INVALID_ENTRY_CODE_OFFSET("올바르지 않은 입장코드 오프셋입니다."),
INVALID_ROLE_NAME("해당하는 Role이 없습니다."),
FOR_TEST_ERROR("테스트용 에러입니다."),
- ;
+ FAIL_SEND_FCM_MESSAGE("FCM Message 전송에 실패했습니다."),
+ FCM_NOT_FOUND("유효하지 않은 MemberFCM 이 감지 되었습니다.");
private final String message;
diff --git a/backend/src/main/java/com/festago/exception/FestaGoException.java b/backend/src/main/java/com/festago/common/exception/FestaGoException.java
similarity index 93%
rename from backend/src/main/java/com/festago/exception/FestaGoException.java
rename to backend/src/main/java/com/festago/common/exception/FestaGoException.java
index 1a9ad0fa0..4bbdab694 100644
--- a/backend/src/main/java/com/festago/exception/FestaGoException.java
+++ b/backend/src/main/java/com/festago/common/exception/FestaGoException.java
@@ -1,4 +1,4 @@
-package com.festago.exception;
+package com.festago.common.exception;
import org.springframework.core.NestedRuntimeException;
diff --git a/backend/src/main/java/com/festago/exception/ForbiddenException.java b/backend/src/main/java/com/festago/common/exception/ForbiddenException.java
similarity index 79%
rename from backend/src/main/java/com/festago/exception/ForbiddenException.java
rename to backend/src/main/java/com/festago/common/exception/ForbiddenException.java
index b1b9f400e..e7b08812b 100644
--- a/backend/src/main/java/com/festago/exception/ForbiddenException.java
+++ b/backend/src/main/java/com/festago/common/exception/ForbiddenException.java
@@ -1,4 +1,4 @@
-package com.festago.exception;
+package com.festago.common.exception;
public class ForbiddenException extends FestaGoException {
diff --git a/backend/src/main/java/com/festago/exception/InternalServerException.java b/backend/src/main/java/com/festago/common/exception/InternalServerException.java
similarity index 87%
rename from backend/src/main/java/com/festago/exception/InternalServerException.java
rename to backend/src/main/java/com/festago/common/exception/InternalServerException.java
index d9cf8f4aa..0fbf320f5 100644
--- a/backend/src/main/java/com/festago/exception/InternalServerException.java
+++ b/backend/src/main/java/com/festago/common/exception/InternalServerException.java
@@ -1,4 +1,4 @@
-package com.festago.exception;
+package com.festago.common.exception;
public class InternalServerException extends FestaGoException {
diff --git a/backend/src/main/java/com/festago/exception/NotFoundException.java b/backend/src/main/java/com/festago/common/exception/NotFoundException.java
similarity index 79%
rename from backend/src/main/java/com/festago/exception/NotFoundException.java
rename to backend/src/main/java/com/festago/common/exception/NotFoundException.java
index 6fc8777ca..4600daf49 100644
--- a/backend/src/main/java/com/festago/exception/NotFoundException.java
+++ b/backend/src/main/java/com/festago/common/exception/NotFoundException.java
@@ -1,4 +1,4 @@
-package com.festago.exception;
+package com.festago.common.exception;
public class NotFoundException extends FestaGoException {
diff --git a/backend/src/main/java/com/festago/common/exception/TooManyRequestException.java b/backend/src/main/java/com/festago/common/exception/TooManyRequestException.java
new file mode 100644
index 000000000..5c02f7955
--- /dev/null
+++ b/backend/src/main/java/com/festago/common/exception/TooManyRequestException.java
@@ -0,0 +1,8 @@
+package com.festago.common.exception;
+
+public class TooManyRequestException extends FestaGoException {
+
+ public TooManyRequestException(ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/backend/src/main/java/com/festago/exception/UnauthorizedException.java b/backend/src/main/java/com/festago/common/exception/UnauthorizedException.java
similarity index 80%
rename from backend/src/main/java/com/festago/exception/UnauthorizedException.java
rename to backend/src/main/java/com/festago/common/exception/UnauthorizedException.java
index 55935b1fd..ffef316c0 100644
--- a/backend/src/main/java/com/festago/exception/UnauthorizedException.java
+++ b/backend/src/main/java/com/festago/common/exception/UnauthorizedException.java
@@ -1,4 +1,4 @@
-package com.festago.exception;
+package com.festago.common.exception;
public class UnauthorizedException extends FestaGoException {
diff --git a/backend/src/main/java/com/festago/common/exception/dto/ErrorResponse.java b/backend/src/main/java/com/festago/common/exception/dto/ErrorResponse.java
new file mode 100644
index 000000000..a15d330fc
--- /dev/null
+++ b/backend/src/main/java/com/festago/common/exception/dto/ErrorResponse.java
@@ -0,0 +1,34 @@
+package com.festago.common.exception.dto;
+
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.FestaGoException;
+import java.util.List;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+
+public record ErrorResponse(
+ ErrorCode errorCode,
+ String message
+) {
+
+ private static final String NOT_CUSTOM_EXCEPTION = "Validation failed";
+
+ public static ErrorResponse from(FestaGoException festaGoException) {
+ return ErrorResponse.from(festaGoException.getErrorCode());
+ }
+
+ public static ErrorResponse from(ErrorCode errorCode) {
+ return new ErrorResponse(errorCode, errorCode.getMessage());
+ }
+
+ public static ErrorResponse from(ErrorCode errorCode, MethodArgumentNotValidException e) {
+ List fieldErrors = e.getBindingResult().getFieldErrors();
+ if (fieldErrors.isEmpty()) {
+ return new ErrorResponse(errorCode, errorCode.getMessage());
+ }
+ if (e.getMessage().startsWith(NOT_CUSTOM_EXCEPTION)) {
+ return new ErrorResponse(errorCode, fieldErrors.get(0).getDefaultMessage());
+ }
+ return new ErrorResponse(errorCode, e.getMessage());
+ }
+}
diff --git a/backend/src/main/java/com/festago/common/util/Validator.java b/backend/src/main/java/com/festago/common/util/Validator.java
new file mode 100644
index 000000000..481a29730
--- /dev/null
+++ b/backend/src/main/java/com/festago/common/util/Validator.java
@@ -0,0 +1,49 @@
+package com.festago.common.util;
+
+public final class Validator {
+
+ private Validator() {
+ }
+
+ /**
+ * 문자열의 최대 길이를 검증합니다. null 값은 무시됩니다. 최대 길이가 0 이하이면 예외를 던집니다. 문자열의 길이가 maxLength보다 작거나 같으면 예외를 던지지 않습니다.
+ *
+ * @param input 검증할 문자열
+ * @param maxLength 검증할 문자열의 최대 길이
+ * @param message 예외 메시지
+ * @throws IllegalArgumentException 문자열의 길이가 초과되거나, 최대 길이가 0 이하이면
+ */
+ public static void maxLength(CharSequence input, int maxLength, String message) {
+ if (maxLength <= 0) {
+ throw new IllegalArgumentException("검증 길이는 0보다 커야합니다.");
+ }
+ // avoid NPE
+ if (input == null) {
+ return;
+ }
+ if (input.length() > maxLength) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * 문자열의 최소 길이를 검증합니다. null 값은 무시됩니다. 최소 길이가 0 이하이면 예외를 던집니다. 문자열의 길이가 minLength보다 크거나 같으면 예외를 던지지 않습니다.
+ *
+ * @param input 검증할 문자열
+ * @param minLength 검증할 문자열의 최소 길이
+ * @param message 예외 메시지
+ * @throws IllegalArgumentException 문자열의 길이가 작으면, 최대 길이가 0 이하이면
+ */
+ public static void minLength(CharSequence input, int minLength, String message) {
+ if (minLength <= 0) {
+ throw new IllegalArgumentException("검증 길이는 0보다 커야합니다.");
+ }
+ // avoid NPE
+ if (input == null) {
+ return;
+ }
+ if (input.length() < minLength) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+}
diff --git a/backend/src/main/java/com/festago/config/AsyncConfig.java b/backend/src/main/java/com/festago/config/AsyncConfig.java
new file mode 100644
index 000000000..fbf428a9b
--- /dev/null
+++ b/backend/src/main/java/com/festago/config/AsyncConfig.java
@@ -0,0 +1,10 @@
+package com.festago.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+@EnableAsync
+@Configuration
+public class AsyncConfig {
+
+}
diff --git a/backend/src/main/java/com/festago/config/ErrorLoggerConfig.java b/backend/src/main/java/com/festago/config/ErrorLoggerConfig.java
new file mode 100644
index 000000000..d0ec785df
--- /dev/null
+++ b/backend/src/main/java/com/festago/config/ErrorLoggerConfig.java
@@ -0,0 +1,15 @@
+package com.festago.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ErrorLoggerConfig {
+
+ @Bean
+ public Logger errorLogger() {
+ return LoggerFactory.getLogger("ErrorLogger");
+ }
+}
diff --git a/backend/src/main/java/com/festago/domain/EntryCodeExtractor.java b/backend/src/main/java/com/festago/domain/EntryCodeExtractor.java
deleted file mode 100644
index 429df79ad..000000000
--- a/backend/src/main/java/com/festago/domain/EntryCodeExtractor.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.festago.domain;
-
-public interface EntryCodeExtractor {
-
- EntryCodePayload extract(String code);
-}
diff --git a/backend/src/main/java/com/festago/domain/Festival.java b/backend/src/main/java/com/festago/domain/Festival.java
deleted file mode 100644
index 088c4d080..000000000
--- a/backend/src/main/java/com/festago/domain/Festival.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.festago.domain;
-
-import com.festago.exception.BadRequestException;
-import com.festago.exception.ErrorCode;
-import jakarta.persistence.Entity;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-
-@Entity
-public class Festival extends BaseTimeEntity {
-
- private static final String DEFAULT_THUMBNAIL = "https://picsum.photos/536/354";
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- private String name;
-
- private LocalDate startDate;
-
- private LocalDate endDate;
-
- private String thumbnail;
-
- protected Festival() {
- }
-
- public Festival(String name, LocalDate startDate, LocalDate endDate) {
- this(null, name, startDate, endDate, DEFAULT_THUMBNAIL);
- }
-
- public Festival(String name, LocalDate startDate, LocalDate endDate, String thumbnail) {
- this(null, name, startDate, endDate, thumbnail);
- }
-
- public Festival(Long id, String name, LocalDate startDate, LocalDate endDate, String thumbnail) {
- validate(startDate, endDate);
- this.id = id;
- this.name = name;
- this.startDate = startDate;
- this.endDate = endDate;
- this.thumbnail = thumbnail;
- }
-
- private void validate(LocalDate startDate, LocalDate endDate) {
- if (startDate.isAfter(endDate)) {
- throw new BadRequestException(ErrorCode.INVALID_FESTIVAL_DURATION);
- }
- }
-
- public boolean canCreate(LocalDate currentDate) {
- return startDate.isEqual(currentDate) || startDate.isAfter(currentDate);
- }
-
- public boolean isNotInDuration(LocalDateTime time) {
- LocalDate date = time.toLocalDate();
- return date.isBefore(startDate) || date.isAfter(endDate);
- }
-
- public Long getId() {
- return id;
- }
-
- public String getName() {
- return name;
- }
-
- public LocalDate getStartDate() {
- return startDate;
- }
-
- public LocalDate getEndDate() {
- return endDate;
- }
-
- public String getThumbnail() {
- return thumbnail;
- }
-}
diff --git a/backend/src/main/java/com/festago/domain/Member.java b/backend/src/main/java/com/festago/domain/Member.java
deleted file mode 100644
index 2aefd4b7c..000000000
--- a/backend/src/main/java/com/festago/domain/Member.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package com.festago.domain;
-
-import com.festago.auth.domain.SocialType;
-import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import java.time.LocalDateTime;
-import org.hibernate.annotations.SQLDelete;
-import org.hibernate.annotations.Where;
-
-@Entity
-@SQLDelete(sql = "UPDATE member SET deleted_at = now(), nickname = '탈퇴한 회원', profile_image = '' WHERE id=?")
-@Where(clause = "deleted_at is null")
-public class Member extends BaseTimeEntity {
-
- private static final String DEFAULT_IMAGE_URL = "https://festa-go.site/images/default-profile.png";
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- private String socialId;
-
- @Enumerated(EnumType.STRING)
- private SocialType socialType;
-
- private String nickname;
-
- private String profileImage;
-
- private LocalDateTime deletedAt = null;
-
- protected Member() {
- }
-
- public Member(Long id) {
- this.id = id;
- }
-
- public Member(String socialId, SocialType socialType, String nickname, String profileImage) {
- this(null, socialId, socialType, nickname, profileImage);
- }
-
- public Member(Long id, String socialId, SocialType socialType, String nickname, String profileImage) {
- this.id = id;
- this.socialId = socialId;
- this.socialType = socialType;
- this.nickname = nickname;
- this.profileImage = (profileImage != null) ? profileImage : DEFAULT_IMAGE_URL;
- }
-
- public Long getId() {
- return id;
- }
-
- public String getSocialId() {
- return socialId;
- }
-
- public SocialType getSocialType() {
- return socialType;
- }
-
- public String getNickname() {
- return nickname;
- }
-
- public String getProfileImage() {
- return profileImage;
- }
-
- public boolean isDeleted() {
- return deletedAt != null;
- }
-}
diff --git a/backend/src/main/java/com/festago/domain/School.java b/backend/src/main/java/com/festago/domain/School.java
deleted file mode 100644
index 6e701a961..000000000
--- a/backend/src/main/java/com/festago/domain/School.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package com.festago.domain;
-
-import com.festago.exception.ErrorCode;
-import com.festago.exception.InternalServerException;
-import jakarta.persistence.Entity;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.validation.constraints.NotNull;
-import jakarta.validation.constraints.Size;
-
-@Entity
-public class School {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @NotNull
- @Size(max = 50)
- private String domain;
-
- @NotNull
- @Size(max = 255)
- private String name;
-
- protected School() {
- }
-
- public School(Long id, String domain, String name) {
- validate(domain, name);
- this.id = id;
- this.domain = domain;
- this.name = name;
- }
-
- private void validate(String domain, String name) {
- checkNotNull(domain, name);
- checkLength(domain, name);
- }
-
- private void checkNotNull(String domain, String name) {
- if (domain == null ||
- name == null) {
- throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR);
- }
- }
-
- private void checkLength(String domain, String name) {
- if (overLength(domain, 50) ||
- overLength(name, 255)) {
- throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR);
- }
- }
-
- private boolean overLength(String target, int maxLength) {
- if (target == null) {
- return false;
- }
- return target.length() > maxLength;
- }
-
- public Long getId() {
- return id;
- }
-
- public String getDomain() {
- return domain;
- }
-
- public String getName() {
- return name;
- }
-}
diff --git a/backend/src/main/java/com/festago/domain/Stage.java b/backend/src/main/java/com/festago/domain/Stage.java
deleted file mode 100644
index af2a3c299..000000000
--- a/backend/src/main/java/com/festago/domain/Stage.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package com.festago.domain;
-
-import com.festago.exception.BadRequestException;
-import com.festago.exception.ErrorCode;
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.ManyToOne;
-import jakarta.persistence.OneToMany;
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.List;
-
-@Entity
-public class Stage extends BaseTimeEntity {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @Column(nullable = false)
- private LocalDateTime startTime;
-
- private String lineUp;
-
- private LocalDateTime ticketOpenTime;
-
- @ManyToOne(fetch = FetchType.LAZY)
- private Festival festival;
-
- @OneToMany(mappedBy = "stage", fetch = FetchType.LAZY)
- private List tickets = new ArrayList<>();
-
- protected Stage() {
- }
-
- public Stage(LocalDateTime startTime, String lineUp, LocalDateTime ticketOpenTime, Festival festival) {
- this(null, startTime, lineUp, ticketOpenTime, festival);
- }
-
- public Stage(Long id, LocalDateTime startTime, String lineUp, LocalDateTime ticketOpenTime,
- Festival festival) {
- validate(startTime, ticketOpenTime, festival);
- this.id = id;
- this.startTime = startTime;
- this.lineUp = lineUp;
- this.ticketOpenTime = ticketOpenTime;
- this.festival = festival;
- }
-
- private void validate(LocalDateTime startTime, LocalDateTime ticketOpenTime, Festival festival) {
- if (festival.isNotInDuration(startTime)) {
- throw new BadRequestException(ErrorCode.INVALID_STAGE_START_TIME);
- }
- if (ticketOpenTime.isAfter(startTime) || ticketOpenTime.isEqual(startTime)) {
- throw new BadRequestException(ErrorCode.INVALID_TICKET_OPEN_TIME);
- }
- }
-
- public boolean isStart(LocalDateTime currentTime) {
- return currentTime.isAfter(startTime);
- }
-
- public Long getId() {
- return id;
- }
-
- public LocalDateTime getStartTime() {
- return startTime;
- }
-
- public String getLineUp() {
- return lineUp;
- }
-
- public LocalDateTime getTicketOpenTime() {
- return ticketOpenTime;
- }
-
- public Festival getFestival() {
- return festival;
- }
-
- public List getTickets() {
- return tickets;
- }
-}
diff --git a/backend/src/main/java/com/festago/domain/TicketRepository.java b/backend/src/main/java/com/festago/domain/TicketRepository.java
deleted file mode 100644
index e6acfb0e4..000000000
--- a/backend/src/main/java/com/festago/domain/TicketRepository.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.festago.domain;
-
-import java.util.List;
-import java.util.Optional;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface TicketRepository extends JpaRepository {
-
- List findAllByStageId(Long stageId);
-
- Optional findByTicketTypeAndStage(TicketType ticketType, Stage stage);
-}
diff --git a/backend/src/main/java/com/festago/dto/EntryCodeResponse.java b/backend/src/main/java/com/festago/dto/EntryCodeResponse.java
deleted file mode 100644
index c2a21517b..000000000
--- a/backend/src/main/java/com/festago/dto/EntryCodeResponse.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.festago.dto;
-
-import com.festago.domain.EntryCode;
-
-public record EntryCodeResponse(String code, Long period) {
-
- public static EntryCodeResponse of(EntryCode entryCode) {
- return new EntryCodeResponse(entryCode.getCode(), entryCode.getPeriod());
- }
-}
diff --git a/backend/src/main/java/com/festago/dto/ErrorResponse.java b/backend/src/main/java/com/festago/dto/ErrorResponse.java
deleted file mode 100644
index 6d2e36e4f..000000000
--- a/backend/src/main/java/com/festago/dto/ErrorResponse.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.festago.dto;
-
-import com.festago.exception.ErrorCode;
-import com.festago.exception.FestaGoException;
-
-public record ErrorResponse(ErrorCode errorCode, String message) {
-
- public static ErrorResponse from(FestaGoException festaGoException) {
- return ErrorResponse.from(festaGoException.getErrorCode());
- }
-
- public static ErrorResponse from(ErrorCode errorCode) {
- return new ErrorResponse(errorCode, errorCode.getMessage());
- }
-}
diff --git a/backend/src/main/java/com/festago/dto/FestivalCreateRequest.java b/backend/src/main/java/com/festago/dto/FestivalCreateRequest.java
deleted file mode 100644
index f214aec6b..000000000
--- a/backend/src/main/java/com/festago/dto/FestivalCreateRequest.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.festago.dto;
-
-import com.festago.domain.Festival;
-import java.time.LocalDate;
-import java.util.Objects;
-
-public record FestivalCreateRequest(String name, LocalDate startDate, LocalDate endDate, String thumbnail) {
-
- public Festival toEntity() {
- if (Objects.isNull(thumbnail) || thumbnail.isBlank()) {
- return new Festival(name, startDate, endDate);
- }
- return new Festival(name, startDate, endDate, thumbnail);
- }
-}
diff --git a/backend/src/main/java/com/festago/dto/FestivalDetailTicketResponse.java b/backend/src/main/java/com/festago/dto/FestivalDetailTicketResponse.java
deleted file mode 100644
index f197d22f7..000000000
--- a/backend/src/main/java/com/festago/dto/FestivalDetailTicketResponse.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.festago.dto;
-
-import com.festago.domain.Ticket;
-import com.festago.domain.TicketAmount;
-import com.festago.domain.TicketType;
-
-public record FestivalDetailTicketResponse(Long id,
- TicketType ticketType,
- Integer totalAmount,
- Integer remainAmount) {
-
- public static FestivalDetailTicketResponse from(Ticket ticket) {
- TicketAmount ticketAmount = ticket.getTicketAmount();
- return new FestivalDetailTicketResponse(
- ticket.getId(),
- ticket.getTicketType(),
- ticketAmount.getTotalAmount(),
- ticketAmount.calculateRemainAmount()
- );
- }
-}
diff --git a/backend/src/main/java/com/festago/dto/MemberTicketFestivalResponse.java b/backend/src/main/java/com/festago/dto/MemberTicketFestivalResponse.java
deleted file mode 100644
index e4cdb01bc..000000000
--- a/backend/src/main/java/com/festago/dto/MemberTicketFestivalResponse.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.festago.dto;
-
-import com.festago.domain.Festival;
-
-public record MemberTicketFestivalResponse(Long id, String name, String thumbnail) {
-
- public static MemberTicketFestivalResponse from(Festival festival) {
- return new MemberTicketFestivalResponse(festival.getId(), festival.getName(), festival.getThumbnail());
- }
-}
diff --git a/backend/src/main/java/com/festago/dto/StageCreateRequest.java b/backend/src/main/java/com/festago/dto/StageCreateRequest.java
deleted file mode 100644
index fb8d94a40..000000000
--- a/backend/src/main/java/com/festago/dto/StageCreateRequest.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.festago.dto;
-
-import java.time.LocalDateTime;
-
-public record StageCreateRequest(LocalDateTime startTime,
- String lineUp,
- LocalDateTime ticketOpenTime,
- Long festivalId) {
-
-}
diff --git a/backend/src/main/java/com/festago/dto/StageResponse.java b/backend/src/main/java/com/festago/dto/StageResponse.java
deleted file mode 100644
index f4a0bc876..000000000
--- a/backend/src/main/java/com/festago/dto/StageResponse.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.festago.dto;
-
-import com.festago.domain.Stage;
-import java.time.LocalDateTime;
-
-public record StageResponse(Long id, LocalDateTime startTime) {
-
- public static StageResponse from(Stage stage) {
- return new StageResponse(stage.getId(), stage.getStartTime());
- }
-}
diff --git a/backend/src/main/java/com/festago/dto/StageTicketResponse.java b/backend/src/main/java/com/festago/dto/StageTicketResponse.java
deleted file mode 100644
index 51255b276..000000000
--- a/backend/src/main/java/com/festago/dto/StageTicketResponse.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.festago.dto;
-
-import com.festago.domain.Ticket;
-import com.festago.domain.TicketAmount;
-import com.festago.domain.TicketType;
-
-public record StageTicketResponse(Long id,
- TicketType ticketType,
- Integer totalAmount,
- Integer remainAmount) {
-
- public static StageTicketResponse from(Ticket ticket) {
- TicketAmount ticketAmount = ticket.getTicketAmount();
- return new StageTicketResponse(
- ticket.getId(),
- ticket.getTicketType(),
- ticketAmount.getTotalAmount(),
- ticketAmount.calculateRemainAmount()
- );
- }
-}
diff --git a/backend/src/main/java/com/festago/dto/TicketCreateRequest.java b/backend/src/main/java/com/festago/dto/TicketCreateRequest.java
deleted file mode 100644
index 37f6fb9e0..000000000
--- a/backend/src/main/java/com/festago/dto/TicketCreateRequest.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.festago.dto;
-
-import com.festago.domain.TicketType;
-import java.time.LocalDateTime;
-
-public record TicketCreateRequest(Long stageId, TicketType ticketType, Integer amount, LocalDateTime entryTime) {
-
-}
diff --git a/backend/src/main/java/com/festago/dto/TicketValidationRequest.java b/backend/src/main/java/com/festago/dto/TicketValidationRequest.java
deleted file mode 100644
index f98f8de2e..000000000
--- a/backend/src/main/java/com/festago/dto/TicketValidationRequest.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.festago.dto;
-
-public record TicketValidationRequest(String code) {
-
-}
diff --git a/backend/src/main/java/com/festago/dto/TicketValidationResponse.java b/backend/src/main/java/com/festago/dto/TicketValidationResponse.java
deleted file mode 100644
index 7e5a5422c..000000000
--- a/backend/src/main/java/com/festago/dto/TicketValidationResponse.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.festago.dto;
-
-import com.festago.domain.EntryState;
-import com.festago.domain.MemberTicket;
-
-public record TicketValidationResponse(EntryState updatedState) {
-
- public static TicketValidationResponse from(MemberTicket memberTicket) {
- return new TicketValidationResponse(memberTicket.getEntryState());
- }
-}
diff --git a/backend/src/main/java/com/festago/dto/TicketingRequest.java b/backend/src/main/java/com/festago/dto/TicketingRequest.java
deleted file mode 100644
index 048039f0f..000000000
--- a/backend/src/main/java/com/festago/dto/TicketingRequest.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.festago.dto;
-
-public record TicketingRequest(Long ticketId) {
-
-}
diff --git a/backend/src/main/java/com/festago/entry/application/EntryCodeExtractor.java b/backend/src/main/java/com/festago/entry/application/EntryCodeExtractor.java
new file mode 100644
index 000000000..89716d035
--- /dev/null
+++ b/backend/src/main/java/com/festago/entry/application/EntryCodeExtractor.java
@@ -0,0 +1,8 @@
+package com.festago.entry.application;
+
+import com.festago.entry.domain.EntryCodePayload;
+
+public interface EntryCodeExtractor {
+
+ EntryCodePayload extract(String code);
+}
diff --git a/backend/src/main/java/com/festago/entry/application/EntryCodeManager.java b/backend/src/main/java/com/festago/entry/application/EntryCodeManager.java
new file mode 100644
index 000000000..b8dbcbbd3
--- /dev/null
+++ b/backend/src/main/java/com/festago/entry/application/EntryCodeManager.java
@@ -0,0 +1,29 @@
+package com.festago.entry.application;
+
+import com.festago.entry.domain.EntryCode;
+import com.festago.entry.domain.EntryCodePayload;
+import java.util.Date;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class EntryCodeManager {
+
+ private static final int MILLISECOND_FACTOR = 1_000;
+ private static final int DEFAULT_PERIOD = 30;
+ private static final int DEFAULT_OFFSET = 10;
+
+ private final EntryCodeProvider entryCodeProvider;
+ private final EntryCodeExtractor entryCodeExtractor;
+
+ public EntryCode provide(EntryCodePayload entryCodePayload, long currentTimeMillis) {
+ Date expiredAt = new Date(currentTimeMillis + (DEFAULT_PERIOD + DEFAULT_OFFSET) * MILLISECOND_FACTOR);
+ String code = entryCodeProvider.provide(entryCodePayload, expiredAt);
+ return new EntryCode(code, DEFAULT_PERIOD, DEFAULT_OFFSET);
+ }
+
+ public EntryCodePayload extract(String code) {
+ return entryCodeExtractor.extract(code);
+ }
+}
diff --git a/backend/src/main/java/com/festago/domain/EntryCodeProvider.java b/backend/src/main/java/com/festago/entry/application/EntryCodeProvider.java
similarity index 60%
rename from backend/src/main/java/com/festago/domain/EntryCodeProvider.java
rename to backend/src/main/java/com/festago/entry/application/EntryCodeProvider.java
index 083bc01c0..1f024f9ec 100644
--- a/backend/src/main/java/com/festago/domain/EntryCodeProvider.java
+++ b/backend/src/main/java/com/festago/entry/application/EntryCodeProvider.java
@@ -1,5 +1,6 @@
-package com.festago.domain;
+package com.festago.entry.application;
+import com.festago.entry.domain.EntryCodePayload;
import java.util.Date;
public interface EntryCodeProvider {
diff --git a/backend/src/main/java/com/festago/entry/application/EntryService.java b/backend/src/main/java/com/festago/entry/application/EntryService.java
new file mode 100644
index 000000000..061e369d8
--- /dev/null
+++ b/backend/src/main/java/com/festago/entry/application/EntryService.java
@@ -0,0 +1,55 @@
+package com.festago.entry.application;
+
+import com.festago.common.exception.BadRequestException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.NotFoundException;
+import com.festago.entry.domain.EntryCode;
+import com.festago.entry.domain.EntryCodePayload;
+import com.festago.entry.dto.EntryCodeResponse;
+import com.festago.entry.dto.TicketValidationRequest;
+import com.festago.entry.dto.TicketValidationResponse;
+import com.festago.entry.dto.event.EntryProcessEvent;
+import com.festago.ticketing.domain.MemberTicket;
+import com.festago.ticketing.repository.MemberTicketRepository;
+import java.time.Clock;
+import java.time.LocalDateTime;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class EntryService {
+
+ private final EntryCodeManager entryCodeManager;
+ private final MemberTicketRepository memberTicketRepository;
+ private final ApplicationEventPublisher publisher;
+ private final Clock clock;
+
+ public EntryCodeResponse createEntryCode(Long memberId, Long memberTicketId) {
+ MemberTicket memberTicket = findMemberTicket(memberTicketId);
+ if (!memberTicket.isOwner(memberId)) {
+ throw new BadRequestException(ErrorCode.NOT_MEMBER_TICKET_OWNER);
+ }
+ if (!memberTicket.canEntry(LocalDateTime.now(clock))) {
+ throw new BadRequestException(ErrorCode.NOT_ENTRY_TIME);
+ }
+ EntryCode entryCode = entryCodeManager.provide(EntryCodePayload.from(memberTicket), clock.millis());
+ return EntryCodeResponse.of(entryCode);
+ }
+
+ private MemberTicket findMemberTicket(Long memberTicketId) {
+ return memberTicketRepository.findById(memberTicketId)
+ .orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_TICKET_NOT_FOUND));
+ }
+
+ public TicketValidationResponse validate(TicketValidationRequest request) {
+ EntryCodePayload entryCodePayload = entryCodeManager.extract(request.code());
+ MemberTicket memberTicket = findMemberTicket(entryCodePayload.getMemberTicketId());
+ memberTicket.changeState(entryCodePayload.getEntryState());
+ publisher.publishEvent(new EntryProcessEvent(memberTicket.getOwner().getId()));
+ return TicketValidationResponse.from(memberTicket);
+ }
+}
diff --git a/backend/src/main/java/com/festago/config/EntryCodeConfig.java b/backend/src/main/java/com/festago/entry/config/EntryCodeConfig.java
similarity index 69%
rename from backend/src/main/java/com/festago/config/EntryCodeConfig.java
rename to backend/src/main/java/com/festago/entry/config/EntryCodeConfig.java
index 924e798b2..bc910c2b5 100644
--- a/backend/src/main/java/com/festago/config/EntryCodeConfig.java
+++ b/backend/src/main/java/com/festago/entry/config/EntryCodeConfig.java
@@ -1,9 +1,9 @@
-package com.festago.config;
+package com.festago.entry.config;
-import com.festago.domain.EntryCodeExtractor;
-import com.festago.domain.EntryCodeProvider;
-import com.festago.infrastructure.JwtEntryCodeExtractor;
-import com.festago.infrastructure.JwtEntryCodeProvider;
+import com.festago.entry.application.EntryCodeExtractor;
+import com.festago.entry.application.EntryCodeProvider;
+import com.festago.entry.infrastructure.JwtEntryCodeExtractor;
+import com.festago.entry.infrastructure.JwtEntryCodeProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/backend/src/main/java/com/festago/domain/EntryCode.java b/backend/src/main/java/com/festago/entry/domain/EntryCode.java
similarity index 59%
rename from backend/src/main/java/com/festago/domain/EntryCode.java
rename to backend/src/main/java/com/festago/entry/domain/EntryCode.java
index 2ad8fbca0..c75fde867 100644
--- a/backend/src/main/java/com/festago/domain/EntryCode.java
+++ b/backend/src/main/java/com/festago/entry/domain/EntryCode.java
@@ -1,14 +1,10 @@
-package com.festago.domain;
+package com.festago.entry.domain;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.InternalServerException;
-import java.util.Date;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.InternalServerException;
public class EntryCode {
- private static final long DEFAULT_PERIOD = 30;
- private static final long DEFAULT_OFFSET = 10;
- private static final int MILLISECOND_FACTOR = 1000;
private static final int MINIMUM_PERIOD = 0;
private static final int MINIMUM_OFFSET = 0;
@@ -36,12 +32,6 @@ private boolean isNegative(long offset) {
return offset < MINIMUM_OFFSET;
}
- public static EntryCode create(EntryCodeProvider entryCodeProvider, MemberTicket memberTicket) {
- Date expiredAt = new Date(new Date().getTime() + (DEFAULT_PERIOD + DEFAULT_OFFSET) * MILLISECOND_FACTOR);
- String code = entryCodeProvider.provide(EntryCodePayload.from(memberTicket), expiredAt);
- return new EntryCode(code, DEFAULT_PERIOD, DEFAULT_OFFSET);
- }
-
public String getCode() {
return code;
}
diff --git a/backend/src/main/java/com/festago/domain/EntryCodePayload.java b/backend/src/main/java/com/festago/entry/domain/EntryCodePayload.java
similarity index 78%
rename from backend/src/main/java/com/festago/domain/EntryCodePayload.java
rename to backend/src/main/java/com/festago/entry/domain/EntryCodePayload.java
index ee29374e0..38ff7c411 100644
--- a/backend/src/main/java/com/festago/domain/EntryCodePayload.java
+++ b/backend/src/main/java/com/festago/entry/domain/EntryCodePayload.java
@@ -1,7 +1,9 @@
-package com.festago.domain;
+package com.festago.entry.domain;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.InternalServerException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.InternalServerException;
+import com.festago.ticketing.domain.EntryState;
+import com.festago.ticketing.domain.MemberTicket;
public class EntryCodePayload {
diff --git a/backend/src/main/java/com/festago/entry/dto/EntryCodeResponse.java b/backend/src/main/java/com/festago/entry/dto/EntryCodeResponse.java
new file mode 100644
index 000000000..c161502e9
--- /dev/null
+++ b/backend/src/main/java/com/festago/entry/dto/EntryCodeResponse.java
@@ -0,0 +1,14 @@
+package com.festago.entry.dto;
+
+import com.festago.entry.domain.EntryCode;
+
+public record EntryCodeResponse(
+ String code,
+ Long period) {
+
+ public static EntryCodeResponse of(EntryCode entryCode) {
+ return new EntryCodeResponse(
+ entryCode.getCode(),
+ entryCode.getPeriod());
+ }
+}
diff --git a/backend/src/main/java/com/festago/entry/dto/TicketValidationRequest.java b/backend/src/main/java/com/festago/entry/dto/TicketValidationRequest.java
new file mode 100644
index 000000000..c006b24e6
--- /dev/null
+++ b/backend/src/main/java/com/festago/entry/dto/TicketValidationRequest.java
@@ -0,0 +1,10 @@
+package com.festago.entry.dto;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record TicketValidationRequest(
+ @NotBlank(message = "code는 공백일 수 없습니다.")
+ String code
+) {
+
+}
diff --git a/backend/src/main/java/com/festago/entry/dto/TicketValidationResponse.java b/backend/src/main/java/com/festago/entry/dto/TicketValidationResponse.java
new file mode 100644
index 000000000..3c5f01175
--- /dev/null
+++ b/backend/src/main/java/com/festago/entry/dto/TicketValidationResponse.java
@@ -0,0 +1,12 @@
+package com.festago.entry.dto;
+
+import com.festago.ticketing.domain.EntryState;
+import com.festago.ticketing.domain.MemberTicket;
+
+public record TicketValidationResponse(
+ EntryState updatedState) {
+
+ public static TicketValidationResponse from(MemberTicket memberTicket) {
+ return new TicketValidationResponse(memberTicket.getEntryState());
+ }
+}
diff --git a/backend/src/main/java/com/festago/entry/dto/event/EntryProcessEvent.java b/backend/src/main/java/com/festago/entry/dto/event/EntryProcessEvent.java
new file mode 100644
index 000000000..6a8775bc2
--- /dev/null
+++ b/backend/src/main/java/com/festago/entry/dto/event/EntryProcessEvent.java
@@ -0,0 +1,7 @@
+package com.festago.entry.dto.event;
+
+public record EntryProcessEvent(
+ Long memberId
+) {
+
+}
diff --git a/backend/src/main/java/com/festago/infrastructure/JwtEntryCodeExtractor.java b/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeExtractor.java
similarity index 80%
rename from backend/src/main/java/com/festago/infrastructure/JwtEntryCodeExtractor.java
rename to backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeExtractor.java
index d58010f29..dbc0bf63c 100644
--- a/backend/src/main/java/com/festago/infrastructure/JwtEntryCodeExtractor.java
+++ b/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeExtractor.java
@@ -1,10 +1,10 @@
-package com.festago.infrastructure;
+package com.festago.entry.infrastructure;
-import com.festago.domain.EntryCodeExtractor;
-import com.festago.domain.EntryCodePayload;
-import com.festago.domain.EntryState;
-import com.festago.exception.BadRequestException;
-import com.festago.exception.ErrorCode;
+import com.festago.common.exception.BadRequestException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.entry.application.EntryCodeExtractor;
+import com.festago.entry.domain.EntryCodePayload;
+import com.festago.ticketing.domain.EntryState;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
@@ -43,7 +43,7 @@ private Claims getClaims(String code) {
.getBody();
} catch (ExpiredJwtException e) {
throw new BadRequestException(ErrorCode.EXPIRED_ENTRY_CODE);
- } catch (JwtException e) {
+ } catch (JwtException | IllegalArgumentException e) {
throw new BadRequestException(ErrorCode.INVALID_ENTRY_CODE);
}
}
diff --git a/backend/src/main/java/com/festago/infrastructure/JwtEntryCodeProvider.java b/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeProvider.java
similarity index 82%
rename from backend/src/main/java/com/festago/infrastructure/JwtEntryCodeProvider.java
rename to backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeProvider.java
index 634fd52a2..852915e6c 100644
--- a/backend/src/main/java/com/festago/infrastructure/JwtEntryCodeProvider.java
+++ b/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeProvider.java
@@ -1,9 +1,9 @@
-package com.festago.infrastructure;
+package com.festago.entry.infrastructure;
-import com.festago.domain.EntryCodePayload;
-import com.festago.domain.EntryCodeProvider;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.InternalServerException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.InternalServerException;
+import com.festago.entry.application.EntryCodeProvider;
+import com.festago.entry.domain.EntryCodePayload;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
diff --git a/backend/src/main/java/com/festago/fcm/application/FCMNotificationEventListener.java b/backend/src/main/java/com/festago/fcm/application/FCMNotificationEventListener.java
new file mode 100644
index 000000000..92f369e34
--- /dev/null
+++ b/backend/src/main/java/com/festago/fcm/application/FCMNotificationEventListener.java
@@ -0,0 +1,87 @@
+package com.festago.fcm.application;
+
+import com.festago.entry.dto.event.EntryProcessEvent;
+import com.festago.fcm.domain.FCMChannel;
+import com.festago.fcm.dto.MemberFCMResponse;
+import com.google.firebase.messaging.AndroidConfig;
+import com.google.firebase.messaging.AndroidNotification;
+import com.google.firebase.messaging.BatchResponse;
+import com.google.firebase.messaging.FirebaseMessaging;
+import com.google.firebase.messaging.FirebaseMessagingException;
+import com.google.firebase.messaging.Message;
+import com.google.firebase.messaging.SendResponse;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Profile;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.event.TransactionPhase;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+@Component
+@Profile("prod | dev")
+public class FCMNotificationEventListener {
+
+ private static final Logger log = LoggerFactory.getLogger(FCMNotificationEventListener.class);
+
+ private final FirebaseMessaging firebaseMessaging;
+ private final MemberFCMService memberFCMService;
+
+ public FCMNotificationEventListener(FirebaseMessaging firebaseMessaging, MemberFCMService memberFCMService) {
+ this.firebaseMessaging = firebaseMessaging;
+ this.memberFCMService = memberFCMService;
+ }
+
+ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+ @Async
+ public void sendFcmNotification(EntryProcessEvent event) {
+ List messages = createMessages(getMemberFCMToken(event.memberId()), FCMChannel.ENTRY_PROCESS.name());
+ try {
+ BatchResponse batchResponse = firebaseMessaging.sendAll(messages);
+ checkAllSuccess(batchResponse, event.memberId());
+ } catch (FirebaseMessagingException e) {
+ log.warn("fail send FCM message", e);
+ }
+ }
+
+ private List getMemberFCMToken(Long memberId) {
+ return memberFCMService.findMemberFCM(memberId).memberFCMs().stream()
+ .map(MemberFCMResponse::fcmToken)
+ .toList();
+ }
+
+ private List createMessages(List tokens, String channelId) {
+ return tokens.stream()
+ .map(token -> createMessage(token, channelId))
+ .toList();
+ }
+
+ private Message createMessage(String token, String channelId) {
+ return Message.builder()
+ .setAndroidConfig(createAndroidConfig(channelId))
+ .setToken(token)
+ .build();
+ }
+
+ private AndroidConfig createAndroidConfig(String channelId) {
+ return AndroidConfig.builder()
+ .setNotification(createAndroidNotification(channelId))
+ .build();
+ }
+
+ private AndroidNotification createAndroidNotification(String channelId) {
+ return AndroidNotification.builder()
+ .setChannelId(channelId)
+ .build();
+ }
+
+ private void checkAllSuccess(BatchResponse batchResponse, Long memberId) {
+ if (batchResponse.getFailureCount() > 0) {
+ List failSend = batchResponse.getResponses().stream()
+ .filter(sendResponse -> !sendResponse.isSuccessful())
+ .toList();
+ log.warn("member {} 에 대한 다음 요청들이 실패했습니다. {}", memberId, failSend);
+ }
+ }
+}
diff --git a/backend/src/main/java/com/festago/fcm/application/MemberFCMService.java b/backend/src/main/java/com/festago/fcm/application/MemberFCMService.java
new file mode 100644
index 000000000..ad346b2d6
--- /dev/null
+++ b/backend/src/main/java/com/festago/fcm/application/MemberFCMService.java
@@ -0,0 +1,70 @@
+package com.festago.fcm.application;
+
+import com.festago.auth.application.AuthExtractor;
+import com.festago.auth.domain.AuthPayload;
+import com.festago.fcm.domain.MemberFCM;
+import com.festago.fcm.dto.MemberFCMsResponse;
+import com.festago.fcm.repository.MemberFCMRepository;
+import java.util.List;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class MemberFCMService {
+
+ private static final Logger log = LoggerFactory.getLogger(MemberFCMService.class);
+
+ private final MemberFCMRepository memberFCMRepository;
+ private final AuthExtractor authExtractor;
+
+ public MemberFCMService(MemberFCMRepository memberFCMRepository, AuthExtractor authExtractor) {
+ this.memberFCMRepository = memberFCMRepository;
+ this.authExtractor = authExtractor;
+ }
+
+ @Transactional(readOnly = true)
+ public MemberFCMsResponse findMemberFCM(Long memberId) {
+ List memberFCM = memberFCMRepository.findByMemberId(memberId);
+ if (memberFCM.isEmpty()) {
+ log.warn("member {} 의 FCM 토큰이 발급되지 않았습니다.", memberId);
+ }
+ return MemberFCMsResponse.from(memberFCM);
+ }
+
+ @Async
+ public void saveMemberFCM(boolean isNewMember, String accessToken, String fcmToken) {
+ if (isNewMember) {
+ saveNewMemberFCM(accessToken, fcmToken);
+ return;
+ }
+ saveOriginMemberFCM(accessToken, fcmToken);
+ }
+
+ private void saveOriginMemberFCM(String accessToken, String fcmToken) {
+ Long memberId = extractMemberId(accessToken);
+ Optional memberFCM = memberFCMRepository.findMemberFCMByMemberIdAndFcmToken(memberId, fcmToken);
+ if (memberFCM.isEmpty()) {
+ memberFCMRepository.save(new MemberFCM(memberId, fcmToken));
+ }
+ }
+
+ private Long extractMemberId(String accessToken) {
+ AuthPayload authPayload = authExtractor.extract(accessToken);
+ return authPayload.getMemberId();
+ }
+
+ private void saveNewMemberFCM(String accessToken, String fcmToken) {
+ Long memberId = extractMemberId(accessToken);
+ memberFCMRepository.save(new MemberFCM(memberId, fcmToken));
+ }
+
+ @Async
+ public void deleteMemberFCM(Long memberId) {
+ memberFCMRepository.deleteAllByMemberId(memberId);
+ }
+}
diff --git a/backend/src/main/java/com/festago/fcm/config/FCMConfig.java b/backend/src/main/java/com/festago/fcm/config/FCMConfig.java
new file mode 100644
index 000000000..7d792b377
--- /dev/null
+++ b/backend/src/main/java/com/festago/fcm/config/FCMConfig.java
@@ -0,0 +1,58 @@
+package com.festago.fcm.config;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.messaging.FirebaseMessaging;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.core.io.ClassPathResource;
+
+@Configuration
+@Profile("prod | dev")
+public class FCMConfig {
+
+ @Value("${fcm.key.path}")
+ private String fcmPrivateKeyPath;
+
+ @Value("${fcm.key.scope}")
+ private String fireBaseScope;
+
+ @Bean
+ public FirebaseMessaging firebaseMessaging() throws IOException {
+ Optional defaultFirebaseApp = defaultFirebaseApp();
+ if (defaultFirebaseApp.isPresent()) {
+ return FirebaseMessaging.getInstance(defaultFirebaseApp.get());
+ }
+ return FirebaseMessaging.getInstance(
+ FirebaseApp.initializeApp(createFirebaseOption())
+ );
+ }
+
+ private Optional defaultFirebaseApp() {
+ List firebaseAppList = FirebaseApp.getApps();
+ if (firebaseAppList == null || firebaseAppList.isEmpty()) {
+ return Optional.empty();
+ }
+ return firebaseAppList.stream()
+ .filter(firebaseApp -> firebaseApp.getName().equals(FirebaseApp.DEFAULT_APP_NAME))
+ .findAny();
+ }
+
+ private FirebaseOptions createFirebaseOption() throws IOException {
+ return FirebaseOptions.builder()
+ .setCredentials(createGoogleCredentials())
+ .build();
+ }
+
+ private GoogleCredentials createGoogleCredentials() throws IOException {
+ return GoogleCredentials
+ .fromStream(new ClassPathResource(fcmPrivateKeyPath).getInputStream())
+ .createScoped(fireBaseScope);
+ }
+}
diff --git a/backend/src/main/java/com/festago/fcm/domain/FCMChannel.java b/backend/src/main/java/com/festago/fcm/domain/FCMChannel.java
new file mode 100644
index 000000000..617086e32
--- /dev/null
+++ b/backend/src/main/java/com/festago/fcm/domain/FCMChannel.java
@@ -0,0 +1,6 @@
+package com.festago.fcm.domain;
+
+public enum FCMChannel {
+ ENTRY_PROCESS,
+ ENTRY_ALERT
+}
diff --git a/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java b/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java
new file mode 100644
index 000000000..8a97f7f07
--- /dev/null
+++ b/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java
@@ -0,0 +1,56 @@
+package com.festago.fcm.domain;
+
+import com.festago.common.domain.BaseTimeEntity;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.validation.constraints.NotNull;
+
+@Entity
+@Table(name = "member_fcm")
+public class MemberFCM extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @NotNull
+ private Long memberId;
+
+ @NotNull
+ private String fcmToken;
+
+ protected MemberFCM() {
+ }
+
+ public MemberFCM(Long memberId, String fcmToken) {
+ this(null, memberId, fcmToken);
+ }
+
+ public MemberFCM(Long id, Long memberId, String fcmToken) {
+ validate(memberId, fcmToken);
+ this.id = id;
+ this.memberId = memberId;
+ this.fcmToken = fcmToken;
+ }
+
+ private void validate(Long memberId, String fcmToken) {
+ if (memberId == null || fcmToken == null) {
+ throw new IllegalArgumentException("MemberFCM 은 허용되지 않은 null 값으로 생성할 수 없습니다.");
+ }
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public String getFcmToken() {
+ return fcmToken;
+ }
+}
diff --git a/backend/src/main/java/com/festago/fcm/dto/MemberFCMResponse.java b/backend/src/main/java/com/festago/fcm/dto/MemberFCMResponse.java
new file mode 100644
index 000000000..61f252a9b
--- /dev/null
+++ b/backend/src/main/java/com/festago/fcm/dto/MemberFCMResponse.java
@@ -0,0 +1,18 @@
+package com.festago.fcm.dto;
+
+import com.festago.fcm.domain.MemberFCM;
+
+public record MemberFCMResponse(
+ Long id,
+ Long memberId,
+ String fcmToken
+) {
+
+ public static MemberFCMResponse from(MemberFCM memberFCM) {
+ return new MemberFCMResponse(
+ memberFCM.getId(),
+ memberFCM.getMemberId(),
+ memberFCM.getFcmToken()
+ );
+ }
+}
diff --git a/backend/src/main/java/com/festago/fcm/dto/MemberFCMsResponse.java b/backend/src/main/java/com/festago/fcm/dto/MemberFCMsResponse.java
new file mode 100644
index 000000000..80df8a602
--- /dev/null
+++ b/backend/src/main/java/com/festago/fcm/dto/MemberFCMsResponse.java
@@ -0,0 +1,17 @@
+package com.festago.fcm.dto;
+
+import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.toList;
+
+import com.festago.fcm.domain.MemberFCM;
+import java.util.List;
+
+public record MemberFCMsResponse(List memberFCMs
+) {
+
+ public static MemberFCMsResponse from(List memberFCMs) {
+ return memberFCMs.stream()
+ .map(MemberFCMResponse::from)
+ .collect(collectingAndThen(toList(), MemberFCMsResponse::new));
+ }
+}
diff --git a/backend/src/main/java/com/festago/fcm/repository/MemberFCMRepository.java b/backend/src/main/java/com/festago/fcm/repository/MemberFCMRepository.java
new file mode 100644
index 000000000..fb6ed5709
--- /dev/null
+++ b/backend/src/main/java/com/festago/fcm/repository/MemberFCMRepository.java
@@ -0,0 +1,15 @@
+package com.festago.fcm.repository;
+
+import com.festago.fcm.domain.MemberFCM;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface MemberFCMRepository extends JpaRepository {
+
+ List findByMemberId(Long memberId);
+
+ Optional findMemberFCMByMemberIdAndFcmToken(Long memberId, String fcmToken);
+
+ void deleteAllByMemberId(Long memberId);
+}
diff --git a/backend/src/main/java/com/festago/festival/application/FestivalService.java b/backend/src/main/java/com/festago/festival/application/FestivalService.java
new file mode 100644
index 000000000..4fe7c285f
--- /dev/null
+++ b/backend/src/main/java/com/festago/festival/application/FestivalService.java
@@ -0,0 +1,87 @@
+package com.festago.festival.application;
+
+import static java.util.Comparator.comparing;
+
+import com.festago.common.exception.BadRequestException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.NotFoundException;
+import com.festago.festival.domain.Festival;
+import com.festago.festival.dto.FestivalCreateRequest;
+import com.festago.festival.dto.FestivalDetailResponse;
+import com.festago.festival.dto.FestivalResponse;
+import com.festago.festival.dto.FestivalUpdateRequest;
+import com.festago.festival.dto.FestivalsResponse;
+import com.festago.festival.repository.FestivalRepository;
+import com.festago.school.domain.School;
+import com.festago.school.repository.SchoolRepository;
+import com.festago.stage.domain.Stage;
+import com.festago.stage.repository.StageRepository;
+import java.time.Clock;
+import java.time.LocalDate;
+import java.util.List;
+import org.springframework.dao.DataIntegrityViolationException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class FestivalService {
+
+ private final FestivalRepository festivalRepository;
+ private final StageRepository stageRepository;
+ private final SchoolRepository schoolRepository;
+ private final Clock clock;
+
+ public FestivalResponse create(FestivalCreateRequest request) {
+ School school = schoolRepository.findById(request.schoolId())
+ .orElseThrow(() -> new NotFoundException(ErrorCode.SCHOOL_NOT_FOUND));
+ Festival festival = request.toEntity(school);
+ validate(festival);
+ return FestivalResponse.from(festivalRepository.save(festival));
+ }
+
+ private void validate(Festival festival) {
+ if (!festival.canCreate(LocalDate.now(clock))) {
+ throw new BadRequestException(ErrorCode.INVALID_FESTIVAL_START_DATE);
+ }
+ }
+
+ @Transactional(readOnly = true)
+ public FestivalsResponse findAll() {
+ List festivals = festivalRepository.findAll();
+ return FestivalsResponse.from(festivals);
+ }
+
+ @Transactional(readOnly = true)
+ public FestivalDetailResponse findDetail(Long festivalId) {
+ Festival festival = findFestival(festivalId);
+ List stages = stageRepository.findAllDetailByFestivalId(festivalId).stream()
+ .sorted(comparing(Stage::getStartTime))
+ .toList();
+ return FestivalDetailResponse.of(festival, stages);
+ }
+
+ private Festival findFestival(Long festivalId) {
+ return festivalRepository.findById(festivalId)
+ .orElseThrow(() -> new NotFoundException(ErrorCode.FESTIVAL_NOT_FOUND));
+ }
+
+ public void update(Long festivalId, FestivalUpdateRequest request) {
+ Festival festival = findFestival(festivalId);
+ festival.changeName(request.name());
+ festival.changeThumbnail(request.thumbnail());
+ festival.changeDate(request.startDate(), request.endDate());
+ validate(festival);
+ }
+
+ public void delete(Long festivalId) {
+ try {
+ festivalRepository.deleteById(festivalId);
+ festivalRepository.flush();
+ } catch (DataIntegrityViolationException e) {
+ throw new BadRequestException(ErrorCode.DELETE_CONSTRAINT_FESTIVAL);
+ }
+ }
+}
diff --git a/backend/src/main/java/com/festago/festival/domain/Festival.java b/backend/src/main/java/com/festago/festival/domain/Festival.java
new file mode 100644
index 000000000..9b0522c08
--- /dev/null
+++ b/backend/src/main/java/com/festago/festival/domain/Festival.java
@@ -0,0 +1,138 @@
+package com.festago.festival.domain;
+
+import com.festago.common.domain.BaseTimeEntity;
+import com.festago.common.util.Validator;
+import com.festago.school.domain.School;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import org.springframework.util.Assert;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Festival extends BaseTimeEntity {
+
+ private static final String DEFAULT_THUMBNAIL = "https://picsum.photos/536/354";
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @NotNull
+ @Size(max = 50)
+ private String name;
+
+ @NotNull
+ private LocalDate startDate;
+
+ @NotNull
+ private LocalDate endDate;
+
+ @NotNull
+ @Size(max = 255)
+ private String thumbnail;
+
+ @NotNull
+ @ManyToOne(fetch = FetchType.LAZY)
+ private School school;
+
+ public Festival(String name, LocalDate startDate, LocalDate endDate, School school) {
+ this(null, name, startDate, endDate, DEFAULT_THUMBNAIL, school);
+ }
+
+ public Festival(String name, LocalDate startDate, LocalDate endDate, String thumbnail, School school) {
+ this(null, name, startDate, endDate, thumbnail, school);
+ }
+
+ public Festival(Long id, String name, LocalDate startDate, LocalDate endDate, String thumbnail, School school) {
+ validate(name, startDate, endDate, thumbnail);
+ this.id = id;
+ this.name = name;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.thumbnail = thumbnail;
+ this.school = school;
+ }
+
+ private void validate(String name, LocalDate startDate, LocalDate endDate, String thumbnail) {
+ validateName(name);
+ validateThumbnail(thumbnail);
+ validateDate(startDate, endDate);
+ }
+
+ private void validateName(String name) {
+ Assert.notNull(name, "name은 null 값이 될 수 없습니다.");
+ Validator.maxLength(name, 50, "name은 50글자를 넘을 수 없습니다.");
+ }
+
+ private void validateThumbnail(String thumbnail) {
+ Assert.notNull(thumbnail, "thumbnail은 null 값이 될 수 없습니다.");
+ Validator.maxLength(thumbnail, 255, "thumbnail은 50글자를 넘을 수 없습니다.");
+ }
+
+ private void validateDate(LocalDate startDate, LocalDate endDate) {
+ Assert.notNull(startDate, "startDate는 null 값이 될 수 없습니다.");
+ Assert.notNull(endDate, "endDate는 null 값이 될 수 없습니다.");
+ if (startDate.isAfter(endDate)) {
+ throw new IllegalArgumentException("축제 시작 일은 종료일 이전이어야 합니다.");
+ }
+ }
+
+ public boolean canCreate(LocalDate currentDate) {
+ return startDate.isEqual(currentDate) || startDate.isAfter(currentDate);
+ }
+
+ public boolean isNotInDuration(LocalDateTime time) {
+ LocalDate date = time.toLocalDate();
+ return date.isBefore(startDate) || date.isAfter(endDate);
+ }
+
+ public void changeName(String name) {
+ validateName(name);
+ this.name = name;
+ }
+
+ public void changeThumbnail(String thumbnail) {
+ validateThumbnail(thumbnail);
+ this.thumbnail = thumbnail;
+ }
+
+ public void changeDate(LocalDate startDate, LocalDate endDate) {
+ validateDate(startDate, endDate);
+ this.startDate = startDate;
+ this.endDate = endDate;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public LocalDate getStartDate() {
+ return startDate;
+ }
+
+ public LocalDate getEndDate() {
+ return endDate;
+ }
+
+ public String getThumbnail() {
+ return thumbnail;
+ }
+
+ public School getSchool() {
+ return school;
+ }
+}
diff --git a/backend/src/main/java/com/festago/festival/dto/FestivalCreateRequest.java b/backend/src/main/java/com/festago/festival/dto/FestivalCreateRequest.java
new file mode 100644
index 000000000..3fe810347
--- /dev/null
+++ b/backend/src/main/java/com/festago/festival/dto/FestivalCreateRequest.java
@@ -0,0 +1,30 @@
+package com.festago.festival.dto;
+
+import com.festago.festival.domain.Festival;
+import com.festago.school.domain.School;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import java.time.LocalDate;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.format.annotation.DateTimeFormat.ISO;
+
+public record FestivalCreateRequest(
+ @NotBlank(message = "name은 공백일 수 없습니다.")
+ String name,
+ @NotNull(message = "startDate는 null 일 수 없습니다.")
+ @DateTimeFormat(iso = ISO.DATE)
+ LocalDate startDate,
+ @NotNull(message = "endDate는 null 일 수 없습니다.")
+ @DateTimeFormat(iso = ISO.DATE)
+ LocalDate endDate,
+ String thumbnail,
+ @NotNull(message = "schoolId는 null 일 수 없습니다.")
+ Long schoolId) {
+
+ public Festival toEntity(School school) {
+ if (thumbnail == null || thumbnail.isBlank()) {
+ return new Festival(name, startDate, endDate, school);
+ }
+ return new Festival(name, startDate, endDate, thumbnail, school);
+ }
+}
diff --git a/backend/src/main/java/com/festago/dto/FestivalDetailResponse.java b/backend/src/main/java/com/festago/festival/dto/FestivalDetailResponse.java
similarity index 55%
rename from backend/src/main/java/com/festago/dto/FestivalDetailResponse.java
rename to backend/src/main/java/com/festago/festival/dto/FestivalDetailResponse.java
index 7f283ac99..8b6d0c950 100644
--- a/backend/src/main/java/com/festago/dto/FestivalDetailResponse.java
+++ b/backend/src/main/java/com/festago/festival/dto/FestivalDetailResponse.java
@@ -1,16 +1,18 @@
-package com.festago.dto;
+package com.festago.festival.dto;
-import com.festago.domain.Festival;
-import com.festago.domain.Stage;
+import com.festago.festival.domain.Festival;
+import com.festago.stage.domain.Stage;
import java.time.LocalDate;
import java.util.List;
-public record FestivalDetailResponse(Long id,
- String name,
- LocalDate startDate,
- LocalDate endDate,
- String thumbnail,
- List stages) {
+public record FestivalDetailResponse(
+ Long id,
+ Long schoolId,
+ String name,
+ LocalDate startDate,
+ LocalDate endDate,
+ String thumbnail,
+ List stages) {
public static FestivalDetailResponse of(Festival festival, List stages) {
List stageResponses = stages.stream()
@@ -18,6 +20,7 @@ public static FestivalDetailResponse of(Festival festival, List stages) {
.toList();
return new FestivalDetailResponse(
festival.getId(),
+ festival.getSchool().getId(),
festival.getName(),
festival.getStartDate(),
festival.getEndDate(),
diff --git a/backend/src/main/java/com/festago/dto/FestivalDetailStageResponse.java b/backend/src/main/java/com/festago/festival/dto/FestivalDetailStageResponse.java
similarity index 55%
rename from backend/src/main/java/com/festago/dto/FestivalDetailStageResponse.java
rename to backend/src/main/java/com/festago/festival/dto/FestivalDetailStageResponse.java
index 04bf9a9ec..e9704cec8 100644
--- a/backend/src/main/java/com/festago/dto/FestivalDetailStageResponse.java
+++ b/backend/src/main/java/com/festago/festival/dto/FestivalDetailStageResponse.java
@@ -1,14 +1,15 @@
-package com.festago.dto;
+package com.festago.festival.dto;
-import com.festago.domain.Stage;
+import com.festago.stage.domain.Stage;
import java.time.LocalDateTime;
import java.util.List;
-public record FestivalDetailStageResponse(Long id,
- LocalDateTime startTime,
- LocalDateTime ticketOpenTime,
- String lineUp,
- List tickets) {
+public record FestivalDetailStageResponse(
+ Long id,
+ LocalDateTime startTime,
+ LocalDateTime ticketOpenTime,
+ String lineUp,
+ List tickets) {
public static FestivalDetailStageResponse from(Stage stage) {
List tickets = stage.getTickets().stream()
diff --git a/backend/src/main/java/com/festago/festival/dto/FestivalDetailTicketResponse.java b/backend/src/main/java/com/festago/festival/dto/FestivalDetailTicketResponse.java
new file mode 100644
index 000000000..971042219
--- /dev/null
+++ b/backend/src/main/java/com/festago/festival/dto/FestivalDetailTicketResponse.java
@@ -0,0 +1,22 @@
+package com.festago.festival.dto;
+
+import com.festago.ticket.domain.Ticket;
+import com.festago.ticket.domain.TicketAmount;
+import com.festago.ticket.domain.TicketType;
+
+public record FestivalDetailTicketResponse(
+ Long id,
+ TicketType ticketType,
+ Integer totalAmount,
+ Integer remainAmount) {
+
+ public static FestivalDetailTicketResponse from(Ticket ticket) {
+ TicketAmount ticketAmount = ticket.getTicketAmount();
+ return new FestivalDetailTicketResponse(
+ ticket.getId(),
+ ticket.getTicketType(),
+ ticketAmount.getTotalAmount(),
+ ticketAmount.calculateRemainAmount()
+ );
+ }
+}
diff --git a/backend/src/main/java/com/festago/dto/FestivalResponse.java b/backend/src/main/java/com/festago/festival/dto/FestivalResponse.java
similarity index 53%
rename from backend/src/main/java/com/festago/dto/FestivalResponse.java
rename to backend/src/main/java/com/festago/festival/dto/FestivalResponse.java
index 5ff54e98e..ad4bac4ff 100644
--- a/backend/src/main/java/com/festago/dto/FestivalResponse.java
+++ b/backend/src/main/java/com/festago/festival/dto/FestivalResponse.java
@@ -1,13 +1,20 @@
-package com.festago.dto;
+package com.festago.festival.dto;
-import com.festago.domain.Festival;
+import com.festago.festival.domain.Festival;
import java.time.LocalDate;
-public record FestivalResponse(Long id, String name, LocalDate startDate, LocalDate endDate, String thumbnail) {
+public record FestivalResponse(
+ Long id,
+ Long schoolId,
+ String name,
+ LocalDate startDate,
+ LocalDate endDate,
+ String thumbnail) {
public static FestivalResponse from(Festival festival) {
return new FestivalResponse(
festival.getId(),
+ festival.getSchool().getId(),
festival.getName(),
festival.getStartDate(),
festival.getEndDate(),
diff --git a/backend/src/main/java/com/festago/festival/dto/FestivalUpdateRequest.java b/backend/src/main/java/com/festago/festival/dto/FestivalUpdateRequest.java
new file mode 100644
index 000000000..2609b7451
--- /dev/null
+++ b/backend/src/main/java/com/festago/festival/dto/FestivalUpdateRequest.java
@@ -0,0 +1,16 @@
+package com.festago.festival.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import java.time.LocalDate;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.format.annotation.DateTimeFormat.ISO;
+
+public record FestivalUpdateRequest(
+ @NotBlank(message = "name은 공백일 수 없습니다.") String name,
+ @NotNull(message = "startDate는 null일 수 없습니다.") @DateTimeFormat(iso = ISO.DATE) LocalDate startDate,
+ @NotNull(message = "endDate는 null일 수 없습니다.") @DateTimeFormat(iso = ISO.DATE) LocalDate endDate,
+ String thumbnail
+) {
+
+}
diff --git a/backend/src/main/java/com/festago/dto/FestivalsResponse.java b/backend/src/main/java/com/festago/festival/dto/FestivalsResponse.java
similarity index 70%
rename from backend/src/main/java/com/festago/dto/FestivalsResponse.java
rename to backend/src/main/java/com/festago/festival/dto/FestivalsResponse.java
index a6c556648..44b04c1b1 100644
--- a/backend/src/main/java/com/festago/dto/FestivalsResponse.java
+++ b/backend/src/main/java/com/festago/festival/dto/FestivalsResponse.java
@@ -1,12 +1,13 @@
-package com.festago.dto;
+package com.festago.festival.dto;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toList;
-import com.festago.domain.Festival;
+import com.festago.festival.domain.Festival;
import java.util.List;
-public record FestivalsResponse(List festivals) {
+public record FestivalsResponse(
+ List festivals) {
public static FestivalsResponse from(List festivals) {
return festivals.stream()
diff --git a/backend/src/main/java/com/festago/domain/FestivalRepository.java b/backend/src/main/java/com/festago/festival/repository/FestivalRepository.java
similarity index 62%
rename from backend/src/main/java/com/festago/domain/FestivalRepository.java
rename to backend/src/main/java/com/festago/festival/repository/FestivalRepository.java
index f17005af6..10b3c02c6 100644
--- a/backend/src/main/java/com/festago/domain/FestivalRepository.java
+++ b/backend/src/main/java/com/festago/festival/repository/FestivalRepository.java
@@ -1,5 +1,6 @@
-package com.festago.domain;
+package com.festago.festival.repository;
+import com.festago.festival.domain.Festival;
import org.springframework.data.jpa.repository.JpaRepository;
public interface FestivalRepository extends JpaRepository {
diff --git a/backend/src/main/java/com/festago/application/MemberService.java b/backend/src/main/java/com/festago/member/application/MemberService.java
similarity index 59%
rename from backend/src/main/java/com/festago/application/MemberService.java
rename to backend/src/main/java/com/festago/member/application/MemberService.java
index d5f094d6f..bd8cdf397 100644
--- a/backend/src/main/java/com/festago/application/MemberService.java
+++ b/backend/src/main/java/com/festago/member/application/MemberService.java
@@ -1,23 +1,21 @@
-package com.festago.application;
+package com.festago.member.application;
-import com.festago.domain.Member;
-import com.festago.domain.MemberRepository;
-import com.festago.dto.MemberProfileResponse;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.NotFoundException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.NotFoundException;
+import com.festago.member.domain.Member;
+import com.festago.member.dto.MemberProfileResponse;
+import com.festago.member.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Transactional
@Service
+@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
- public MemberService(MemberRepository memberRepository) {
- this.memberRepository = memberRepository;
- }
-
@Transactional(readOnly = true)
public MemberProfileResponse findMemberProfile(Long memberId) {
Member member = memberRepository.findById(memberId)
diff --git a/backend/src/main/java/com/festago/member/domain/Member.java b/backend/src/main/java/com/festago/member/domain/Member.java
new file mode 100644
index 000000000..2ee68e879
--- /dev/null
+++ b/backend/src/main/java/com/festago/member/domain/Member.java
@@ -0,0 +1,129 @@
+package com.festago.member.domain;
+
+import com.festago.auth.domain.SocialType;
+import com.festago.common.domain.BaseTimeEntity;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import java.time.LocalDateTime;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.SQLDelete;
+import org.hibernate.annotations.Where;
+
+@Entity
+@SQLDelete(sql = "UPDATE member SET deleted_at = now(), nickname = '탈퇴한 회원', profile_image = '', social_id = null WHERE id=?")
+@Where(clause = "deleted_at is null")
+@Table(
+ uniqueConstraints = {
+ @UniqueConstraint(
+ name = "SOCIAL_UNIQUE",
+ columnNames = {
+ "socialId",
+ "socialType"
+ }
+ )
+ }
+)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Member extends BaseTimeEntity {
+
+ private static final String DEFAULT_IMAGE_URL = "https://festa-go.site/images/default-profile.png";
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Size(max = 255)
+ private String socialId;
+
+ @NotNull
+ @Enumerated(value = EnumType.STRING)
+ private SocialType socialType;
+
+ @NotNull
+ @Size(max = 30)
+ private String nickname;
+
+ @NotNull
+ @Size(max = 255)
+ private String profileImage;
+
+ private LocalDateTime deletedAt = null;
+
+ public Member(Long id) {
+ this.id = id;
+ }
+
+ public Member(String socialId, SocialType socialType, String nickname, String profileImage) {
+ this(null, socialId, socialType, nickname, profileImage);
+ }
+
+ public Member(Long id, String socialId, SocialType socialType, String nickname, String profileImage) {
+ validate(socialId, socialType, nickname, profileImage);
+ this.id = id;
+ this.socialId = socialId;
+ this.socialType = socialType;
+ this.nickname = nickname;
+ this.profileImage = (profileImage != null) ? profileImage : DEFAULT_IMAGE_URL;
+ }
+
+ private void validate(String socialId, SocialType socialType, String nickname, String profileImage) {
+ checkNotNull(socialId, socialType, nickname);
+ checkLength(socialId, nickname, profileImage);
+ }
+
+ private void checkNotNull(String socialId, SocialType socialType, String nickname) {
+ if (socialId == null ||
+ socialType == null ||
+ nickname == null) {
+ throw new IllegalArgumentException("Member 는 허용되지 않은 null 값으로 생성할 수 없습니다.");
+ }
+ }
+
+ private void checkLength(String socialId, String nickname, String profileImage) {
+ if (overLength(socialId, 255) ||
+ overLength(nickname, 30) ||
+ overLength(profileImage, 255)) {
+ throw new IllegalArgumentException("Member 의 필드로 허용된 길이를 넘은 column 을 넣을 수 없습니다.");
+ }
+ }
+
+ private boolean overLength(String target, int maxLength) {
+ if (target == null) {
+ return false;
+ }
+ return target.length() > maxLength;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getSocialId() {
+ return socialId;
+ }
+
+ public SocialType getSocialType() {
+ return socialType;
+ }
+
+ public String getNickname() {
+ return nickname;
+ }
+
+ public String getProfileImage() {
+ return profileImage;
+ }
+
+ public boolean isDeleted() {
+ return deletedAt != null;
+ }
+}
diff --git a/backend/src/main/java/com/festago/dto/MemberProfileResponse.java b/backend/src/main/java/com/festago/member/dto/MemberProfileResponse.java
similarity index 79%
rename from backend/src/main/java/com/festago/dto/MemberProfileResponse.java
rename to backend/src/main/java/com/festago/member/dto/MemberProfileResponse.java
index aeec3fb2c..70e30baf7 100644
--- a/backend/src/main/java/com/festago/dto/MemberProfileResponse.java
+++ b/backend/src/main/java/com/festago/member/dto/MemberProfileResponse.java
@@ -1,6 +1,6 @@
-package com.festago.dto;
+package com.festago.member.dto;
-import com.festago.domain.Member;
+import com.festago.member.domain.Member;
public record MemberProfileResponse(
Long memberId,
diff --git a/backend/src/main/java/com/festago/domain/MemberRepository.java b/backend/src/main/java/com/festago/member/repository/MemberRepository.java
similarity index 78%
rename from backend/src/main/java/com/festago/domain/MemberRepository.java
rename to backend/src/main/java/com/festago/member/repository/MemberRepository.java
index b9cab1507..e9536e0af 100644
--- a/backend/src/main/java/com/festago/domain/MemberRepository.java
+++ b/backend/src/main/java/com/festago/member/repository/MemberRepository.java
@@ -1,6 +1,7 @@
-package com.festago.domain;
+package com.festago.member.repository;
import com.festago.auth.domain.SocialType;
+import com.festago.member.domain.Member;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
diff --git a/backend/src/main/java/com/festago/presentation/AdminController.java b/backend/src/main/java/com/festago/presentation/AdminController.java
index 1d58abbbf..adb63cff4 100644
--- a/backend/src/main/java/com/festago/presentation/AdminController.java
+++ b/backend/src/main/java/com/festago/presentation/AdminController.java
@@ -1,48 +1,54 @@
package com.festago.presentation;
-import com.festago.application.AdminService;
-import com.festago.application.FestivalService;
-import com.festago.application.StageService;
-import com.festago.application.TicketService;
+import com.festago.admin.application.AdminService;
+import com.festago.admin.dto.AdminResponse;
import com.festago.auth.annotation.Admin;
import com.festago.auth.application.AdminAuthService;
import com.festago.auth.dto.AdminLoginRequest;
import com.festago.auth.dto.AdminSignupRequest;
import com.festago.auth.dto.AdminSignupResponse;
import com.festago.auth.dto.RootAdminInitializeRequest;
-import com.festago.dto.AdminResponse;
-import com.festago.dto.FestivalCreateRequest;
-import com.festago.dto.FestivalResponse;
-import com.festago.dto.StageCreateRequest;
-import com.festago.dto.StageResponse;
-import com.festago.dto.TicketCreateRequest;
-import com.festago.dto.TicketCreateResponse;
-import com.festago.exception.ErrorCode;
-import com.festago.exception.InternalServerException;
+import com.festago.common.exception.BadRequestException;
+import com.festago.common.exception.ErrorCode;
+import com.festago.common.exception.InternalServerException;
+import com.festago.festival.application.FestivalService;
+import com.festago.festival.dto.FestivalCreateRequest;
+import com.festago.festival.dto.FestivalResponse;
+import com.festago.festival.dto.FestivalUpdateRequest;
+import com.festago.school.application.SchoolService;
+import com.festago.school.dto.SchoolCreateRequest;
+import com.festago.school.dto.SchoolResponse;
+import com.festago.school.dto.SchoolUpdateRequest;
+import com.festago.stage.application.StageService;
+import com.festago.stage.dto.StageCreateRequest;
+import com.festago.stage.dto.StageResponse;
+import com.festago.stage.dto.StageUpdateRequest;
+import com.festago.ticket.application.TicketService;
+import com.festago.ticket.dto.TicketCreateRequest;
+import com.festago.ticket.dto.TicketCreateResponse;
import io.swagger.v3.oas.annotations.Hidden;
-import com.festago.exception.UnauthorizedException;
-import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Optional;
+import lombok.RequiredArgsConstructor;
import org.springframework.boot.info.BuildProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.servlet.ModelAndView;
-import org.springframework.web.servlet.View;
-import org.springframework.web.servlet.view.InternalResourceView;
-import org.springframework.web.servlet.view.RedirectView;
@RestController
-@RequestMapping("/admin")
+@RequestMapping("/admin/api")
@Hidden
+@RequiredArgsConstructor
public class AdminController {
private final FestivalService festivalService;
@@ -50,52 +56,84 @@ public class AdminController {
private final TicketService ticketService;
private final AdminService adminService;
private final AdminAuthService adminAuthService;
+ private final SchoolService schoolService;
private final Optional properties;
- public AdminController(FestivalService festivalService, StageService stageService, TicketService ticketService,
- AdminService adminService, AdminAuthService adminAuthService,
- Optional buildProperties) {
- this.festivalService = festivalService;
- this.stageService = stageService;
- this.ticketService = ticketService;
- this.adminService = adminService;
- this.adminAuthService = adminAuthService;
- this.properties = buildProperties;
- }
-
@PostMapping("/festivals")
- public ResponseEntity createFestival(@RequestBody FestivalCreateRequest request) {
+ public ResponseEntity createFestival(@RequestBody @Valid FestivalCreateRequest request) {
FestivalResponse response = festivalService.create(request);
return ResponseEntity.ok()
.body(response);
}
+ @PatchMapping("/festivals/{festivalId}")
+ public ResponseEntity updateFestival(@RequestBody @Valid FestivalUpdateRequest request,
+ @PathVariable Long festivalId) {
+ festivalService.update(festivalId, request);
+ return ResponseEntity.ok()
+ .build();
+ }
+
+ @DeleteMapping("/festivals/{festivalId}")
+ public ResponseEntity