diff --git a/.github/workflows/cd-back-dev.yml b/.github/workflows/cd-back-dev.yml new file mode 100644 index 000000000..c6ec9db95 --- /dev/null +++ b/.github/workflows/cd-back-dev.yml @@ -0,0 +1,43 @@ +name: CD-Back-Dev + +on: + push: + branches: + - dev + paths: 'backend/**' + + workflow_dispatch: + +defaults: + run: + working-directory: backend + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: 리포지토리 체크아웃 + uses: actions/checkout@v3 + with: + submodules: recursive + token: ${{ secrets.SUBMODULE_TOKEN }} + + - name: 자바 설치 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'zulu' + + - name: gradlew 권한 부여 + run: chmod +x gradlew + + - name: Gradle Test + run: ./gradlew test + + - name: trigger to jenkins dev cd + uses: appleboy/jenkins-action@master + with: + url: ${{ secrets.JENKINS_URL }} + user: "festago" + token: ${{ secrets.JENKINS_API_TOKEN}} + job: "festago-dev-cd" diff --git a/.github/workflows/ci-back.yml b/.github/workflows/ci-back.yml index 58b8d7159..cec9580d9 100644 --- a/.github/workflows/ci-back.yml +++ b/.github/workflows/ci-back.yml @@ -1,10 +1,6 @@ name: CI-Back on: - push: - branches: - - dev - paths: 'backend/**' pull_request: branches: - dev @@ -20,6 +16,9 @@ jobs: steps: - name: 리포지토리 체크아웃 uses: actions/checkout@v3 + with: + submodules: recursive + token: ${{ secrets.SUBMODULE_TOKEN }} - name: 자바 설치 uses: actions/setup-java@v3 diff --git a/android/festago/.idea/gradle.xml b/android/festago/.idea/gradle.xml index 8e6d21a77..72b4213c2 100644 --- a/android/festago/.idea/gradle.xml +++ b/android/festago/.idea/gradle.xml @@ -12,6 +12,7 @@ + diff --git a/android/festago/app/build.gradle.kts b/android/festago/app/build.gradle.kts index 5ff9304f5..1bc94749f 100644 --- a/android/festago/app/build.gradle.kts +++ b/android/festago/app/build.gradle.kts @@ -19,8 +19,8 @@ android { applicationId = "com.festago.festago" minSdk = 28 targetSdk = 33 - versionCode = 2 - versionName = "1.0.0" + versionCode = 3 + versionName = "1.0.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -35,7 +35,8 @@ android { buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true + isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", @@ -135,6 +136,9 @@ dependencies { // Encrypted SharedPreference implementation("androidx.security:security-crypto-ktx:1.1.0-alpha06") + + // domain + implementation(project(":domain")) } fun getSecretKey(propertyKey: String): String { diff --git a/android/festago/app/proguard-rules.pro b/android/festago/app/proguard-rules.pro index 1afc9529c..2b3412c13 100644 --- a/android/festago/app/proguard-rules.pro +++ b/android/festago/app/proguard-rules.pro @@ -22,3 +22,66 @@ # https://developers.kakao.com/docs/latest/en/getting-started/sdk-android#configure-for-shrinking-and-obfuscation-(optional) -keep class com.kakao.sdk.**.model.* { ; } + +#---------------------------------------- Retrofit +# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and +# EnclosingMethod is required to use InnerClasses. +-keepattributes Signature, InnerClasses, EnclosingMethod + +# Retrofit does reflection on method and parameter annotations. +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations + +# Keep annotation default values (e.g., retrofit2.http.Field.encoded). +-keepattributes AnnotationDefault + +# Retain service method parameters when optimizing. +-keepclassmembers,allowshrinking,allowobfuscation interface * { + @retrofit2.http.* ; +} + +# Ignore annotation used for build tooling. +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +# Ignore JSR 305 annotations for embedding nullability information. +-dontwarn javax.annotation.** + +# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. +-dontwarn kotlin.Unit + +# Top-level functions that can only be used by Kotlin. +-dontwarn retrofit2.KotlinExtensions +-dontwarn retrofit2.KotlinExtensions$* + +# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy +# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> + +# Keep inherited services. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface * extends <1> + +# With R8 full mode generic signatures are stripped for classes that are not +# kept. Suspend functions are wrapped in continuations where the type argument +# is used. +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation + +# R8 full mode strips generic signatures from return types if not kept. +-if interface * { @retrofit2.http.* public *** *(...); } +-keep,allowoptimization,allowshrinking,allowobfuscation class <3> + +# With R8 full mode generic signatures are stripped for classes that are not kept. +-keep,allowobfuscation,allowshrinking class retrofit2.Response + +#---------------------------------------- Glide +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep class * extends com.bumptech.glide.module.AppGlideModule { + (...); +} +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} +-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder { + *** rewind(); +} diff --git a/android/festago/app/src/main/ic_festago_logo-playstore.png b/android/festago/app/src/main/ic_festago_logo-playstore.png index 2b186954b..bc1cf755b 100644 Binary files a/android/festago/app/src/main/ic_festago_logo-playstore.png and b/android/festago/app/src/main/ic_festago_logo-playstore.png differ 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 5bc117046..91aab83e0 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 @@ -2,29 +2,57 @@ package com.festago.festago import android.app.Application import com.festago.festago.analytics.FirebaseAnalyticsHelper -import com.festago.festago.data.datasource.AuthLocalDataSource -import com.festago.festago.data.retrofit.AuthRetrofitClient -import com.festago.festago.data.retrofit.NormalRetrofitClient +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 com.kakao.sdk.common.KakaoSdk class FestagoApplication : Application() { override fun onCreate() { super.onCreate() - FirebaseAnalyticsHelper.init(applicationContext) + initKakaoSdk() + initRepositoryContainer() + initFirebaseContainer() + } + + private fun initKakaoSdk() { KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY) - initRetrofit() } - private fun initRetrofit() { - val authLocalDataSource = AuthLocalDataSource.getInstance(applicationContext) - NormalRetrofitClient.init(BuildConfig.BASE_URL) - AuthRetrofitClient.init(BuildConfig.BASE_URL) { - authLocalDataSource.token ?: NULL_TOKEN - } + 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 initFirebaseContainer() { + FirebaseAnalyticsHelper.init(applicationContext) + analysisContainer = AnalysisContainer() } - companion object { - private const val NULL_TOKEN = "null" + companion object DependencyContainer { + lateinit var normalServiceContainer: NormalServiceContainer + lateinit var authServiceContainer: AuthServiceContainer + lateinit var repositoryContainer: RepositoryContainer + lateinit var analysisContainer: AnalysisContainer + lateinit var tokenContainer: TokenContainer } } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/RetrofitClient.kt b/android/festago/app/src/main/java/com/festago/festago/data/RetrofitClient.kt deleted file mode 100644 index e69de29bb..000000000 diff --git a/android/festago/app/src/main/java/com/festago/festago/data/datasource/AuthDataSource.kt b/android/festago/app/src/main/java/com/festago/festago/data/datasource/TokenDataSource.kt similarity index 71% rename from android/festago/app/src/main/java/com/festago/festago/data/datasource/AuthDataSource.kt rename to android/festago/app/src/main/java/com/festago/festago/data/datasource/TokenDataSource.kt index 3a6e9453b..6d03bbe75 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/datasource/AuthDataSource.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/datasource/TokenDataSource.kt @@ -1,5 +1,5 @@ package com.festago.festago.data.datasource -interface AuthDataSource { +interface TokenDataSource { var token: String? } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/datasource/AuthLocalDataSource.kt b/android/festago/app/src/main/java/com/festago/festago/data/datasource/TokenLocalDataSource.kt similarity index 71% rename from android/festago/app/src/main/java/com/festago/festago/data/datasource/AuthLocalDataSource.kt rename to android/festago/app/src/main/java/com/festago/festago/data/datasource/TokenLocalDataSource.kt index f975965ba..6cce56927 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/datasource/AuthLocalDataSource.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/datasource/TokenLocalDataSource.kt @@ -5,9 +5,7 @@ import android.content.SharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey -class AuthLocalDataSource private constructor( - context: Context, -) : AuthDataSource { +class TokenLocalDataSource(context: Context) : TokenDataSource { private val sharedPreference: SharedPreferences by lazy { val masterKeyAlias = MasterKey @@ -27,13 +25,5 @@ class AuthLocalDataSource private constructor( companion object { private const val ENCRYPTED_PREF_FILE = "encrypted_pref_file" private const val TOKEN_KEY = "TOKEN_KEY" - - private var INSTANCE: AuthLocalDataSource? = null - - @Synchronized - fun getInstance(context: Context): AuthLocalDataSource { - return INSTANCE ?: AuthLocalDataSource(context.applicationContext) - .also { INSTANCE = it } - } } } 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 9d8852844..ba8e2195a 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 @@ -1,6 +1,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.Festival +import com.festago.festago.model.Festival import kotlinx.serialization.Serializable import java.time.LocalDate diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/FestivalsResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/FestivalsResponse.kt index 39d7e3d27..19711ffc1 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/FestivalsResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/FestivalsResponse.kt @@ -1,6 +1,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.Festival +import com.festago.festago.model.Festival import kotlinx.serialization.Serializable @Serializable diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketFestivalResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketFestivalResponse.kt index d6d2dc9c5..a21586108 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketFestivalResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketFestivalResponse.kt @@ -1,6 +1,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.MemberTicketFestival +import com.festago.festago.model.MemberTicketFestival import kotlinx.serialization.Serializable @Serializable diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketResponse.kt index 05c17beda..eaed1061f 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketResponse.kt @@ -1,7 +1,7 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.Ticket -import com.festago.festago.domain.model.TicketCondition +import com.festago.festago.model.Ticket +import com.festago.festago.model.TicketCondition import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.time.LocalDateTime diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketsResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketsResponse.kt index 0d30d5bca..695962c40 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketsResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/MemberTicketsResponse.kt @@ -1,6 +1,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.Ticket +import com.festago.festago.model.Ticket import kotlinx.serialization.Serializable @Serializable 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 32bff55b4..bf83c7e33 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 @@ -1,6 +1,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.Reservation +import com.festago.festago.model.Reservation import kotlinx.serialization.Serializable import java.time.LocalDate 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 5d2b60c1f..f01c23a8f 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,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.ReservationStage +import com.festago.festago.model.ReservationStage import kotlinx.serialization.Serializable import java.time.LocalDateTime 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 17c90de50..36966445c 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,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.ReservationTicket +import com.festago.festago.model.ReservationTicket import kotlinx.serialization.Serializable @Serializable 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 e48d68f58..e7a68e221 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,6 +1,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.ReservationTicket +import com.festago.festago.model.ReservationTicket import kotlinx.serialization.Serializable @Serializable diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservedTicketResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservedTicketResponse.kt index 1d771b3dc..ccaf6f561 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservedTicketResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/ReservedTicketResponse.kt @@ -1,6 +1,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.ReservedTicket +import com.festago.festago.model.ReservedTicket import kotlinx.serialization.Serializable import java.time.LocalDateTime diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/StageResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/StageResponse.kt index a801e2642..aa2f2730f 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/StageResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/StageResponse.kt @@ -1,6 +1,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.Stage +import com.festago.festago.model.Stage import kotlinx.serialization.Serializable import java.time.LocalDateTime diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/TicketCodeDto.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/TicketCodeDto.kt index 85e34181c..0f1d61a7c 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/TicketCodeDto.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/TicketCodeDto.kt @@ -1,6 +1,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.TicketCode +import com.festago.festago.model.TicketCode import kotlinx.serialization.Serializable @Serializable diff --git a/android/festago/app/src/main/java/com/festago/festago/data/dto/UserProfileResponse.kt b/android/festago/app/src/main/java/com/festago/festago/data/dto/UserProfileResponse.kt index 9ef431445..4adab45a3 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/dto/UserProfileResponse.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/dto/UserProfileResponse.kt @@ -1,6 +1,6 @@ package com.festago.festago.data.dto -import com.festago.festago.domain.model.UserProfile +import com.festago.festago.model.UserProfile import kotlinx.serialization.Serializable @Serializable 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 3762d2654..2594cb3c4 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,38 +1,29 @@ package com.festago.festago.data.repository -import com.festago.festago.data.datasource.AuthDataSource -import com.festago.festago.data.dto.OauthRequest -import com.festago.festago.data.service.AuthRetrofitService import com.festago.festago.data.service.UserRetrofitService import com.festago.festago.data.util.runCatchingWithErrorHandler -import com.festago.festago.domain.repository.AuthRepository +import com.festago.festago.repository.AuthRepository +import com.festago.festago.repository.TokenRepository import com.kakao.sdk.user.UserApiClient class AuthDefaultRepository( - private val authRetrofitService: AuthRetrofitService, - private val authDataSource: AuthDataSource, private val userRetrofitService: UserRetrofitService, + private val tokenRepository: TokenRepository, ) : AuthRepository { override val isSigned: Boolean - get() = authDataSource.token != null + get() = tokenRepository.token != null override val token: String? - get() = authDataSource.token + get() = tokenRepository.token override suspend fun signIn(socialType: String, token: String): Result { - authRetrofitService.getOauthToken(OauthRequest(socialType, token)) - .runCatchingWithErrorHandler() - .getOrElse { error -> return Result.failure(error) } - .let { - authDataSource.token = it.accessToken - return Result.success(Unit) - } + return tokenRepository.signIn(socialType, token) } override suspend fun signOut(): Result { UserApiClient.instance.logout { - authDataSource.token = null + tokenRepository.token = null } return Result.success(Unit) } @@ -43,7 +34,7 @@ class AuthDefaultRepository( .let { UserApiClient.instance.unlink { error -> if (error == null) { - authDataSource.token = null + tokenRepository.token = null } } return Result.success(Unit) 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 3ea5ff704..d7fece84d 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 @@ -2,9 +2,9 @@ package com.festago.festago.data.repository import com.festago.festago.data.service.FestivalRetrofitService import com.festago.festago.data.util.runCatchingWithErrorHandler -import com.festago.festago.domain.model.Festival -import com.festago.festago.domain.model.Reservation -import com.festago.festago.domain.repository.FestivalRepository +import com.festago.festago.model.Festival +import com.festago.festago.model.Reservation +import com.festago.festago.repository.FestivalRepository class FestivalDefaultRepository( private val festivalRetrofitService: FestivalRetrofitService, 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 3009faec8..8519a45fe 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 @@ -2,8 +2,8 @@ package com.festago.festago.data.repository import com.festago.festago.data.service.ReservationTicketRetrofitService import com.festago.festago.data.util.runCatchingWithErrorHandler -import com.festago.festago.domain.model.ReservationTicket -import com.festago.festago.domain.repository.ReservationTicketRepository +import com.festago.festago.model.ReservationTicket +import com.festago.festago.repository.ReservationTicketRepository class ReservationTicketDefaultRepository( private val reservationTicketRetrofitService: ReservationTicketRetrofitService, 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 626471362..a04facbac 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 @@ -3,10 +3,10 @@ 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.domain.model.ReservedTicket -import com.festago.festago.domain.model.Ticket -import com.festago.festago.domain.model.TicketCode -import com.festago.festago.domain.repository.TicketRepository +import com.festago.festago.model.ReservedTicket +import com.festago.festago.model.Ticket +import com.festago.festago.model.TicketCode +import com.festago.festago.repository.TicketRepository class TicketDefaultRepository( private val ticketRetrofitService: TicketRetrofitService, 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 new file mode 100644 index 000000000..3b9c7d87c --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/repository/TokenDefaultRepository.kt @@ -0,0 +1,39 @@ +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 3937b55e4..5babfa87c 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 @@ -2,8 +2,8 @@ package com.festago.festago.data.repository import com.festago.festago.data.service.UserRetrofitService import com.festago.festago.data.util.runCatchingWithErrorHandler -import com.festago.festago.domain.model.UserProfile -import com.festago.festago.domain.repository.UserRepository +import com.festago.festago.model.UserProfile +import com.festago.festago.repository.UserRepository class UserDefaultRepository( private val userProfileService: UserRetrofitService, 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 ae5e12e3c..80151658b 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 @@ -3,15 +3,23 @@ package com.festago.festago.data.retrofit import okhttp3.Interceptor import okhttp3.Response -class AuthInterceptor(private val tokenProvider: () -> String) : Interceptor { +class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - .newBuilder() - .addHeader(HEADER_AUTHORIZATION, AUTHORIZATION_TOKEN_FORMAT.format(tokenProvider())) - .build() - return chain.proceed(request) + val response = chain.proceed(request = getNewRequest(chain)) + if (response.code == 401) { + response.close() + + tokenManager.refreshToken() + return chain.proceed(request = getNewRequest(chain)) + } + return response } + private fun getNewRequest(chain: Interceptor.Chain) = chain.request() + .newBuilder() + .addHeader(HEADER_AUTHORIZATION, AUTHORIZATION_TOKEN_FORMAT.format(tokenManager.token)) + .build() + companion object { private const val HEADER_AUTHORIZATION = "Authorization" private const val AUTHORIZATION_TOKEN_FORMAT = "Bearer %s" 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 new file mode 100644 index 000000000..4cb33e5a4 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/data/retrofit/TokenManager.kt @@ -0,0 +1,20 @@ +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/AuthRetrofitService.kt b/android/festago/app/src/main/java/com/festago/festago/data/service/TokenRetrofitService.kt similarity index 91% rename from android/festago/app/src/main/java/com/festago/festago/data/service/AuthRetrofitService.kt rename to android/festago/app/src/main/java/com/festago/festago/data/service/TokenRetrofitService.kt index 7257be0ab..6e54d2a89 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/service/AuthRetrofitService.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/service/TokenRetrofitService.kt @@ -6,7 +6,7 @@ import retrofit2.Response import retrofit2.http.Body import retrofit2.http.POST -interface AuthRetrofitService { +interface TokenRetrofitService { @POST("/auth/oauth2") suspend fun getOauthToken( 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 new file mode 100644 index 000000000..ad63cda83 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/di/AnalysisContainer.kt @@ -0,0 +1,8 @@ +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/data/retrofit/AuthRetrofitClient.kt b/android/festago/app/src/main/java/com/festago/festago/di/AuthServiceContainer.kt similarity index 51% rename from android/festago/app/src/main/java/com/festago/festago/data/retrofit/AuthRetrofitClient.kt rename to android/festago/app/src/main/java/com/festago/festago/di/AuthServiceContainer.kt index ee983108c..05e512157 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/retrofit/AuthRetrofitClient.kt +++ b/android/festago/app/src/main/java/com/festago/festago/di/AuthServiceContainer.kt @@ -1,5 +1,6 @@ -package com.festago.festago.data.retrofit +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 @@ -8,9 +9,18 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import retrofit2.Retrofit -object AuthRetrofitClient { +class AuthServiceContainer(baseUrl: String, tokenContainer: TokenContainer) { - private lateinit var authRetrofit: Retrofit + 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) @@ -19,17 +29,4 @@ object AuthRetrofitClient { val userRetrofitService: UserRetrofitService by lazy { authRetrofit.create(UserRetrofitService::class.java) } - fun init(baseUrl: String = "", tokenProvider: () -> String) { - if (::authRetrofit.isInitialized) return - authRetrofit = Retrofit.Builder() - .baseUrl(baseUrl) - .client(getOkHttpClient(tokenProvider)) - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .build() - } - - private fun getOkHttpClient(tokenProvider: () -> String): OkHttpClient = OkHttpClient - .Builder() - .addInterceptor(AuthInterceptor(tokenProvider)) - .build() } 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 new file mode 100644 index 000000000..a1b9aa242 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/di/LocalDataSourceContainer.kt @@ -0,0 +1,9 @@ +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/data/retrofit/NormalRetrofitClient.kt b/android/festago/app/src/main/java/com/festago/festago/di/NormalServiceContainer.kt similarity index 52% rename from android/festago/app/src/main/java/com/festago/festago/data/retrofit/NormalRetrofitClient.kt rename to android/festago/app/src/main/java/com/festago/festago/di/NormalServiceContainer.kt index f9bd27ba2..509fd36f0 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/retrofit/NormalRetrofitClient.kt +++ b/android/festago/app/src/main/java/com/festago/festago/di/NormalServiceContainer.kt @@ -1,34 +1,28 @@ -package com.festago.festago.data.retrofit +package com.festago.festago.di -import com.festago.festago.data.service.AuthRetrofitService 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 -object NormalRetrofitClient { - - private lateinit var normalRetrofit: 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 authRetrofitService: AuthRetrofitService by lazy { - normalRetrofit.create(AuthRetrofitService::class.java) + val tokenRetrofitService: TokenRetrofitService by lazy { + normalRetrofit.create(TokenRetrofitService::class.java) } val reservationTicketRetrofitService: ReservationTicketRetrofitService by lazy { normalRetrofit.create(ReservationTicketRetrofitService::class.java) } - - fun init(baseUrl: String = "") { - if (::normalRetrofit.isInitialized) return - normalRetrofit = Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .build() - } } 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 new file mode 100644 index 000000000..886a0d2e0 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/di/RepositoryContainer.kt @@ -0,0 +1,43 @@ +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 new file mode 100644 index 000000000..d17a15af9 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/di/TokenContainer.kt @@ -0,0 +1,15 @@ +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/domain/model/Token.kt b/android/festago/app/src/main/java/com/festago/festago/domain/model/Token.kt deleted file mode 100644 index 510e6b9f9..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/Token.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.festago.festago.domain.model - -data class Token( - val accessToken: String, - val refreshToken: String = "", -) diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/repository/UserRepository.kt b/android/festago/app/src/main/java/com/festago/festago/domain/repository/UserRepository.kt deleted file mode 100644 index 3ad05c019..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/domain/repository/UserRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.festago.festago.domain.repository - -import com.festago.festago.domain.model.UserProfile - -interface UserRepository { - suspend fun loadUserProfile(): Result -} 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 index 2bd6f6c6a..b69bfa493 100644 --- 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 @@ -1,6 +1,6 @@ package com.festago.festago.presentation.mapper -import com.festago.festago.domain.model.Festival +import com.festago.festago.model.Festival import com.festago.festago.presentation.model.FestivalUiModel fun Festival.toPresentation(): FestivalUiModel = 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 index e94f1fae5..e69de29bb 100644 --- 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 @@ -1,22 +0,0 @@ -package com.festago.festago.presentation.mapper - -import com.festago.festago.domain.model.Reservation -import com.festago.festago.presentation.model.ReservationUiModel - -fun Reservation.toPresentation() = ReservationUiModel( - endDate = endDate, - id = id, - name = name, - reservationStages = reservationStages.map { it.toPresentation() }, - startDate = startDate, - thumbnail = thumbnail, -) - -fun ReservationUiModel.toDomain() = Reservation( - endDate = endDate, - id = id, - name = name, - reservationStages = reservationStages.map { it.toDomain() }, - startDate = startDate, - thumbnail = thumbnail, -) 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 index 7c7f630b0..4c5c35f64 100644 --- 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 @@ -1,8 +1,8 @@ package com.festago.festago.presentation.mapper -import com.festago.festago.domain.model.ReservationStage +import com.festago.festago.model.ReservationStage import com.festago.festago.presentation.model.ReservationStageUiModel -import com.festago.festago.presentation.model.TicketReserveItemUiModel +import com.festago.festago.presentation.ui.ticketreserve.TicketReserveItemUiState import java.time.LocalDateTime fun List.toPresentation() = map { it.toPresentation() } @@ -24,21 +24,7 @@ fun ReservationStageUiModel.toDomain() = ReservationStage( ticketOpenTime = ticketOpenTime, ) -fun ReservationStageUiModel.toTicketReserveItem(isSigned: Boolean) = TicketReserveItemUiModel( - id = id, - lineUp = lineUp, - startTime = startTime, - ticketOpenTime = ticketOpenTime, - reservationTickets = reservationTickets, - canReserve = canReserve, - isSigned = isSigned, -) - -fun List.toTicketReserveItem(isSigned: Boolean) = map { - it.toTicketReserveItem(isSigned) -} - -fun TicketReserveItemUiModel.toPresentation() = ReservationStageUiModel( +fun TicketReserveItemUiState.toPresentation() = ReservationStageUiModel( id = id, lineUp = lineUp, startTime = startTime, 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 index 9a8cb6237..4f17bb0f2 100644 --- 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 @@ -1,6 +1,6 @@ package com.festago.festago.presentation.mapper -import com.festago.festago.domain.model.ReservationTicket +import com.festago.festago.model.ReservationTicket import com.festago.festago.presentation.model.ReservationTicketUiModel fun ReservationTicket.toPresentation() = ReservationTicketUiModel( 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 index c9cca3ac4..81eaaaca3 100644 --- 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 @@ -1,6 +1,6 @@ package com.festago.festago.presentation.mapper -import com.festago.festago.domain.model.ReservedTicket +import com.festago.festago.model.ReservedTicket import com.festago.festago.presentation.model.ReservedTicketUiModel fun ReservedTicket.toPresentation() = ReservedTicketUiModel( 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 index 445d63596..f6ca17e9b 100644 --- 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 @@ -1,6 +1,6 @@ package com.festago.festago.presentation.mapper -import com.festago.festago.domain.model.Stage +import com.festago.festago.model.Stage import com.festago.festago.presentation.model.StageUiModel fun Stage.toPresentation() = StageUiModel( 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 index 8766943c6..56117dafd 100644 --- 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 @@ -1,6 +1,6 @@ package com.festago.festago.presentation.mapper -import com.festago.festago.domain.model.TicketCode +import com.festago.festago.model.TicketCode import com.festago.festago.presentation.model.TicketCodeUiModel fun TicketCode.toPresentation(): TicketCodeUiModel = TicketCodeUiModel(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 index b3c15f5ff..9c8506a44 100644 --- 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 @@ -1,9 +1,7 @@ package com.festago.festago.presentation.mapper -import com.festago.festago.domain.model.Ticket -import com.festago.festago.presentation.model.MemberTicketUiModel +import com.festago.festago.model.Ticket import com.festago.festago.presentation.model.TicketUiModel -import java.time.LocalDateTime fun Ticket.toPresentation(): TicketUiModel = TicketUiModel( id = id, @@ -17,21 +15,5 @@ fun Ticket.toPresentation(): TicketUiModel = TicketUiModel( festivalThumbnail = festivalTicket.thumbnail, ) -fun TicketUiModel.toMemberTicketModel(): MemberTicketUiModel = MemberTicketUiModel( - id = id, - number = number, - entryTime = entryTime, - condition = condition, - stage = stage, - reserveAt = reserveAt, - festivalId = festivalId, - festivalName = festivalName, - festivalThumbnail = festivalThumbnail, - canEntry = LocalDateTime.now().isAfter(entryTime), -) - fun List.toPresentation(): List = this.map { ticket -> ticket.toPresentation() } - -fun List.toMemberTicketModel(): List = - this.map { ticketUiModel -> ticketUiModel.toMemberTicketModel() } 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 index 87b10612c..c74b34fd7 100644 --- 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 @@ -1,6 +1,6 @@ package com.festago.festago.presentation.mapper -import com.festago.festago.domain.model.TicketCondition +import com.festago.festago.model.TicketCondition import com.festago.festago.presentation.model.TicketConditionUiModel fun TicketCondition.toPresentation(): TicketConditionUiModel = 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 index 53571ec1c..a959a0af5 100644 --- 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 @@ -1,6 +1,6 @@ package com.festago.festago.presentation.mapper -import com.festago.festago.domain.model.UserProfile +import com.festago.festago.model.UserProfile import com.festago.festago.presentation.model.UserProfileUiModel fun UserProfile.toPresentation(): UserProfileUiModel = UserProfileUiModel( 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 new file mode 100644 index 000000000..b7a1888d5 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ViewModelFactory.kt @@ -0,0 +1,69 @@ +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/home/HomeActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeActivity.kt index 255429f36..d2df6fc11 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 @@ -7,12 +7,8 @@ import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import com.festago.festago.R -import com.festago.festago.data.datasource.AuthLocalDataSource -import com.festago.festago.data.repository.AuthDefaultRepository -import com.festago.festago.data.retrofit.AuthRetrofitClient -import com.festago.festago.data.retrofit.NormalRetrofitClient import com.festago.festago.databinding.ActivityHomeBinding -import com.festago.festago.presentation.ui.home.HomeViewModel.HomeViewModelFactory +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 @@ -23,15 +19,7 @@ class HomeActivity : AppCompatActivity() { private var _binding: ActivityHomeBinding? = null private val binding get() = _binding!! - private val vm: HomeViewModel by viewModels { - HomeViewModelFactory( - AuthDefaultRepository( - authRetrofitService = NormalRetrofitClient.authRetrofitService, - authDataSource = AuthLocalDataSource.getInstance(this), - userRetrofitService = AuthRetrofitClient.userRetrofitService, - ), - ) - } + private val vm: HomeViewModel by viewModels { FestagoViewModelFactory } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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 c49cc651c..23e25f3e4 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,13 +1,12 @@ package com.festago.festago.presentation.ui.home import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.festago.festago.domain.repository.AuthRepository 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 com.festago.festago.repository.AuthRepository class HomeViewModel(private val authRepository: AuthRepository) : ViewModel() { @@ -22,17 +21,4 @@ class HomeViewModel(private val authRepository: AuthRepository) : ViewModel() { homeItemType == MY_PAGE -> _event.setValue(HomeEvent.ShowMyPage) } } - - class HomeViewModelFactory( - private val authRepository: AuthRepository, - ) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(HomeViewModel::class.java)) { - return HomeViewModel(authRepository) as T - } - throw IllegalArgumentException("Unknown ViewModel Class") - } - } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalItemUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalItemUiState.kt new file mode 100644 index 000000000..c7e93267a --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalItemUiState.kt @@ -0,0 +1,12 @@ +package com.festago.festago.presentation.ui.home.festivallist + +import java.time.LocalDate + +data class FestivalItemUiState( + val id: Long, + val name: String, + val startDate: LocalDate, + val endDate: LocalDate, + val thumbnail: String, + val onFestivalDetail: (festivalId: Long) -> Unit, +) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalItemViewHolder.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalItemViewHolder.kt index 76eaa8d53..b07700b58 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalItemViewHolder.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalItemViewHolder.kt @@ -4,29 +4,23 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.festago.festago.databinding.ItemFestivalListBinding -import com.festago.festago.presentation.model.FestivalUiModel class FestivalItemViewHolder( private val binding: ItemFestivalListBinding, - vm: FestivalListViewModel, ) : RecyclerView.ViewHolder(binding.root) { - init { - binding.vm = vm - } - - fun bind(item: FestivalUiModel) { + fun bind(item: FestivalItemUiState) { binding.festival = item } companion object { - fun of(parent: ViewGroup, vm: FestivalListViewModel): FestivalItemViewHolder { + fun of(parent: ViewGroup): FestivalItemViewHolder { val binding = ItemFestivalListBinding.inflate( LayoutInflater.from(parent.context), parent, false, ) - return FestivalItemViewHolder(binding, vm) + return FestivalItemViewHolder(binding) } } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListAdapter.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListAdapter.kt index 182e123a5..ec7cd4510 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListAdapter.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListAdapter.kt @@ -3,14 +3,11 @@ package com.festago.festago.presentation.ui.home.festivallist import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import com.festago.festago.presentation.model.FestivalUiModel -class FestivalListAdapter( - private val vm: FestivalListViewModel, -) : ListAdapter(diffUtil) { +class FestivalListAdapter : ListAdapter(diffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FestivalItemViewHolder { - return FestivalItemViewHolder.of(parent, vm) + return FestivalItemViewHolder.of(parent) } override fun onBindViewHolder(holder: FestivalItemViewHolder, position: Int) { @@ -18,17 +15,17 @@ class FestivalListAdapter( } companion object { - val diffUtil = object : DiffUtil.ItemCallback() { + val diffUtil = object : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: FestivalUiModel, - newItem: FestivalUiModel, + oldItem: FestivalItemUiState, + newItem: FestivalItemUiState, ): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame( - oldItem: FestivalUiModel, - newItem: FestivalUiModel, + oldItem: FestivalItemUiState, + newItem: FestivalItemUiState, ): Boolean { return oldItem == newItem } 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 88a4b1f0c..a69fb51c5 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 @@ -7,11 +7,8 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.festago.festago.R -import com.festago.festago.analytics.FirebaseAnalyticsHelper -import com.festago.festago.data.repository.FestivalDefaultRepository -import com.festago.festago.data.retrofit.NormalRetrofitClient import com.festago.festago.databinding.FragmentFestivalListBinding -import com.festago.festago.presentation.ui.home.festivallist.FestivalListViewModel.FestivalListViewModelFactory +import com.festago.festago.presentation.ui.FestagoViewModelFactory import com.festago.festago.presentation.ui.home.ticketlist.TicketListFragment import com.festago.festago.presentation.ui.ticketreserve.TicketReserveActivity @@ -20,12 +17,7 @@ class FestivalListFragment : Fragment(R.layout.fragment_festival_list) { private var _binding: FragmentFestivalListBinding? = null private val binding get() = _binding!! - private val vm: FestivalListViewModel by viewModels { - FestivalListViewModelFactory( - FestivalDefaultRepository(NormalRetrofitClient.festivalRetrofitService), - FirebaseAnalyticsHelper, - ) - } + private val vm: FestivalListViewModel by viewModels { FestagoViewModelFactory } private lateinit var adapter: FestivalListAdapter override fun onCreateView( @@ -55,7 +47,7 @@ class FestivalListFragment : Fragment(R.layout.fragment_festival_list) { } private fun initView() { - adapter = FestivalListAdapter(vm) + adapter = FestivalListAdapter() binding.rvFestivalList.adapter = adapter vm.loadFestivals() diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListUiState.kt index 29039b77b..eda06b3a9 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListUiState.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListUiState.kt @@ -1,11 +1,11 @@ package com.festago.festago.presentation.ui.home.festivallist -import com.festago.festago.presentation.model.FestivalUiModel - sealed interface FestivalListUiState { object Loading : FestivalListUiState - data class Success(val festivals: List) : FestivalListUiState { + data class Success( + val festivals: List + ) : FestivalListUiState { val hasFestival get() = festivals.isNotEmpty() } 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 b3cac4596..08da64fdb 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 @@ -3,15 +3,13 @@ package com.festago.festago.presentation.ui.home.festivallist import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.domain.repository.FestivalRepository -import com.festago.festago.presentation.mapper.toPresentation 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 kotlinx.coroutines.launch class FestivalListViewModel( @@ -28,7 +26,18 @@ class FestivalListViewModel( viewModelScope.launch { festivalRepository.loadFestivals() .onSuccess { - _uiState.value = FestivalListUiState.Success(it.toPresentation()) + _uiState.value = FestivalListUiState.Success( + festivals = it.map { festival -> + FestivalItemUiState( + id = festival.id, + name = festival.name, + startDate = festival.startDate, + endDate = festival.endDate, + thumbnail = festival.thumbnail, + onFestivalDetail = ::showTicketReserve, + ) + }, + ) }.onFailure { _uiState.value = FestivalListUiState.Error analyticsHelper.logNetworkFailure(KEY_LOAD_FESTIVALS_LOG, it.message.toString()) @@ -40,20 +49,6 @@ class FestivalListViewModel( _event.setValue(ShowTicketReserve(festivalId)) } - class FestivalListViewModelFactory( - private val festivalRepository: FestivalRepository, - private val analyticsHelper: AnalyticsHelper, - ) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(FestivalListViewModel::class.java)) { - return FestivalListViewModel(festivalRepository, analyticsHelper) as T - } - throw IllegalArgumentException("Unknown ViewModel Class") - } - } - companion object { private const val KEY_LOAD_FESTIVALS_LOG = "load_festivals" } 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 5eeeb15ca..a0dac3641 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 @@ -8,16 +8,9 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.festago.festago.R -import com.festago.festago.analytics.FirebaseAnalyticsHelper -import com.festago.festago.data.datasource.AuthLocalDataSource -import com.festago.festago.data.repository.AuthDefaultRepository -import com.festago.festago.data.repository.TicketDefaultRepository -import com.festago.festago.data.repository.UserDefaultRepository -import com.festago.festago.data.retrofit.AuthRetrofitClient -import com.festago.festago.data.retrofit.NormalRetrofitClient 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.home.mypage.MyPageViewModel.MyPageViewModelFactory import com.festago.festago.presentation.ui.signin.SignInActivity import com.festago.festago.presentation.ui.tickethistory.TicketHistoryActivity @@ -26,22 +19,7 @@ class MyPageFragment : Fragment(R.layout.fragment_my_page) { private var _binding: FragmentMyPageBinding? = null private val binding get() = _binding!! - private val vm: MyPageViewModel by viewModels { - MyPageViewModelFactory( - userRepository = UserDefaultRepository( - userProfileService = AuthRetrofitClient.userRetrofitService, - ), - ticketRepository = TicketDefaultRepository( - ticketRetrofitService = AuthRetrofitClient.ticketRetrofitService, - ), - authRepository = AuthDefaultRepository( - authRetrofitService = NormalRetrofitClient.authRetrofitService, - authDataSource = AuthLocalDataSource.getInstance(requireContext()), - userRetrofitService = AuthRetrofitClient.userRetrofitService, - ), - analyticsHelper = FirebaseAnalyticsHelper, - ) - } + private val vm: MyPageViewModel by viewModels { FestagoViewModelFactory } override fun onCreateView( inflater: LayoutInflater, 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 245cf4711..33b8a3db6 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 @@ -3,18 +3,16 @@ package com.festago.festago.presentation.ui.home.mypage import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.domain.repository.AuthRepository -import com.festago.festago.domain.repository.TicketRepository -import com.festago.festago.domain.repository.UserRepository 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.kakao.sdk.auth.TokenManagerProvider +import com.festago.festago.repository.AuthRepository +import com.festago.festago.repository.TicketRepository +import com.festago.festago.repository.UserRepository import kotlinx.coroutines.launch class MyPageViewModel( @@ -55,16 +53,6 @@ class MyPageViewModel( _uiState.value = current.copy(userProfile = it.toPresentation()) } }.onFailure { - if (it.message.toString().contains("401")) { - authRepository.signIn( - socialType = "KAKAO", - token = TokenManagerProvider.instance.manager.getToken()?.accessToken ?: "", - ).onSuccess { - loadUserInfo() - return - } - } - _uiState.value = MyPageUiState.Error analyticsHelper.logNetworkFailure( key = KEY_LOAD_USER_INFO, @@ -134,27 +122,6 @@ class MyPageViewModel( _event.setValue(MyPageEvent.ShowTicketHistory) } - class MyPageViewModelFactory( - private val userRepository: UserRepository, - private val ticketRepository: TicketRepository, - private val authRepository: AuthRepository, - private val analyticsHelper: AnalyticsHelper, - ) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(MyPageViewModel::class.java)) { - return MyPageViewModel( - userRepository = userRepository, - ticketRepository = ticketRepository, - authRepository = authRepository, - analyticsHelper = analyticsHelper, - ) as T - } - throw IllegalArgumentException("Unknown ViewModel Class") - } - } - companion object { private const val KEY_LOAD_USER_INFO = "loadUserInfo" private const val KEY_SIGN_OUT = "KEY_SIGN_OUT" diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListAdapter.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListAdapter.kt index fb0cfcdfb..a9b619bb9 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListAdapter.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListAdapter.kt @@ -3,14 +3,12 @@ package com.festago.festago.presentation.ui.home.ticketlist import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import com.festago.festago.presentation.model.MemberTicketUiModel -class TicketListAdapter( - private val vm: TicketListViewModel, -) : ListAdapter(ticketDiffUtil) { +class TicketListAdapter : + ListAdapter(ticketDiffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TicketListItemViewHolder { - return TicketListItemViewHolder.of(parent, vm) + return TicketListItemViewHolder.from(parent) } override fun onBindViewHolder(holder: TicketListItemViewHolder, position: Int) { @@ -18,15 +16,15 @@ class TicketListAdapter( } companion object { - val ticketDiffUtil = object : DiffUtil.ItemCallback() { + val ticketDiffUtil = object : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: MemberTicketUiModel, - newItem: MemberTicketUiModel, + oldItem: TicketListItemUiState, + newItem: TicketListItemUiState, ) = oldItem.id == newItem.id override fun areContentsTheSame( - oldItem: MemberTicketUiModel, - newItem: MemberTicketUiModel, + oldItem: TicketListItemUiState, + newItem: TicketListItemUiState, ) = oldItem == newItem } } 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 77be12894..76bf53220 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 @@ -10,10 +10,8 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.festago.festago.R -import com.festago.festago.analytics.FirebaseAnalyticsHelper -import com.festago.festago.data.repository.TicketDefaultRepository -import com.festago.festago.data.retrofit.AuthRetrofitClient import com.festago.festago.databinding.FragmentTicketListBinding +import com.festago.festago.presentation.ui.FestagoViewModelFactory import com.festago.festago.presentation.ui.ticketentry.TicketEntryActivity class TicketListFragment : Fragment(R.layout.fragment_ticket_list) { @@ -25,14 +23,7 @@ class TicketListFragment : Fragment(R.layout.fragment_ticket_list) { private lateinit var resultLauncher: ActivityResultLauncher - private val vm: TicketListViewModel by viewModels { - TicketListViewModel.TicketListViewModelFactory( - TicketDefaultRepository( - ticketRetrofitService = AuthRetrofitClient.ticketRetrofitService, - ), - analyticsHelper = FirebaseAnalyticsHelper, - ) - } + private val vm: TicketListViewModel by viewModels { FestagoViewModelFactory } override fun onCreateView( inflater: LayoutInflater, @@ -100,7 +91,7 @@ class TicketListFragment : Fragment(R.layout.fragment_ticket_list) { } private fun initView() { - adapter = TicketListAdapter(vm) + adapter = TicketListAdapter() binding.rvTicketList.adapter = adapter vm.loadCurrentTickets() initRefresh() diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/MemberTicketUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListItemUiState.kt similarity index 62% rename from android/festago/app/src/main/java/com/festago/festago/presentation/model/MemberTicketUiModel.kt rename to android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListItemUiState.kt index c7111ef92..d5c03ca6e 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/MemberTicketUiModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListItemUiState.kt @@ -1,8 +1,10 @@ -package com.festago.festago.presentation.model +package com.festago.festago.presentation.ui.home.ticketlist +import com.festago.festago.presentation.model.StageUiModel +import com.festago.festago.presentation.model.TicketConditionUiModel import java.time.LocalDateTime -data class MemberTicketUiModel( +data class TicketListItemUiState( val id: Long = -1, val number: Int = -1, val entryTime: LocalDateTime = LocalDateTime.MIN, @@ -13,4 +15,5 @@ data class MemberTicketUiModel( val festivalName: String = "", val festivalThumbnail: String = "", val canEntry: Boolean, + val onTicketEntry: (ticketId: Long) -> Unit, ) 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 587e8022a..26f34bd45 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,36 +6,28 @@ import android.widget.Button import androidx.recyclerview.widget.RecyclerView import com.festago.festago.R import com.festago.festago.databinding.ItemTicketListBinding -import com.festago.festago.presentation.model.MemberTicketUiModel -import java.time.LocalDateTime import java.time.format.DateTimeFormatter class TicketListItemViewHolder( val binding: ItemTicketListBinding, - vm: TicketListViewModel, ) : RecyclerView.ViewHolder(binding.root) { - init { - binding.vm = vm - } - - fun bind(item: MemberTicketUiModel) { + fun bind(item: TicketListItemUiState) { binding.ticket = item setTicketEntryBtn(item) } - private fun setTicketEntryBtn(item: MemberTicketUiModel) { + private fun setTicketEntryBtn(item: TicketListItemUiState) { val btn = binding.btnTicketEntry - val isAfterEntryTime = LocalDateTime.now().isAfter(item.entryTime) - btn.isEnabled = isAfterEntryTime + btn.isEnabled = item.canEntry - setTicketEntryBtnText(isAfterEntryTime = isAfterEntryTime, btn = btn, ticket = item) + setTicketEntryBtnText(isAfterEntryTime = item.canEntry, btn = btn, ticket = item) } private fun setTicketEntryBtnText( isAfterEntryTime: Boolean, btn: Button, - ticket: MemberTicketUiModel, + ticket: TicketListItemUiState, ) { if (isAfterEntryTime) { btn.text = btn.context.getString(R.string.ticket_list_btn_ticket_entry) @@ -49,13 +41,13 @@ class TicketListItemViewHolder( } companion object { - fun of(parent: ViewGroup, vm: TicketListViewModel): TicketListItemViewHolder { + fun from(parent: ViewGroup): TicketListItemViewHolder { val binding = ItemTicketListBinding.inflate( LayoutInflater.from(parent.context), parent, false, ) - return TicketListItemViewHolder(binding, vm) + return TicketListItemViewHolder(binding) } } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListUiState.kt index fc315dc71..211d7904c 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListUiState.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListUiState.kt @@ -1,11 +1,9 @@ package com.festago.festago.presentation.ui.home.ticketlist -import com.festago.festago.presentation.model.MemberTicketUiModel - sealed interface TicketListUiState { object Loading : TicketListUiState - data class Success(val tickets: List) : TicketListUiState { + data class Success(val tickets: List) : TicketListUiState { val hasTicket get() = tickets.isNotEmpty() val hasNotTicket get() = tickets.isEmpty() } 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 c63dbe589..8a7788194 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 @@ -3,16 +3,16 @@ package com.festago.festago.presentation.ui.home.ticketlist import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.domain.repository.TicketRepository -import com.festago.festago.presentation.mapper.toMemberTicketModel +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 kotlinx.coroutines.launch +import java.time.LocalDateTime class TicketListViewModel( private val ticketRepository: TicketRepository, @@ -29,8 +29,7 @@ class TicketListViewModel( viewModelScope.launch { ticketRepository.loadCurrentTickets() .onSuccess { tickets -> - _uiState.value = - TicketListUiState.Success(tickets.toPresentation().toMemberTicketModel()) + _uiState.value = TicketListUiState.Success(tickets.map { it.toUiState() }) }.onFailure { _uiState.value = TicketListUiState.Error analyticsHelper.logNetworkFailure(KEY_LOAD_TICKETS_LOG, it.message.toString()) @@ -42,19 +41,19 @@ class TicketListViewModel( _event.setValue(TicketListEvent.ShowTicketEntry(ticketId)) } - class TicketListViewModelFactory( - private val ticketRepository: TicketRepository, - private val analyticsHelper: AnalyticsHelper, - ) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(TicketListViewModel::class.java)) { - return TicketListViewModel(ticketRepository, analyticsHelper) as T - } - throw IllegalArgumentException("Unknown ViewModel Class") - } - } + 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/signin/SignInActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInActivity.kt index b5b619c5c..974494da1 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 @@ -10,15 +10,10 @@ import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.festago.festago.R -import com.festago.festago.analytics.FirebaseAnalyticsHelper -import com.festago.festago.data.datasource.AuthLocalDataSource -import com.festago.festago.data.repository.AuthDefaultRepository -import com.festago.festago.data.retrofit.AuthRetrofitClient -import com.festago.festago.data.retrofit.NormalRetrofitClient 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.ui.signin.SignInViewModel.SignInViewModelFactory import com.festago.festago.presentation.util.loginWithKakao import com.kakao.sdk.user.UserApiClient import kotlinx.coroutines.launch @@ -27,16 +22,7 @@ class SignInActivity : AppCompatActivity() { private lateinit var binding: ActivitySignInBinding - private val vm: SignInViewModel by viewModels { - SignInViewModelFactory( - AuthDefaultRepository( - authRetrofitService = NormalRetrofitClient.authRetrofitService, - authDataSource = AuthLocalDataSource.getInstance(this), - userRetrofitService = AuthRetrofitClient.userRetrofitService, - ), - FirebaseAnalyticsHelper, - ) - } + private val vm: SignInViewModel by viewModels { FestagoViewModelFactory } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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 352033ff7..ce770e3d4 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 @@ -1,13 +1,12 @@ package com.festago.festago.presentation.ui.signin import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.domain.repository.AuthRepository import com.festago.festago.presentation.util.MutableSingleLiveData import com.festago.festago.presentation.util.SingleLiveData +import com.festago.festago.repository.AuthRepository import kotlinx.coroutines.launch class SignInViewModel( @@ -34,20 +33,6 @@ class SignInViewModel( } } - class SignInViewModelFactory( - private val authRepository: AuthRepository, - private val analyticsHelper: AnalyticsHelper, - ) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(SignInViewModel::class.java)) { - return SignInViewModel(authRepository, analyticsHelper) as T - } - throw IllegalArgumentException("Unknown ViewModel Class") - } - } - 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/ticketentry/TicketEntryActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryActivity.kt index cbdfe79fa..6d7a3184f 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 @@ -7,12 +7,9 @@ import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.content.res.ResourcesCompat -import com.festago.festago.analytics.FirebaseAnalyticsHelper -import com.festago.festago.data.repository.TicketDefaultRepository -import com.festago.festago.data.retrofit.AuthRetrofitClient import com.festago.festago.databinding.ActivityTicketEntryBinding import com.festago.festago.presentation.mapper.toPresentation -import com.festago.festago.presentation.ui.ticketentry.TicketEntryViewModel.TicketEntryViewModelFactory +import com.festago.festago.presentation.ui.FestagoViewModelFactory import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.BarcodeEncoder @@ -20,14 +17,7 @@ class TicketEntryActivity : AppCompatActivity() { private lateinit var binding: ActivityTicketEntryBinding - private val vm: TicketEntryViewModel by viewModels { - TicketEntryViewModelFactory( - TicketDefaultRepository( - ticketRetrofitService = AuthRetrofitClient.ticketRetrofitService, - ), - FirebaseAnalyticsHelper, - ) - } + private val vm: TicketEntryViewModel by viewModels { FestagoViewModelFactory } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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 a092e481c..55762d2a2 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,7 +1,7 @@ package com.festago.festago.presentation.ui.ticketentry import com.festago.festago.R -import com.festago.festago.domain.model.TicketCode +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 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 c0971b138..043ce7b3a 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 @@ -3,15 +3,14 @@ package com.festago.festago.presentation.ui.ticketentry import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.domain.model.TicketCode -import com.festago.festago.domain.model.timer.Timer -import com.festago.festago.domain.model.timer.TimerListener -import com.festago.festago.domain.repository.TicketRepository +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 kotlinx.coroutines.launch class TicketEntryViewModel( @@ -98,20 +97,6 @@ class TicketEntryViewModel( } } - class TicketEntryViewModelFactory( - private val ticketRepository: TicketRepository, - private val analyticsHelper: AnalyticsHelper, - ) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(TicketEntryViewModel::class.java)) { - return TicketEntryViewModel(ticketRepository, analyticsHelper) as T - } - throw IllegalArgumentException("Unknown ViewModel Class") - } - } - companion object { private const val KEY_LOAD_Ticket_LOG = "load_ticket" private const val KEY_LOAD_CODE_LOG = "load_code" 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 0a6b9aee3..ed95ff13a 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 @@ -5,23 +5,13 @@ import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import com.festago.festago.analytics.FirebaseAnalyticsHelper -import com.festago.festago.data.repository.TicketDefaultRepository -import com.festago.festago.data.retrofit.AuthRetrofitClient import com.festago.festago.databinding.ActivityTicketHistoryBinding -import com.festago.festago.presentation.ui.tickethistory.TicketHistoryViewModel.TicketHistoryViewModelFactory +import com.festago.festago.presentation.ui.FestagoViewModelFactory class TicketHistoryActivity : AppCompatActivity() { private lateinit var binding: ActivityTicketHistoryBinding - private val vm: TicketHistoryViewModel by viewModels { - TicketHistoryViewModelFactory( - TicketDefaultRepository( - ticketRetrofitService = AuthRetrofitClient.ticketRetrofitService, - ), - FirebaseAnalyticsHelper, - ) - } + private val vm: TicketHistoryViewModel by viewModels { FestagoViewModelFactory } private var adapter: TicketHistoryAdapter = TicketHistoryAdapter() 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 3be54aa28..f30406ab0 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 @@ -3,12 +3,11 @@ package com.festago.festago.presentation.ui.tickethistory import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.domain.repository.TicketRepository import com.festago.festago.presentation.mapper.toPresentation +import com.festago.festago.repository.TicketRepository import kotlinx.coroutines.launch class TicketHistoryViewModel( @@ -37,20 +36,6 @@ class TicketHistoryViewModel( } } - class TicketHistoryViewModelFactory( - private val ticketRepository: TicketRepository, - private val analyticsHelper: AnalyticsHelper, - ) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(TicketHistoryViewModel::class.java)) { - return TicketHistoryViewModel(ticketRepository, analyticsHelper) as T - } - throw IllegalArgumentException("Unknown ViewModel Class") - } - } - companion object { private const val KEY_LOAD_TICKET_HISTORIES_LOG = "ticket_histories" } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservationUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/ReservationFestivalUiState.kt similarity index 53% rename from android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservationUiModel.kt rename to android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/ReservationFestivalUiState.kt index 5b0845452..696a7e010 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/ReservationUiModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/ReservationFestivalUiState.kt @@ -1,12 +1,11 @@ -package com.festago.festago.presentation.model +package com.festago.festago.presentation.ui.ticketreserve import java.time.LocalDate -data class ReservationUiModel( +data class ReservationFestivalUiState( val id: Int, val name: String, val thumbnail: String, val endDate: LocalDate, val startDate: LocalDate, - val reservationStages: List, ) 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 5809a9db3..dad5aade1 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 @@ -6,19 +6,12 @@ import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.ConcatAdapter -import com.festago.festago.analytics.FirebaseAnalyticsHelper -import com.festago.festago.data.datasource.AuthLocalDataSource -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.retrofit.AuthRetrofitClient -import com.festago.festago.data.retrofit.NormalRetrofitClient +import com.festago.festago.R import com.festago.festago.databinding.ActivityTicketReserveBinding -import com.festago.festago.domain.model.ReservedTicket +import com.festago.festago.model.ReservedTicket import com.festago.festago.presentation.mapper.toPresentation -import com.festago.festago.presentation.mapper.toTicketReserveItem 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.signin.SignInActivity @@ -26,35 +19,19 @@ import com.festago.festago.presentation.ui.ticketreserve.TicketReserveEvent.Rese import com.festago.festago.presentation.ui.ticketreserve.TicketReserveEvent.ReserveTicketSuccess import com.festago.festago.presentation.ui.ticketreserve.TicketReserveEvent.ShowSignIn import com.festago.festago.presentation.ui.ticketreserve.TicketReserveEvent.ShowTicketTypes -import com.festago.festago.presentation.ui.ticketreserve.TicketReserveViewModel.Companion.TicketReservationViewModelFactory 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.TicketReserveBottomSheetFragment +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale class TicketReserveActivity : AppCompatActivity() { private lateinit var binding: ActivityTicketReserveBinding - private val vm: TicketReserveViewModel by viewModels { - TicketReservationViewModelFactory( - ReservationTicketDefaultRepository( - reservationTicketRetrofitService = NormalRetrofitClient.reservationTicketRetrofitService, - ), - FestivalDefaultRepository( - festivalRetrofitService = NormalRetrofitClient.festivalRetrofitService, - ), - TicketDefaultRepository( - ticketRetrofitService = AuthRetrofitClient.ticketRetrofitService, - ), - AuthDefaultRepository( - authRetrofitService = NormalRetrofitClient.authRetrofitService, - authDataSource = AuthLocalDataSource.getInstance(this), - userRetrofitService = AuthRetrofitClient.userRetrofitService, - ), - FirebaseAnalyticsHelper, - ) - } + private val vm: TicketReserveViewModel by viewModels { FestagoViewModelFactory } - private val contentsAdapter by lazy { TicketReserveAdapter(vm) } + private val contentsAdapter by lazy { TicketReserveAdapter() } private val headerAdapter by lazy { TicketReserveHeaderAdapter() } private val concatAdapter by lazy { ConcatAdapter(headerAdapter, contentsAdapter) } @@ -84,17 +61,29 @@ class TicketReserveActivity : AppCompatActivity() { } private fun handleEvent(event: TicketReserveEvent) = when (event) { - is ShowTicketTypes -> handleShowTicketTypes(event.stageId, event.tickets) + is ShowTicketTypes -> handleShowTicketTypes( + stageStartTime = event.stageStartTime, + tickets = event.tickets, + ) + is ReserveTicketSuccess -> handleReserveTicketSuccess(event.reservedTicket) is ReserveTicketFailed -> handleReserveTicketFailed() is ShowSignIn -> handleShowSignIn() } - private fun handleShowTicketTypes(stageId: Int, tickets: List) { - contentsAdapter.currentList.find { it.id == stageId }?.let { stage -> - TicketReserveBottomSheetFragment.newInstance(stage.toPresentation(), tickets) - .show(supportFragmentManager, TicketReserveBottomSheetFragment::class.java.name) - } + private fun handleShowTicketTypes( + stageStartTime: LocalDateTime, + tickets: List, + ) { + TicketReserveBottomSheetFragment.newInstance( + stageStartTime.format( + DateTimeFormatter.ofPattern( + getString(R.string.ticket_reserve_tv_start_time), + Locale.KOREA, + ), + ), + tickets, + ).show(supportFragmentManager, TicketReserveBottomSheetFragment::class.java.name) } private fun handleReserveTicketSuccess(reservedTicket: ReservedTicket) { @@ -135,10 +124,8 @@ class TicketReserveActivity : AppCompatActivity() { } private fun updateSuccess(successState: TicketReserveUiState.Success) { - headerAdapter.submitList(listOf(successState.reservation)) - val contents = - successState.reservation.reservationStages.toTicketReserveItem(successState.isSigned) - contentsAdapter.submitList(contents) + headerAdapter.submitList(listOf(successState.festival)) + contentsAdapter.submitList(successState.stages) binding.srlTicketReserve.isRefreshing = false } 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 16f5742bc..04ef9faf5 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,12 +1,13 @@ package com.festago.festago.presentation.ui.ticketreserve -import com.festago.festago.domain.model.ReservedTicket +import com.festago.festago.model.ReservedTicket import com.festago.festago.presentation.model.ReservationTicketUiModel +import java.time.LocalDateTime sealed interface TicketReserveEvent { class ShowTicketTypes( - val stageId: Int, - val tickets: List + val stageStartTime: LocalDateTime, + val tickets: List, ) : TicketReserveEvent class ReserveTicketSuccess(val reservedTicket: ReservedTicket) : TicketReserveEvent diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketReserveItemUiModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveItemUiState.kt similarity index 58% rename from android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketReserveItemUiModel.kt rename to android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveItemUiState.kt index c3635b057..f3da26b8e 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/model/TicketReserveItemUiModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveItemUiState.kt @@ -1,11 +1,12 @@ -package com.festago.festago.presentation.model +package com.festago.festago.presentation.ui.ticketreserve import android.os.Parcelable +import com.festago.festago.presentation.model.ReservationTicketUiModel import kotlinx.parcelize.Parcelize import java.time.LocalDateTime @Parcelize -data class TicketReserveItemUiModel( +data class TicketReserveItemUiState( val id: Int, val lineUp: String, val startTime: LocalDateTime, @@ -13,4 +14,5 @@ data class TicketReserveItemUiModel( 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/TicketReserveUiState.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveUiState.kt index 9ec5e933c..52d0db9aa 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveUiState.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveUiState.kt @@ -1,12 +1,10 @@ package com.festago.festago.presentation.ui.ticketreserve -import com.festago.festago.presentation.model.ReservationUiModel - sealed interface TicketReserveUiState { object Loading : TicketReserveUiState class Success( - val reservation: ReservationUiModel, - val isSigned: Boolean, + val festival: ReservationFestivalUiState, + val stages: List, ) : TicketReserveUiState object Error : TicketReserveUiState 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 bfb043ffb..110270e00 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 @@ -3,18 +3,19 @@ package com.festago.festago.presentation.ui.ticketreserve import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.domain.repository.AuthRepository -import com.festago.festago.domain.repository.FestivalRepository -import com.festago.festago.domain.repository.ReservationTicketRepository -import com.festago.festago.domain.repository.TicketRepository +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 kotlinx.coroutines.launch +import java.time.LocalDateTime class TicketReserveViewModel( private val reservationTicketRepository: ReservationTicketRepository, @@ -35,7 +36,16 @@ class TicketReserveViewModel( festivalRepository.loadFestivalDetail(festivalId) .onSuccess { _uiState.setValue( - TicketReserveUiState.Success(it.toPresentation(), authRepository.isSigned), + 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 @@ -47,14 +57,14 @@ class TicketReserveViewModel( } } - fun showTicketTypes(stageId: Int) { + fun showTicketTypes(stageId: Int, stageStartTime: LocalDateTime) { viewModelScope.launch { if (authRepository.isSigned) { reservationTicketRepository.loadTicketTypes(stageId) .onSuccess { tickets -> _event.setValue( TicketReserveEvent.ShowTicketTypes( - stageId, + stageStartTime, tickets.map { it.toPresentation() }, ), ) @@ -78,31 +88,23 @@ class TicketReserveViewModel( } } - companion object { + private fun ReservationStage.toTicketReserveItem() = TicketReserveItemUiState( + id = id, + lineUp = lineUp, + startTime = startTime, + ticketOpenTime = ticketOpenTime, + reservationTickets = reservationTickets.map { it.toPresentation() }, + canReserve = LocalDateTime.now().isAfter(ticketOpenTime), + isSigned = authRepository.isSigned, + onShowStageTickets = ::showTicketTypes, + ) - private const val KEY_LOAD_RESERVATION_LOG = "load_reservation" + private fun List.toTicketReserveItems() = map { + it.toTicketReserveItem() + } - class TicketReservationViewModelFactory( - private val reservationTicketRepository: ReservationTicketRepository, - private val festivalRepository: FestivalRepository, - private val ticketRepository: TicketRepository, - private val authRepository: AuthRepository, - private val analyticsHelper: AnalyticsHelper, - ) : ViewModelProvider.Factory { + companion object { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(TicketReserveViewModel::class.java)) { - return TicketReserveViewModel( - reservationTicketRepository = reservationTicketRepository, - festivalRepository = festivalRepository, - ticketRepository = ticketRepository, - authRepository = authRepository, - analyticsHelper = analyticsHelper, - ) as T - } - throw IllegalArgumentException("Unknown ViewModel Class") - } - } + private const val KEY_LOAD_RESERVATION_LOG = "load_reservation" } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/adapter/TicketReserveAdapter.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/adapter/TicketReserveAdapter.kt index 07f094ddc..f646bb930 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/adapter/TicketReserveAdapter.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/adapter/TicketReserveAdapter.kt @@ -3,16 +3,14 @@ package com.festago.festago.presentation.ui.ticketreserve.adapter import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import com.festago.festago.presentation.model.TicketReserveItemUiModel -import com.festago.festago.presentation.ui.ticketreserve.TicketReserveViewModel +import com.festago.festago.presentation.ui.ticketreserve.TicketReserveItemUiState import com.festago.festago.presentation.ui.ticketreserve.viewHolder.TicketReserveViewHolder -class TicketReserveAdapter( - private val vm: TicketReserveViewModel, -) : ListAdapter(diffUtil) { +class TicketReserveAdapter : + ListAdapter(diffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TicketReserveViewHolder { - return TicketReserveViewHolder.of(parent, vm) + return TicketReserveViewHolder.from(parent) } override fun onBindViewHolder(holder: TicketReserveViewHolder, position: Int) { @@ -20,15 +18,15 @@ class TicketReserveAdapter( } companion object { - val diffUtil = object : DiffUtil.ItemCallback() { + private val diffUtil = object : DiffUtil.ItemCallback() { override fun areContentsTheSame( - oldItem: TicketReserveItemUiModel, - newItem: TicketReserveItemUiModel, + oldItem: TicketReserveItemUiState, + newItem: TicketReserveItemUiState, ) = oldItem == newItem override fun areItemsTheSame( - oldItem: TicketReserveItemUiModel, - newItem: TicketReserveItemUiModel, + oldItem: TicketReserveItemUiState, + newItem: TicketReserveItemUiState, ) = oldItem.id == newItem.id } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/adapter/TicketReserveHeaderAdapter.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/adapter/TicketReserveHeaderAdapter.kt index 9055ef174..9e6433aec 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/adapter/TicketReserveHeaderAdapter.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/adapter/TicketReserveHeaderAdapter.kt @@ -3,11 +3,11 @@ package com.festago.festago.presentation.ui.ticketreserve.adapter import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import com.festago.festago.presentation.model.ReservationUiModel +import com.festago.festago.presentation.ui.ticketreserve.ReservationFestivalUiState import com.festago.festago.presentation.ui.ticketreserve.viewHolder.TicketReserveHeaderViewHolder class TicketReserveHeaderAdapter : - ListAdapter(diffUtil) { + ListAdapter(diffUtil) { override fun onCreateViewHolder( parent: ViewGroup, @@ -21,13 +21,16 @@ class TicketReserveHeaderAdapter : } companion object { - val diffUtil = object : DiffUtil.ItemCallback() { + val diffUtil = object : DiffUtil.ItemCallback() { override fun areContentsTheSame( - oldItem: ReservationUiModel, - newItem: ReservationUiModel, + oldItem: ReservationFestivalUiState, + newItem: ReservationFestivalUiState, ) = oldItem == newItem - override fun areItemsTheSame(oldItem: ReservationUiModel, newItem: ReservationUiModel) = + override fun areItemsTheSame( + oldItem: ReservationFestivalUiState, + newItem: ReservationFestivalUiState + ) = oldItem.id == newItem.id } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomSheetAdapter.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomSheetAdapter.kt index d5becf96f..bc19083d5 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomSheetAdapter.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/bottomsheet/TicketReserveBottomSheetAdapter.kt @@ -6,8 +6,7 @@ import androidx.recyclerview.widget.ListAdapter class TicketReserveBottomSheetAdapter( private val callback: TicketReserveBottomSheetCallback, -) : - ListAdapter(diffUtil) { +) : ListAdapter(diffUtil) { override fun onCreateViewHolder( parent: ViewGroup, 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 9c8e11566..810924c90 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,9 @@ import android.view.View import android.view.ViewGroup import androidx.lifecycle.ViewModelProvider import com.festago.festago.databinding.FragmentTicketReserveBottomSheetBinding -import com.festago.festago.presentation.model.ReservationStageUiModel import com.festago.festago.presentation.model.ReservationTicketUiModel import com.festago.festago.presentation.ui.ticketreserve.TicketReserveViewModel import com.festago.festago.presentation.util.getParcelableArrayListCompat -import com.festago.festago.presentation.util.getParcelableCompat import com.google.android.material.bottomsheet.BottomSheetDialogFragment class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { @@ -38,16 +36,15 @@ class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { ): View { _binding = FragmentTicketReserveBottomSheetBinding.inflate(inflater) binding.lifecycleOwner = viewLifecycleOwner - binding.vm = vm return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { arguments?.apply { - getParcelableCompat(KEY_STAGE)?.let { stage -> - binding.stage = stage + getString(KEY_STAGE_START_TIME)?.let { startTime -> + binding.stageStartTime = startTime } - getParcelableArrayListCompat(KEY_ITEM)?.let { + getParcelableArrayListCompat(KEY_ITEMS)?.let { ticketTypeAdapter.submitList(it.map(::TicketReserveBottomItem)) } } @@ -57,6 +54,8 @@ class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { private fun initView() { binding.rvTicketTypes.adapter = ticketTypeAdapter + val onReserve: (Int) -> Unit = { id -> vm.reserveTicket(id) } + binding.onReserve = onReserve } override fun onDestroyView() { @@ -65,16 +64,16 @@ class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { } companion object { - private const val KEY_STAGE = "KEY_STAGE" - private const val KEY_ITEM = "KEY_ITEM" + private const val KEY_STAGE_START_TIME = "KEY_STAGE_START_TIME" + private const val KEY_ITEMS = "KEY_ITEMS" fun newInstance( - stage: ReservationStageUiModel, + stageStartTime: String, items: List, ) = TicketReserveBottomSheetFragment().apply { arguments = Bundle().apply { - putParcelable(KEY_STAGE, stage) - putParcelableArrayList(KEY_ITEM, items as ArrayList) + putString(KEY_STAGE_START_TIME, stageStartTime) + putParcelableArrayList(KEY_ITEMS, items as ArrayList) } } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/viewHolder/TicketReserveHeaderViewHolder.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/viewHolder/TicketReserveHeaderViewHolder.kt index 4c2bf6cd3..6548ed102 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/viewHolder/TicketReserveHeaderViewHolder.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/viewHolder/TicketReserveHeaderViewHolder.kt @@ -7,15 +7,16 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.festago.festago.R import com.festago.festago.databinding.ItemTicketReserveHeaderBinding -import com.festago.festago.presentation.model.ReservationUiModel +import com.festago.festago.presentation.ui.ticketreserve.ReservationFestivalUiState import java.time.format.DateTimeFormatter class TicketReserveHeaderViewHolder( private val binding: ItemTicketReserveHeaderBinding, ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: ReservationUiModel) { - val formatter = - DateTimeFormatter.ofPattern(binding.root.context.getString(R.string.ticket_reserve_tv_date_range_format)) + fun bind(item: ReservationFestivalUiState) { + val formatter = DateTimeFormatter.ofPattern( + binding.root.context.getString(R.string.ticket_reserve_tv_date_range_format) + ) binding.tvDateRange.text = binding.root.context.getString(R.string.ticket_reserve_tv_date_range) 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 3f96b161b..10f252fc5 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,36 +5,29 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.festago.festago.R import com.festago.festago.databinding.ItemTicketReserveBinding -import com.festago.festago.presentation.model.TicketReserveItemUiModel -import com.festago.festago.presentation.ui.ticketreserve.TicketReserveViewModel +import com.festago.festago.presentation.ui.ticketreserve.TicketReserveItemUiState import java.time.format.DateTimeFormatter class TicketReserveViewHolder( private val binding: ItemTicketReserveBinding, - vm: TicketReserveViewModel, ) : RecyclerView.ViewHolder(binding.root) { - init { - binding.vm = vm - } - fun bind(item: TicketReserveItemUiModel) { + fun bind(item: TicketReserveItemUiState) { binding.stage = item + binding.btnReserveTicket.isEnabled = !item.isSigned || item.canReserve when { !item.isSigned -> { - binding.btnReserveTicket.isEnabled = true binding.btnReserveTicket.text = binding.root.context.getString(R.string.ticket_reserve_tv_signin) } item.canReserve -> { - binding.btnReserveTicket.isEnabled = true binding.btnReserveTicket.text = binding.root.context.getString(R.string.ticket_reserve_tv_btn_reserve_ticket) } else -> { - binding.btnReserveTicket.isEnabled = false val pattern = DateTimeFormatter.ofPattern( binding.root.context.getString(R.string.ticket_reserve_tv_btn_reserve_ticket_not_open), ) @@ -54,16 +47,13 @@ class TicketReserveViewHolder( } companion object { - fun of( - parent: ViewGroup, - vm: TicketReserveViewModel, - ): TicketReserveViewHolder { + fun from(parent: ViewGroup): TicketReserveViewHolder { val binding = ItemTicketReserveBinding.inflate( LayoutInflater.from(parent.context), parent, false, ) - return TicketReserveViewHolder(binding, vm) + return TicketReserveViewHolder(binding) } } } diff --git a/android/festago/app/src/main/res/drawable/ic_festago_logo_background.xml b/android/festago/app/src/main/res/drawable/ic_festago_logo_background.xml index ca3826a46..3fbd73a7f 100644 --- a/android/festago/app/src/main/res/drawable/ic_festago_logo_background.xml +++ b/android/festago/app/src/main/res/drawable/ic_festago_logo_background.xml @@ -1,74 +1,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:height="108dp" + android:viewportWidth="1024" + android:viewportHeight="1024"> + + + + + + + + + + diff --git a/android/festago/app/src/main/res/drawable/ic_festago_logo_foreground.xml b/android/festago/app/src/main/res/drawable/ic_festago_logo_foreground.xml index b970636c0..e78ecffc2 100644 --- a/android/festago/app/src/main/res/drawable/ic_festago_logo_foreground.xml +++ b/android/festago/app/src/main/res/drawable/ic_festago_logo_foreground.xml @@ -1,29 +1,14 @@ - + android:viewportWidth="389" + android:viewportHeight="236"> + - - - - - - - - diff --git a/android/festago/app/src/main/res/layout/fragment_ticket_reserve_bottom_sheet.xml b/android/festago/app/src/main/res/layout/fragment_ticket_reserve_bottom_sheet.xml index 445841156..f34d68337 100644 --- a/android/festago/app/src/main/res/layout/fragment_ticket_reserve_bottom_sheet.xml +++ b/android/festago/app/src/main/res/layout/fragment_ticket_reserve_bottom_sheet.xml @@ -10,16 +10,16 @@ - - + name="stageStartTime" + type="String" /> + + - - + type="com.festago.festago.presentation.ui.home.festivallist.FestivalItemUiState" /> @@ -20,7 +16,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:onClick="@{() -> vm.showTicketReserve(festival.id)}" + android:onClick="@{() -> festival.onFestivalDetail.invoke(festival.id)}" android:padding="8dp"> - - + type="com.festago.festago.presentation.ui.home.ticketlist.TicketListItemUiState" /> diff --git a/android/festago/app/src/main/res/layout/item_ticket_reserve.xml b/android/festago/app/src/main/res/layout/item_ticket_reserve.xml index e55f63b57..c1df86cff 100644 --- a/android/festago/app/src/main/res/layout/item_ticket_reserve.xml +++ b/android/festago/app/src/main/res/layout/item_ticket_reserve.xml @@ -9,13 +9,9 @@ - - + type="com.festago.festago.presentation.ui.ticketreserve.TicketReserveItemUiState" /> - festago + 페스타고 yyyy.MM.dd diff --git a/android/festago/app/src/main/res/xml/backup_rules.xml b/android/festago/app/src/main/res/xml/backup_rules.xml index 148c18b65..c3bb41886 100644 --- a/android/festago/app/src/main/res/xml/backup_rules.xml +++ b/android/festago/app/src/main/res/xml/backup_rules.xml @@ -1,13 +1,4 @@ - + - + diff --git a/android/festago/app/src/main/res/xml/data_extraction_rules.xml b/android/festago/app/src/main/res/xml/data_extraction_rules.xml index 0c4f95cab..ddd193e33 100644 --- a/android/festago/app/src/main/res/xml/data_extraction_rules.xml +++ b/android/festago/app/src/main/res/xml/data_extraction_rules.xml @@ -1,19 +1,6 @@ - + - - + + - diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/fixture/TicketFixture.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/fixture/TicketFixture.kt index 2a91abca5..63f3292a2 100644 --- a/android/festago/app/src/test/java/com/festago/festago/presentation/fixture/TicketFixture.kt +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/fixture/TicketFixture.kt @@ -1,9 +1,9 @@ package com.festago.festago.presentation.fixture -import com.festago.festago.domain.model.MemberTicketFestival -import com.festago.festago.domain.model.Stage -import com.festago.festago.domain.model.Ticket -import com.festago.festago.domain.model.TicketCondition +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 java.time.LocalDateTime object TicketFixture { 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 28aac82ff..5e7b174f7 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,7 +1,7 @@ package com.festago.festago.presentation.ui.home import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.festago.festago.domain.repository.AuthRepository +import com.festago.festago.repository.AuthRepository import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.Dispatchers 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 d4eb1dbd6..95d5e5344 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 @@ -2,9 +2,8 @@ package com.festago.festago.presentation.ui.home.festivallist import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.festago.festago.analytics.AnalyticsHelper -import com.festago.festago.domain.model.Festival -import com.festago.festago.domain.repository.FestivalRepository -import com.festago.festago.presentation.mapper.toPresentation +import com.festago.festago.model.Festival +import com.festago.festago.repository.FestivalRepository import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.Dispatchers @@ -19,7 +18,6 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import java.lang.Exception import java.time.LocalDate class FestivalListViewModelTest { @@ -78,7 +76,7 @@ class FestivalListViewModelTest { // and val actual = (vm.uiState.value as FestivalListUiState.Success).festivals - val expected = fakeFestivals.map { it.toPresentation() } + val expected = fakeFestivals.map { it.toUiState() } assertThat(actual).isEqualTo(expected) } softly.assertAll() @@ -142,4 +140,13 @@ class FestivalListViewModelTest { // then assertThat(vm.event.getValue()).isInstanceOf(FestivalListEvent.ShowTicketReserve::class.java) } + + private fun Festival.toUiState() = FestivalItemUiState( + id = id, + name = name, + startDate = startDate, + endDate = endDate, + thumbnail = thumbnail, + onFestivalDetail = vm::showTicketReserve, + ) } 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 adab8da5e..305a1e883 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 @@ -2,15 +2,15 @@ package com.festago.festago.presentation.ui.home.mypage import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.festago.festago.analytics.AnalyticsHelper -import com.festago.festago.domain.model.MemberTicketFestival -import com.festago.festago.domain.model.Stage -import com.festago.festago.domain.model.Ticket -import com.festago.festago.domain.model.TicketCondition -import com.festago.festago.domain.model.UserProfile -import com.festago.festago.domain.repository.AuthRepository -import com.festago.festago.domain.repository.TicketRepository -import com.festago.festago.domain.repository.UserRepository +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 import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.Dispatchers 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 8ce4757a6..e4698197d 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 @@ -2,11 +2,10 @@ package com.festago.festago.presentation.ui.home.ticketlist import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.festago.festago.analytics.AnalyticsHelper -import com.festago.festago.domain.model.Ticket -import com.festago.festago.domain.repository.TicketRepository +import com.festago.festago.model.Ticket import com.festago.festago.presentation.fixture.TicketFixture -import com.festago.festago.presentation.mapper.toMemberTicketModel 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 @@ -21,6 +20,7 @@ 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 @@ -67,7 +67,7 @@ class TicketListViewModelTest { // and val actual = (vm.uiState.value as TicketListUiState.Success).tickets - val expected = tickets.toPresentation().toMemberTicketModel() + val expected = tickets.map { it.toUiState() } assertThat(actual).isEqualTo(expected) } softly.assertAll() @@ -94,7 +94,7 @@ class TicketListViewModelTest { // and val actual = (vm.uiState.value as TicketListUiState.Success).tickets - val expected = fakeEmptyTickets.toPresentation().toMemberTicketModel() + val expected = fakeEmptyTickets.map { it.toUiState() } assertThat(actual).isEqualTo(expected) } softly.assertAll() @@ -164,4 +164,18 @@ class TicketListViewModelTest { val expected = 1L assertThat(actual).isEqualTo(expected) } + + 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/signin/SignInViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/signin/SignInViewModelTest.kt index 025932522..fe3e8a73b 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 @@ -2,7 +2,7 @@ package com.festago.festago.presentation.ui.signin import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.festago.festago.analytics.AnalyticsHelper -import com.festago.festago.domain.repository.AuthRepository +import com.festago.festago.repository.AuthRepository import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.Dispatchers 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 cd31a1ed4..a2e01d3d5 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 @@ -2,10 +2,10 @@ package com.festago.festago.presentation.ui.ticketentry import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.festago.festago.analytics.AnalyticsHelper -import com.festago.festago.domain.model.TicketCode -import com.festago.festago.domain.repository.TicketRepository +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 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 f4dd71e78..4b0885db7 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 @@ -2,9 +2,9 @@ package com.festago.festago.presentation.ui.tickethistory import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.festago.festago.analytics.AnalyticsHelper -import com.festago.festago.domain.repository.TicketRepository 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 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 c7eb46928..d1ac9905f 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 @@ -2,15 +2,15 @@ package com.festago.festago.presentation.ui.ticketreserve import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.festago.festago.analytics.AnalyticsHelper -import com.festago.festago.domain.model.Reservation -import com.festago.festago.domain.model.ReservationStage -import com.festago.festago.domain.model.ReservationTicket -import com.festago.festago.domain.model.ReservedTicket -import com.festago.festago.domain.repository.AuthRepository -import com.festago.festago.domain.repository.FestivalRepository -import com.festago.festago.domain.repository.ReservationTicketRepository -import com.festago.festago.domain.repository.TicketRepository +import com.festago.festago.model.Reservation +import com.festago.festago.model.ReservationStage +import com.festago.festago.model.ReservationTicket +import com.festago.festago.model.ReservedTicket import com.festago.festago.presentation.mapper.toPresentation +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 io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.Dispatchers @@ -103,8 +103,15 @@ class TicketReserveViewModelTest { assertThat(vm.uiState.value).isInstanceOf(TicketReserveUiState.Success::class.java) // and - val uiModel = vm.uiState.value as TicketReserveUiState.Success - assertThat(uiModel.reservation).isEqualTo(fakeReservation.toPresentation()) + val festival = (vm.uiState.value as TicketReserveUiState.Success).festival + val expected = ReservationFestivalUiState( + id = festival.id, + name = festival.name, + thumbnail = festival.thumbnail, + endDate = festival.endDate, + startDate = festival.startDate, + ) + assertThat(festival).isEqualTo(expected) } @Test @@ -152,7 +159,7 @@ class TicketReserveViewModelTest { } // when - vm.showTicketTypes(1) + vm.showTicketTypes(1, LocalDateTime.MIN) // then assertThat(vm.event.getValue()).isInstanceOf(TicketReserveEvent.ShowTicketTypes::class.java) @@ -174,7 +181,7 @@ class TicketReserveViewModelTest { } // when - vm.showTicketTypes(1) + vm.showTicketTypes(1, LocalDateTime.MIN) // then assertThat(vm.uiState.value).isEqualTo(TicketReserveUiState.Error) diff --git a/android/festago/domain/.gitignore b/android/festago/domain/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/android/festago/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/festago/domain/build.gradle.kts b/android/festago/domain/build.gradle.kts new file mode 100644 index 000000000..e3645b404 --- /dev/null +++ b/android/festago/domain/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + kotlin("jvm") +} + +dependencies { + testImplementation("org.junit.jupiter", "junit-jupiter", "5.8.2") + testImplementation("org.assertj", "assertj-core", "3.22.0") + testImplementation("io.kotest", "kotest-runner-junit5", "5.2.3") + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.6.4") +} + +tasks { + compileKotlin { + kotlinOptions.jvmTarget = "17" + } + compileTestKotlin { + kotlinOptions.jvmTarget = "17" + } + test { + useJUnitPlatform() + } +} diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/Festival.kt b/android/festago/domain/src/main/java/com/festago/festago/model/Festival.kt similarity index 81% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/Festival.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/Festival.kt index a493a653d..36a6d80d9 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/Festival.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/Festival.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model +package com.festago.festago.model import java.time.LocalDate diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/MemberTicketFestival.kt b/android/festago/domain/src/main/java/com/festago/festago/model/MemberTicketFestival.kt similarity index 71% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/MemberTicketFestival.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/MemberTicketFestival.kt index a9eb3786f..aaf8ce99e 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/MemberTicketFestival.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/MemberTicketFestival.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model +package com.festago.festago.model data class MemberTicketFestival( val id: Int, diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/Reservation.kt b/android/festago/domain/src/main/java/com/festago/festago/model/Reservation.kt similarity index 84% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/Reservation.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/Reservation.kt index 49f1d34a2..d2510fa5a 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/Reservation.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/Reservation.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model +package com.festago.festago.model import java.time.LocalDate diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/ReservationStage.kt b/android/festago/domain/src/main/java/com/festago/festago/model/ReservationStage.kt similarity index 84% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/ReservationStage.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/ReservationStage.kt index 454abd6f6..4c5bce35f 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/ReservationStage.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/ReservationStage.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model +package com.festago.festago.model import java.time.LocalDateTime diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/ReservationTicket.kt b/android/festago/domain/src/main/java/com/festago/festago/model/ReservationTicket.kt similarity index 76% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/ReservationTicket.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/ReservationTicket.kt index dbc49fc37..694522079 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/ReservationTicket.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/ReservationTicket.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model +package com.festago.festago.model data class ReservationTicket( val id: Int, diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/ReservedTicket.kt b/android/festago/domain/src/main/java/com/festago/festago/model/ReservedTicket.kt similarity index 76% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/ReservedTicket.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/ReservedTicket.kt index be0e403ef..fec8f801b 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/ReservedTicket.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/ReservedTicket.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model +package com.festago.festago.model import java.time.LocalDateTime diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/Stage.kt b/android/festago/domain/src/main/java/com/festago/festago/model/Stage.kt similarity index 71% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/Stage.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/Stage.kt index fa854f639..8a80c1b63 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/Stage.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/Stage.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model +package com.festago.festago.model import java.time.LocalDateTime diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/Ticket.kt b/android/festago/domain/src/main/java/com/festago/festago/model/Ticket.kt similarity index 86% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/Ticket.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/Ticket.kt index cb383a2d7..bd873fe16 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/Ticket.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/Ticket.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model +package com.festago.festago.model import java.time.LocalDateTime diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/TicketCode.kt b/android/festago/domain/src/main/java/com/festago/festago/model/TicketCode.kt similarity index 62% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/TicketCode.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/TicketCode.kt index 8fef0a8b2..d768a5534 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/TicketCode.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/TicketCode.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model +package com.festago.festago.model data class TicketCode( val code: String, diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/TicketCondition.kt b/android/festago/domain/src/main/java/com/festago/festago/model/TicketCondition.kt similarity index 65% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/TicketCondition.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/TicketCondition.kt index edb8a9a9e..efadf3e61 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/TicketCondition.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/TicketCondition.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model +package com.festago.festago.model enum class TicketCondition { BEFORE_ENTRY, diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/UserProfile.kt b/android/festago/domain/src/main/java/com/festago/festago/model/UserProfile.kt similarity index 72% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/UserProfile.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/UserProfile.kt index b48789588..eed278084 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/UserProfile.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/UserProfile.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model +package com.festago.festago.model data class UserProfile( val memberId: Long, diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/timer/Timer.kt b/android/festago/domain/src/main/java/com/festago/festago/model/timer/Timer.kt similarity index 95% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/timer/Timer.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/timer/Timer.kt index ff5ecd67d..83e31b88b 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/timer/Timer.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/timer/Timer.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model.timer +package com.festago.festago.model.timer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/model/timer/TimerListener.kt b/android/festago/domain/src/main/java/com/festago/festago/model/timer/TimerListener.kt similarity index 62% rename from android/festago/app/src/main/java/com/festago/festago/domain/model/timer/TimerListener.kt rename to android/festago/domain/src/main/java/com/festago/festago/model/timer/TimerListener.kt index 8593296b6..1287fb95d 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/model/timer/TimerListener.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/model/timer/TimerListener.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.model.timer +package com.festago.festago.model.timer interface TimerListener { fun onTick(current: Int) diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/repository/AuthRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/AuthRepository.kt similarity index 83% rename from android/festago/app/src/main/java/com/festago/festago/domain/repository/AuthRepository.kt rename to android/festago/domain/src/main/java/com/festago/festago/repository/AuthRepository.kt index 2f8c0fb01..1ce24a7df 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/repository/AuthRepository.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/repository/AuthRepository.kt @@ -1,4 +1,4 @@ -package com.festago.festago.domain.repository +package com.festago.festago.repository interface AuthRepository { val isSigned: Boolean diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/repository/FestivalRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/FestivalRepository.kt similarity index 52% rename from android/festago/app/src/main/java/com/festago/festago/domain/repository/FestivalRepository.kt rename to android/festago/domain/src/main/java/com/festago/festago/repository/FestivalRepository.kt index fe4bd1f08..db08d52dd 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/repository/FestivalRepository.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/repository/FestivalRepository.kt @@ -1,7 +1,7 @@ -package com.festago.festago.domain.repository +package com.festago.festago.repository -import com.festago.festago.domain.model.Festival -import com.festago.festago.domain.model.Reservation +import com.festago.festago.model.Festival +import com.festago.festago.model.Reservation interface FestivalRepository { suspend fun loadFestivals(): Result> diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/repository/ReservationTicketRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/ReservationTicketRepository.kt similarity index 54% rename from android/festago/app/src/main/java/com/festago/festago/domain/repository/ReservationTicketRepository.kt rename to android/festago/domain/src/main/java/com/festago/festago/repository/ReservationTicketRepository.kt index 27f08f9d4..bef4f3654 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/repository/ReservationTicketRepository.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/repository/ReservationTicketRepository.kt @@ -1,6 +1,6 @@ -package com.festago.festago.domain.repository +package com.festago.festago.repository -import com.festago.festago.domain.model.ReservationTicket +import com.festago.festago.model.ReservationTicket interface ReservationTicketRepository { suspend fun loadTicketTypes(stageId: Int): Result> diff --git a/android/festago/app/src/main/java/com/festago/festago/domain/repository/TicketRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/TicketRepository.kt similarity index 64% rename from android/festago/app/src/main/java/com/festago/festago/domain/repository/TicketRepository.kt rename to android/festago/domain/src/main/java/com/festago/festago/repository/TicketRepository.kt index b615c4fd3..3ccf4cf11 100644 --- a/android/festago/app/src/main/java/com/festago/festago/domain/repository/TicketRepository.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/repository/TicketRepository.kt @@ -1,8 +1,8 @@ -package com.festago.festago.domain.repository +package com.festago.festago.repository -import com.festago.festago.domain.model.ReservedTicket -import com.festago.festago.domain.model.Ticket -import com.festago.festago.domain.model.TicketCode +import com.festago.festago.model.ReservedTicket +import com.festago.festago.model.Ticket +import com.festago.festago.model.TicketCode interface TicketRepository { suspend fun loadTicket(ticketId: Long): 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 new file mode 100644 index 000000000..bf915f5be --- /dev/null +++ b/android/festago/domain/src/main/java/com/festago/festago/repository/TokenRepository.kt @@ -0,0 +1,7 @@ +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/main/java/com/festago/festago/repository/UserRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/UserRepository.kt new file mode 100644 index 000000000..e6d3116bb --- /dev/null +++ b/android/festago/domain/src/main/java/com/festago/festago/repository/UserRepository.kt @@ -0,0 +1,7 @@ +package com.festago.festago.repository + +import com.festago.festago.model.UserProfile + +interface UserRepository { + suspend fun loadUserProfile(): Result +} diff --git a/android/festago/settings.gradle.kts b/android/festago/settings.gradle.kts index 889c93cb3..e90fbfa66 100644 --- a/android/festago/settings.gradle.kts +++ b/android/festago/settings.gradle.kts @@ -15,3 +15,4 @@ dependencyResolutionManagement { } rootProject.name = "festago" include(":app") +include(":domain") diff --git a/backend/build.gradle b/backend/build.gradle index e5f823c75..fc90dff7a 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -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.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' @@ -38,6 +39,9 @@ dependencies { // Logback Slack Alarm implementation "com.github.maricn:logback-slack-appender:1.4.0" + + // Mockito + testImplementation 'org.mockito:mockito-inline' } tasks.named('test') { diff --git a/backend/src/main/java/com/festago/aop/LogRequestBody.java b/backend/src/main/java/com/festago/aop/LogRequestBody.java new file mode 100644 index 000000000..005c4d2ae --- /dev/null +++ b/backend/src/main/java/com/festago/aop/LogRequestBody.java @@ -0,0 +1,16 @@ +package com.festago.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.slf4j.event.Level; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface LogRequestBody { + + boolean exceptionOnly() default false; + + Level level() default Level.INFO; +} diff --git a/backend/src/main/java/com/festago/aop/LogRequestBodyAspect.java b/backend/src/main/java/com/festago/aop/LogRequestBodyAspect.java new file mode 100644 index 000000000..c596d21cd --- /dev/null +++ b/backend/src/main/java/com/festago/aop/LogRequestBodyAspect.java @@ -0,0 +1,84 @@ +package com.festago.aop; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.festago.exception.ErrorCode; +import com.festago.exception.InternalServerException; +import com.festago.presentation.ErrorLogger; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Objects; +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.event.Level; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.util.ContentCachingRequestWrapper; + +@Component +@Aspect +public class LogRequestBodyAspect { + + private static final long MAX_CONTENT_LENGTH = 1024; + private static final String LOG_FORMAT = "[REQUEST BODY]\n{}"; + + private final ErrorLogger errorLogger; + private final ObjectMapper objectMapper; + + public LogRequestBodyAspect(ErrorLogger errorLogger, ObjectMapper objectMapper) { + this.errorLogger = errorLogger; + this.objectMapper = objectMapper; + } + + @Around("@annotation(LogRequestBody)") + public Object handleAll(ProceedingJoinPoint pjp) throws Throwable { + MethodSignature signature = (MethodSignature) pjp.getSignature(); + Method method = signature.getMethod(); + LogRequestBody annotation = method.getAnnotation(LogRequestBody.class); + Level level = annotation.level(); + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + + if (attributes == null || !errorLogger.isEnabledForLevel(level)) { + return pjp.proceed(); + } + HttpServletRequest request = attributes.getRequest(); + if (validateRequest(request)) { + return pjp.proceed(); + } + + if (annotation.exceptionOnly()) { + try { + return pjp.proceed(); + } catch (Throwable e) { + log(level, request); + throw e; + } + } + + log(level, request); + return pjp.proceed(); + } + + private boolean validateRequest(HttpServletRequest request) { + return !Objects.equals(request.getContentType(), MediaType.APPLICATION_JSON_VALUE) + || request.getContentLengthLong() > MAX_CONTENT_LENGTH; + } + + private void log(Level level, HttpServletRequest request) { + errorLogger.get(level) + .log(LOG_FORMAT, getRequestPayload(request)); + } + + private String getRequestPayload(HttpServletRequest request) { + try { + ContentCachingRequestWrapper cachedRequest = (ContentCachingRequestWrapper) request; + return objectMapper.readTree(cachedRequest.getContentAsByteArray()).toPrettyString(); + } catch (IOException | ClassCastException e) { + throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/backend/src/main/java/com/festago/application/TicketService.java b/backend/src/main/java/com/festago/application/TicketService.java index 4cab2e8b5..6bdc6ddf4 100644 --- a/backend/src/main/java/com/festago/application/TicketService.java +++ b/backend/src/main/java/com/festago/application/TicketService.java @@ -1,24 +1,16 @@ package com.festago.application; -import com.festago.domain.Member; -import com.festago.domain.MemberRepository; -import com.festago.domain.MemberTicket; -import com.festago.domain.MemberTicketRepository; import com.festago.domain.Stage; import com.festago.domain.StageRepository; import com.festago.domain.Ticket; -import com.festago.domain.TicketAmount; -import com.festago.domain.TicketAmountRepository; import com.festago.domain.TicketRepository; import com.festago.domain.TicketType; import com.festago.dto.StageTicketsResponse; import com.festago.dto.TicketCreateRequest; import com.festago.dto.TicketCreateResponse; -import com.festago.dto.TicketingRequest; -import com.festago.dto.TicketingResponse; -import com.festago.exception.BadRequestException; import com.festago.exception.ErrorCode; import com.festago.exception.NotFoundException; +import java.time.Clock; import java.time.LocalDateTime; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,19 +21,12 @@ public class TicketService { private final TicketRepository ticketRepository; private final StageRepository stageRepository; - private final MemberTicketRepository memberTicketRepository; - private final TicketAmountRepository ticketAmountRepository; - private final MemberRepository memberRepository; + private final Clock clock; - public TicketService(TicketRepository ticketRepository, StageRepository stageRepository, - MemberTicketRepository memberTicketRepository, - TicketAmountRepository ticketAmountRepository, - MemberRepository memberRepository) { + public TicketService(TicketRepository ticketRepository, StageRepository stageRepository, Clock clock) { this.ticketRepository = ticketRepository; this.stageRepository = stageRepository; - this.memberTicketRepository = memberTicketRepository; - this.ticketAmountRepository = ticketAmountRepository; - this.memberRepository = memberRepository; + this.clock = clock; } public TicketCreateResponse create(TicketCreateRequest request) { @@ -51,7 +36,7 @@ public TicketCreateResponse create(TicketCreateRequest request) { Ticket ticket = ticketRepository.findByTicketTypeAndStage(ticketType, stage) .orElseGet(() -> ticketRepository.save(new Ticket(stage, ticketType))); - ticket.addTicketEntryTime(LocalDateTime.now(), request.entryTime(), request.amount()); + ticket.addTicketEntryTime(LocalDateTime.now(clock), request.entryTime(), request.amount()); return TicketCreateResponse.from(ticket); } @@ -61,42 +46,6 @@ private Stage findStageById(Long stageId) { .orElseThrow(() -> new NotFoundException(ErrorCode.STAGE_NOT_FOUND)); } - public TicketingResponse ticketing(Long memberId, TicketingRequest request) { - Ticket ticket = findTicketById(request.ticketId()); - Member member = findMemberById(memberId); - validateAlreadyReserved(member, ticket); - int reservedAmount = reserveTicket(request.ticketId()); - MemberTicket memberTicket = memberTicketRepository.save(ticket.createMemberTicket(member, reservedAmount)); - return TicketingResponse.from(memberTicket); - } - - private Ticket findTicketById(Long ticketId) { - return ticketRepository.findById(ticketId) - .orElseThrow(() -> new NotFoundException(ErrorCode.TICKET_NOT_FOUND)); - } - - private Member findMemberById(Long memberId) { - return memberRepository.findById(memberId) - .orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - } - - private void validateAlreadyReserved(Member member, Ticket ticket) { - if (memberTicketRepository.existsByOwnerAndStage(member, ticket.getStage())) { - throw new BadRequestException(ErrorCode.RESERVE_TICKET_OVER_AMOUNT); - } - } - - private int reserveTicket(Long ticketId) { - TicketAmount ticketAmount = findTicketAmountById(ticketId); - ticketAmount.increaseReservedAmount(); - return ticketAmount.getReservedAmount(); - } - - private TicketAmount findTicketAmountById(Long ticketId) { - return ticketAmountRepository.findByTicketIdForUpdate(ticketId) - .orElseThrow(() -> new NotFoundException(ErrorCode.TICKET_NOT_FOUND)); - } - @Transactional(readOnly = true) public StageTicketsResponse findStageTickets(Long stageId) { return StageTicketsResponse.from(ticketRepository.findAllByStageId(stageId)); diff --git a/backend/src/main/java/com/festago/application/TicketingService.java b/backend/src/main/java/com/festago/application/TicketingService.java new file mode 100644 index 000000000..9d2afc5b4 --- /dev/null +++ b/backend/src/main/java/com/festago/application/TicketingService.java @@ -0,0 +1,77 @@ +package com.festago.application; + +import com.festago.domain.Member; +import com.festago.domain.MemberRepository; +import com.festago.domain.MemberTicket; +import com.festago.domain.MemberTicketRepository; +import com.festago.domain.Ticket; +import com.festago.domain.TicketAmount; +import com.festago.domain.TicketAmountRepository; +import com.festago.domain.TicketRepository; +import com.festago.dto.TicketingRequest; +import com.festago.dto.TicketingResponse; +import com.festago.exception.BadRequestException; +import com.festago.exception.ErrorCode; +import com.festago.exception.NotFoundException; +import java.time.Clock; +import java.time.LocalDateTime; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class TicketingService { + + private final MemberTicketRepository memberTicketRepository; + private final TicketAmountRepository ticketAmountRepository; + private final TicketRepository ticketRepository; + private final MemberRepository memberRepository; + private final Clock clock; + + public TicketingService(MemberTicketRepository memberTicketRepository, + TicketAmountRepository ticketAmountRepository, + TicketRepository ticketRepository, MemberRepository memberRepository, Clock clock) { + this.memberTicketRepository = memberTicketRepository; + this.ticketAmountRepository = ticketAmountRepository; + this.ticketRepository = ticketRepository; + this.memberRepository = memberRepository; + this.clock = clock; + } + + public TicketingResponse ticketing(Long memberId, TicketingRequest request) { + Ticket ticket = findTicketById(request.ticketId()); + Member member = findMemberById(memberId); + validateAlreadyReserved(member, ticket); + int reserveSequence = getReserveSequence(request.ticketId()); + MemberTicket memberTicket = ticket.createMemberTicket(member, reserveSequence, LocalDateTime.now(clock)); + memberTicketRepository.save(memberTicket); + return TicketingResponse.from(memberTicket); + } + + private Ticket findTicketById(Long ticketId) { + return ticketRepository.findById(ticketId) + .orElseThrow(() -> new NotFoundException(ErrorCode.TICKET_NOT_FOUND)); + } + + private Member findMemberById(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + } + + private void validateAlreadyReserved(Member member, Ticket ticket) { + if (memberTicketRepository.existsByOwnerAndStage(member, ticket.getStage())) { + throw new BadRequestException(ErrorCode.RESERVE_TICKET_OVER_AMOUNT); + } + } + + private int getReserveSequence(Long ticketId) { + TicketAmount ticketAmount = findTicketAmountById(ticketId); + ticketAmount.increaseReservedAmount(); + return ticketAmount.getReservedAmount(); + } + + private TicketAmount findTicketAmountById(Long ticketId) { + return ticketAmountRepository.findByTicketIdForUpdate(ticketId) + .orElseThrow(() -> new NotFoundException(ErrorCode.TICKET_NOT_FOUND)); + } +} diff --git a/backend/src/main/java/com/festago/auth/domain/Login.java b/backend/src/main/java/com/festago/auth/annotation/Admin.java similarity index 78% rename from backend/src/main/java/com/festago/auth/domain/Login.java rename to backend/src/main/java/com/festago/auth/annotation/Admin.java index 951306ef8..6820714d5 100644 --- a/backend/src/main/java/com/festago/auth/domain/Login.java +++ b/backend/src/main/java/com/festago/auth/annotation/Admin.java @@ -1,4 +1,4 @@ -package com.festago.auth.domain; +package com.festago.auth.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -7,6 +7,6 @@ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) -public @interface Login { +public @interface Admin { } diff --git a/backend/src/main/java/com/festago/auth/annotation/Anonymous.java b/backend/src/main/java/com/festago/auth/annotation/Anonymous.java new file mode 100644 index 000000000..e3fa0b1bc --- /dev/null +++ b/backend/src/main/java/com/festago/auth/annotation/Anonymous.java @@ -0,0 +1,12 @@ +package com.festago.auth.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Anonymous { + +} diff --git a/backend/src/main/java/com/festago/auth/annotation/Member.java b/backend/src/main/java/com/festago/auth/annotation/Member.java new file mode 100644 index 000000000..1bc35c87f --- /dev/null +++ b/backend/src/main/java/com/festago/auth/annotation/Member.java @@ -0,0 +1,12 @@ +package com.festago.auth.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Member { + +} diff --git a/backend/src/main/java/com/festago/auth/application/AdminAuthService.java b/backend/src/main/java/com/festago/auth/application/AdminAuthService.java new file mode 100644 index 000000000..a5e788238 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/application/AdminAuthService.java @@ -0,0 +1,86 @@ +package com.festago.auth.application; + +import com.festago.auth.domain.Admin; +import com.festago.auth.domain.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 java.util.Objects; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class AdminAuthService { + + private static final String ROOT_ADMIN = "admin"; + + 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); + validatePassword(admin.getPassword(), request.password()); + AuthPayload authPayload = getAuthPayload(admin); + return authProvider.provide(authPayload); + } + + private Admin findAdmin(AdminLoginRequest request) { + return adminRepository.findByUsername(request.username()) + .orElseThrow(() -> new UnauthorizedException(ErrorCode.INCORRECT_PASSWORD_OR_ACCOUNT)); + } + + private void validatePassword(String password, String comparePassword) { + if (!Objects.equals(password, comparePassword)) { + throw new UnauthorizedException(ErrorCode.INCORRECT_PASSWORD_OR_ACCOUNT); + } + } + + private AuthPayload getAuthPayload(Admin admin) { + return new AuthPayload(admin.getId(), Role.ADMIN); + } + + public void initializeRootAdmin(String password) { + adminRepository.findByUsername(ROOT_ADMIN).ifPresentOrElse(admin -> { + throw new BadRequestException(ErrorCode.DUPLICATE_ACCOUNT_USERNAME); + }, () -> adminRepository.save(new Admin(ROOT_ADMIN, password))); + } + + public AdminSignupResponse signup(Long adminId, AdminSignupRequest request) { + validateRootAdmin(adminId); + String username = request.username(); + String password = request.password(); + validateExistsUsername(username); + Admin admin = adminRepository.save(new Admin(username, password)); + return new AdminSignupResponse(admin.getUsername()); + } + + private void validateExistsUsername(String username) { + if (adminRepository.existsByUsername(username)) { + throw new BadRequestException(ErrorCode.DUPLICATE_ACCOUNT_USERNAME); + } + } + + private void validateRootAdmin(Long adminId) { + adminRepository.findById(adminId) + .map(Admin::getUsername) + .filter(username -> Objects.equals(username, ROOT_ADMIN)) + .ifPresentOrElse(username -> { + }, () -> { + throw new ForbiddenException(ErrorCode.NOT_ENOUGH_PERMISSION); + }); + } +} 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 dcd0c95a2..8668782c8 100644 --- a/backend/src/main/java/com/festago/auth/application/AuthService.java +++ b/backend/src/main/java/com/festago/auth/application/AuthService.java @@ -4,6 +4,7 @@ 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; @@ -45,7 +46,7 @@ private UserInfo getUserInfo(LoginRequest request) { } private String getAccessToken(Member member) { - return authProvider.provide(new AuthPayload(member.getId())); + return authProvider.provide(new AuthPayload(member.getId(), Role.MEMBER)); } private Member signUp(UserInfo userInfo) { 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 5f8753133..68a6eff6d 100644 --- a/backend/src/main/java/com/festago/auth/config/LoginConfig.java +++ b/backend/src/main/java/com/festago/auth/config/LoginConfig.java @@ -1,22 +1,68 @@ package com.festago.auth.config; -import com.festago.auth.presentation.LoginMemberResolver; +import com.festago.auth.domain.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 java.util.List; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class LoginConfig implements WebMvcConfigurer { - private final LoginMemberResolver loginMemberResolver; + private final AuthExtractor authExtractor; + private final AuthenticateContext authenticateContext; - public LoginConfig(LoginMemberResolver loginMemberResolver) { - this.loginMemberResolver = loginMemberResolver; + public LoginConfig(AuthExtractor authExtractor, AuthenticateContext context) { + this.authExtractor = authExtractor; + this.authenticateContext = context; } @Override public void addArgumentResolvers(List resolvers) { - resolvers.add(loginMemberResolver); + resolvers.add(new RoleArgumentResolver(Role.MEMBER, authenticateContext)); + resolvers.add(new RoleArgumentResolver(Role.ADMIN, authenticateContext)); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminAuthInterceptor()) + .addPathPatterns("/admin/**", "/js/admin/**") + .excludePathPatterns("/admin/login", "/admin/initialize"); + registry.addInterceptor(memberAuthInterceptor()) + .addPathPatterns("/member-tickets/**", "/members/**"); + } + + @Bean + public AuthInterceptor adminAuthInterceptor() { + return AuthInterceptor.builder() + .authExtractor(authExtractor) + .tokenExtractor(new CookieTokenExtractor()) + .authenticateContext(authenticateContext) + .role(Role.ADMIN) + .build(); + } + + @Bean + public AuthInterceptor memberAuthInterceptor() { + return AuthInterceptor.builder() + .authExtractor(authExtractor) + .tokenExtractor(new HeaderTokenExtractor()) + .authenticateContext(authenticateContext) + .role(Role.MEMBER) + .build(); + } + + @Bean + public ErrorLogger errorLogger() { + return new ErrorLogger(); } } diff --git a/backend/src/main/java/com/festago/auth/domain/Admin.java b/backend/src/main/java/com/festago/auth/domain/Admin.java new file mode 100644 index 000000000..abf4530c5 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/domain/Admin.java @@ -0,0 +1,43 @@ +package com.festago.auth.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class Admin { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String username; + + private String password; + + protected Admin() { + } + + public Admin(String username, String password) { + this(null, username, password); + } + + public Admin(Long id, String username, String password) { + this.id = id; + this.username = username; + this.password = password; + } + + public Long getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} diff --git a/backend/src/main/java/com/festago/auth/domain/AdminRepository.java b/backend/src/main/java/com/festago/auth/domain/AdminRepository.java new file mode 100644 index 000000000..399ad36d9 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/domain/AdminRepository.java @@ -0,0 +1,11 @@ +package com.festago.auth.domain; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AdminRepository extends JpaRepository { + + Optional findByUsername(String username); + + boolean existsByUsername(String username); +} 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 b55a7fecc..253b559e8 100644 --- a/backend/src/main/java/com/festago/auth/domain/AuthPayload.java +++ b/backend/src/main/java/com/festago/auth/domain/AuthPayload.java @@ -6,14 +6,16 @@ public class AuthPayload { private final Long memberId; + private final Role role; - public AuthPayload(Long memberId) { - validate(memberId); + public AuthPayload(Long memberId, Role role) { + validate(memberId, role); this.memberId = memberId; + this.role = role; } - private void validate(Long memberId) { - if (memberId == null) { + private void validate(Long memberId, Role role) { + if (memberId == null || role == null) { throw new InternalServerException(ErrorCode.INVALID_AUTH_TOKEN_PAYLOAD); } } @@ -21,4 +23,8 @@ private void validate(Long memberId) { public Long getMemberId() { return memberId; } + + public Role getRole() { + return role; + } } diff --git a/backend/src/main/java/com/festago/auth/domain/AuthProvider.java b/backend/src/main/java/com/festago/auth/domain/AuthProvider.java index 531630df6..195dc78c2 100644 --- a/backend/src/main/java/com/festago/auth/domain/AuthProvider.java +++ b/backend/src/main/java/com/festago/auth/domain/AuthProvider.java @@ -2,5 +2,5 @@ public interface AuthProvider { - String provide(AuthPayload member); + String provide(AuthPayload authPayload); } diff --git a/backend/src/main/java/com/festago/auth/domain/Role.java b/backend/src/main/java/com/festago/auth/domain/Role.java new file mode 100644 index 000000000..28e1924dd --- /dev/null +++ b/backend/src/main/java/com/festago/auth/domain/Role.java @@ -0,0 +1,33 @@ +package com.festago.auth.domain; + +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 java.lang.annotation.Annotation; + +public enum Role { + ANONYMOUS(Anonymous.class), + MEMBER(Member.class), + ADMIN(Admin.class), + ; + + private final Class extends Annotation> annotation; + + Role(Class extends Annotation> annotation) { + this.annotation = annotation; + } + + public static Role from(String role) { + try { + return valueOf(role); + } catch (NullPointerException | IllegalArgumentException e) { + throw new InternalServerException(ErrorCode.INVALID_ROLE_NAME); + } + } + + public Class extends Annotation> getAnnotation() { + return annotation; + } +} diff --git a/backend/src/main/java/com/festago/auth/domain/TokenExtractor.java b/backend/src/main/java/com/festago/auth/domain/TokenExtractor.java new file mode 100644 index 000000000..004c60bb6 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/domain/TokenExtractor.java @@ -0,0 +1,9 @@ +package com.festago.auth.domain; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Optional; + +public interface TokenExtractor { + + Optional extract(HttpServletRequest request); +} diff --git a/backend/src/main/java/com/festago/auth/dto/AdminLoginRequest.java b/backend/src/main/java/com/festago/auth/dto/AdminLoginRequest.java new file mode 100644 index 000000000..1b71a7e8e --- /dev/null +++ b/backend/src/main/java/com/festago/auth/dto/AdminLoginRequest.java @@ -0,0 +1,8 @@ +package com.festago.auth.dto; + +public record AdminLoginRequest( + String username, + 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 new file mode 100644 index 000000000..b35cf7337 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/dto/AdminSignupRequest.java @@ -0,0 +1,8 @@ +package com.festago.auth.dto; + +public record AdminSignupRequest( + String username, + String password +) { + +} diff --git a/backend/src/main/java/com/festago/auth/dto/AdminSignupResponse.java b/backend/src/main/java/com/festago/auth/dto/AdminSignupResponse.java new file mode 100644 index 000000000..99da63f2d --- /dev/null +++ b/backend/src/main/java/com/festago/auth/dto/AdminSignupResponse.java @@ -0,0 +1,7 @@ +package com.festago.auth.dto; + +public record AdminSignupResponse( + String username +) { + +} diff --git a/backend/src/main/java/com/festago/auth/dto/LoginMember.java b/backend/src/main/java/com/festago/auth/dto/LoginMember.java index b5c5b84c6..48a952fa6 100644 --- a/backend/src/main/java/com/festago/auth/dto/LoginMember.java +++ b/backend/src/main/java/com/festago/auth/dto/LoginMember.java @@ -1,5 +1,8 @@ package com.festago.auth.dto; +import io.swagger.v3.oas.annotations.Hidden; + +@Hidden public record LoginMember( Long memberId ) { diff --git a/backend/src/main/java/com/festago/auth/dto/RootAdminInitializeRequest.java b/backend/src/main/java/com/festago/auth/dto/RootAdminInitializeRequest.java new file mode 100644 index 000000000..e1d408d89 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/dto/RootAdminInitializeRequest.java @@ -0,0 +1,7 @@ +package com.festago.auth.dto; + +public record RootAdminInitializeRequest( + 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 new file mode 100644 index 000000000..1e8f03a41 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/CookieTokenExtractor.java @@ -0,0 +1,26 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.domain.TokenExtractor; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Objects; +import java.util.Optional; + +public class CookieTokenExtractor implements TokenExtractor { + + private static final String TOKEN = "token"; + + @Override + public Optional extract(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return Optional.empty(); + } + for (Cookie cookie : cookies) { + if (Objects.equals(TOKEN, cookie.getName())) { + return Optional.ofNullable(cookie.getValue()); + } + } + return Optional.empty(); + } +} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/HeaderTokenExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/HeaderTokenExtractor.java new file mode 100644 index 000000000..f0e4f6b09 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/infrastructure/HeaderTokenExtractor.java @@ -0,0 +1,33 @@ +package com.festago.auth.infrastructure; + +import com.festago.auth.domain.TokenExtractor; +import com.festago.exception.ErrorCode; +import com.festago.exception.UnauthorizedException; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Optional; +import org.springframework.http.HttpHeaders; + +public class HeaderTokenExtractor implements TokenExtractor { + + private static final String BEARER_TOKEN_PREFIX = "Bearer "; + + @Override + public Optional extract(HttpServletRequest request) { + String header = request.getHeader(HttpHeaders.AUTHORIZATION); + if (header == null) { + return Optional.empty(); + } + return Optional.of(extractToken(header)); + } + + private String extractToken(String header) { + validateHeader(header); + return header.substring(BEARER_TOKEN_PREFIX.length()).trim(); + } + + private void validateHeader(String header) { + if (!header.toLowerCase().startsWith(BEARER_TOKEN_PREFIX.toLowerCase())) { + throw new UnauthorizedException(ErrorCode.NOT_BEARER_TOKEN_TYPE); + } + } +} 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 e22115fb1..1ee63953d 100644 --- a/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthExtractor.java +++ b/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthExtractor.java @@ -2,6 +2,7 @@ import com.festago.auth.domain.AuthExtractor; import com.festago.auth.domain.AuthPayload; +import com.festago.auth.domain.Role; import com.festago.exception.ErrorCode; import com.festago.exception.UnauthorizedException; import io.jsonwebtoken.Claims; @@ -16,6 +17,7 @@ public class JwtAuthExtractor implements AuthExtractor { private static final String MEMBER_ID_KEY = "memberId"; + private static final String ROLE_ID_KEY = "role"; private final JwtParser jwtParser; @@ -30,7 +32,8 @@ public JwtAuthExtractor(String secretKey) { public AuthPayload extract(String token) { Claims claims = getClaims(token); Long memberId = claims.get(MEMBER_ID_KEY, Long.class); - return new AuthPayload(memberId); + String role = claims.get(ROLE_ID_KEY, String.class); + return new AuthPayload(memberId, Role.from(role)); } private Claims getClaims(String code) { @@ -39,7 +42,7 @@ private Claims getClaims(String code) { .getBody(); } catch (ExpiredJwtException e) { throw new UnauthorizedException(ErrorCode.EXPIRED_AUTH_TOKEN); - } catch (JwtException e) { + } catch (JwtException | IllegalArgumentException e) { throw new UnauthorizedException(ErrorCode.INVALID_AUTH_TOKEN); } } 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 9956fc7e5..7e06132c3 100644 --- a/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthProvider.java +++ b/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthProvider.java @@ -14,6 +14,7 @@ public class JwtAuthProvider implements AuthProvider { private static final int SECOND_FACTOR = 60; private static final int MILLISECOND_FACTOR = 1000; private static final String MEMBER_ID_KEY = "memberId"; + private static final String ROLE_ID_KEY = "role"; private final SecretKey key; private final long expirationMinutes; @@ -28,6 +29,7 @@ public String provide(AuthPayload authPayload) { Date now = new Date(); return Jwts.builder() .claim(MEMBER_ID_KEY, authPayload.getMemberId()) + .claim(ROLE_ID_KEY, authPayload.getRole()) .setIssuedAt(now) .setExpiration(new Date(now.getTime() + expirationMinutes * SECOND_FACTOR * MILLISECOND_FACTOR)) .signWith(key, SignatureAlgorithm.HS256) diff --git a/backend/src/main/java/com/festago/auth/presentation/AuthController.java b/backend/src/main/java/com/festago/auth/presentation/AuthController.java index e898f03a1..65d81bb1a 100644 --- a/backend/src/main/java/com/festago/auth/presentation/AuthController.java +++ b/backend/src/main/java/com/festago/auth/presentation/AuthController.java @@ -1,10 +1,12 @@ package com.festago.auth.presentation; +import com.festago.auth.annotation.Member; import com.festago.auth.application.AuthService; -import com.festago.auth.domain.Login; -import com.festago.auth.dto.LoginMember; import com.festago.auth.dto.LoginRequest; import com.festago.auth.dto.LoginResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -14,6 +16,7 @@ @RestController @RequestMapping("/auth") +@Tag(name = "로그인 관련 요청") public class AuthController { private final AuthService authService; @@ -23,6 +26,7 @@ public AuthController(AuthService authService) { } @PostMapping("/oauth2") + @Operation(description = "소셜 엑세스 토큰을 기반으로 로그인 요청을 보낸다.", summary = "OAuth2 로그인") public ResponseEntity login(@RequestBody LoginRequest request) { LoginResponse response = authService.login(request); return ResponseEntity.ok() @@ -30,8 +34,10 @@ public ResponseEntity login(@RequestBody LoginRequest request) { } @DeleteMapping - public ResponseEntity deleteMember(@Login LoginMember loginMember) { - authService.deleteMember(loginMember.memberId()); + @SecurityRequirement(name = "bearerAuth") + @Operation(description = "회원 탈퇴 요청을 보낸다.", summary = "유저 회원 탈퇴") + public ResponseEntity deleteMember(@Member Long memberId) { + authService.deleteMember(memberId); return ResponseEntity.ok() .build(); } diff --git a/backend/src/main/java/com/festago/auth/presentation/AuthInterceptor.java b/backend/src/main/java/com/festago/auth/presentation/AuthInterceptor.java new file mode 100644 index 000000000..87c401da5 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/presentation/AuthInterceptor.java @@ -0,0 +1,82 @@ +package com.festago.auth.presentation; + +import com.festago.auth.domain.AuthExtractor; +import com.festago.auth.domain.AuthPayload; +import com.festago.auth.domain.Role; +import com.festago.auth.domain.TokenExtractor; +import com.festago.exception.ErrorCode; +import com.festago.exception.ForbiddenException; +import com.festago.exception.UnauthorizedException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.Assert; +import org.springframework.web.servlet.HandlerInterceptor; + +public class AuthInterceptor implements HandlerInterceptor { + + private final AuthExtractor authExtractor; + private final TokenExtractor tokenExtractor; + private final AuthenticateContext authenticateContext; + private final Role role; + + private AuthInterceptor(AuthExtractor authExtractor, TokenExtractor tokenExtractor, + AuthenticateContext authenticateContext, Role role) { + Assert.notNull(authExtractor, "The authExtractor must not be null"); + Assert.notNull(tokenExtractor, "The tokenExtractor must not be null"); + Assert.notNull(authenticateContext, "The authenticateContext must not be null"); + Assert.notNull(role, "The role must not be null"); + this.authExtractor = authExtractor; + this.tokenExtractor = tokenExtractor; + this.authenticateContext = authenticateContext; + this.role = role; + } + + public static AuthInterceptorBuilder builder() { + return new AuthInterceptorBuilder(); + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String token = tokenExtractor.extract(request) + .orElseThrow(() -> new UnauthorizedException(ErrorCode.NEED_AUTH_TOKEN)); + AuthPayload payload = authExtractor.extract(token); + if (payload.getRole() != this.role) { + throw new ForbiddenException(ErrorCode.NOT_ENOUGH_PERMISSION); + } + authenticateContext.setAuthenticate(payload.getMemberId(), payload.getRole()); + return true; + } + + public static class AuthInterceptorBuilder { + + private AuthExtractor authExtractor; + private TokenExtractor tokenExtractor; + private AuthenticateContext authenticateContext; + private Role role; + + public AuthInterceptorBuilder authExtractor(AuthExtractor authExtractor) { + this.authExtractor = authExtractor; + return this; + } + + public AuthInterceptorBuilder authenticateContext(AuthenticateContext authenticateContext) { + this.authenticateContext = authenticateContext; + return this; + } + + public AuthInterceptorBuilder tokenExtractor(TokenExtractor tokenExtractor) { + this.tokenExtractor = tokenExtractor; + return this; + } + + public AuthInterceptorBuilder role(Role role) { + this.role = role; + return this; + } + + public AuthInterceptor build() { + return new AuthInterceptor(authExtractor, tokenExtractor, authenticateContext, role); + } + } +} diff --git a/backend/src/main/java/com/festago/auth/presentation/AuthenticateContext.java b/backend/src/main/java/com/festago/auth/presentation/AuthenticateContext.java new file mode 100644 index 000000000..2edae0e28 --- /dev/null +++ b/backend/src/main/java/com/festago/auth/presentation/AuthenticateContext.java @@ -0,0 +1,26 @@ +package com.festago.auth.presentation; + +import com.festago.auth.domain.Role; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +public class AuthenticateContext { + + private Long id; + private Role role = Role.ANONYMOUS; + + public void setAuthenticate(Long id, Role role) { + this.id = id; + this.role = role; + } + + public Long getId() { + return id; + } + + public Role getRole() { + return role; + } +} diff --git a/backend/src/main/java/com/festago/auth/presentation/ErrorFilter.java b/backend/src/main/java/com/festago/auth/presentation/ErrorFilter.java new file mode 100644 index 000000000..8bdb75a0b --- /dev/null +++ b/backend/src/main/java/com/festago/auth/presentation/ErrorFilter.java @@ -0,0 +1,29 @@ +package com.festago.auth.presentation; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +public class ErrorFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + doFilter(request, response, filterChain); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + if (Objects.equals(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) { + return; + } + request.getRequestDispatcher("/error/404").forward(request, response); + } + } +} diff --git a/backend/src/main/java/com/festago/auth/presentation/LoginMemberResolver.java b/backend/src/main/java/com/festago/auth/presentation/LoginMemberResolver.java deleted file mode 100644 index a9ed72246..000000000 --- a/backend/src/main/java/com/festago/auth/presentation/LoginMemberResolver.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.festago.auth.presentation; - -import com.festago.auth.domain.AuthExtractor; -import com.festago.auth.domain.AuthPayload; -import com.festago.auth.domain.Login; -import com.festago.auth.dto.LoginMember; -import com.festago.exception.ErrorCode; -import com.festago.exception.UnauthorizedException; -import org.springframework.core.MethodParameter; -import org.springframework.http.HttpHeaders; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; - -@Component -public class LoginMemberResolver implements HandlerMethodArgumentResolver { - - private static final String BEARER_TOKEN_PREFIX = "Bearer "; - - private final AuthExtractor authExtractor; - - public LoginMemberResolver(AuthExtractor authExtractor) { - this.authExtractor = authExtractor; - } - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return parameter.getParameterType().equals(LoginMember.class) && parameter.hasParameterAnnotation(Login.class); - } - - @Override - public LoginMember resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { - String header = webRequest.getHeader(HttpHeaders.AUTHORIZATION); - String token = extractToken(header); - AuthPayload authPayload = authExtractor.extract(token); - return new LoginMember(authPayload.getMemberId()); - } - - private String extractToken(String header) { - validateHeader(header); - return header.substring(BEARER_TOKEN_PREFIX.length()).trim(); - } - - private void validateHeader(String header) { - if (header == null) { - throw new UnauthorizedException(ErrorCode.NEED_AUTH_TOKEN); - } - if (!header.toLowerCase().startsWith(BEARER_TOKEN_PREFIX.toLowerCase())) { - throw new UnauthorizedException(ErrorCode.NOT_BEARER_TOKEN_TYPE); - } - } -} diff --git a/backend/src/main/java/com/festago/auth/presentation/RoleArgumentResolver.java b/backend/src/main/java/com/festago/auth/presentation/RoleArgumentResolver.java new file mode 100644 index 000000000..9fd393edb --- /dev/null +++ b/backend/src/main/java/com/festago/auth/presentation/RoleArgumentResolver.java @@ -0,0 +1,39 @@ +package com.festago.auth.presentation; + +import com.festago.auth.domain.Role; +import com.festago.exception.ErrorCode; +import com.festago.exception.ForbiddenException; +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +public class RoleArgumentResolver implements HandlerMethodArgumentResolver { + + private final Role role; + private final AuthenticateContext authenticateContext; + + public RoleArgumentResolver(Role role, AuthenticateContext authenticateContext) { + Assert.notNull(authenticateContext, "The authenticateContext must not be null"); + Assert.notNull(role, "The role must not be null"); + this.role = role; + this.authenticateContext = authenticateContext; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(Long.class) && parameter.hasParameterAnnotation( + role.getAnnotation()); + } + + @Override + public Long resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + if (authenticateContext.getRole() != this.role) { + throw new ForbiddenException(ErrorCode.NOT_ENOUGH_PERMISSION); + } + return authenticateContext.getId(); + } +} diff --git a/backend/src/main/java/com/festago/config/RequestWrappingFilter.java b/backend/src/main/java/com/festago/config/RequestWrappingFilter.java new file mode 100644 index 000000000..dba5f7d6e --- /dev/null +++ b/backend/src/main/java/com/festago/config/RequestWrappingFilter.java @@ -0,0 +1,24 @@ +package com.festago.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; + +@Component +public class RequestWrappingFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request); + + chain.doFilter(wrappingRequest, response); + } +} diff --git a/backend/src/main/java/com/festago/config/SwaggerConfig.java b/backend/src/main/java/com/festago/config/SwaggerConfig.java new file mode 100644 index 000000000..c575b539e --- /dev/null +++ b/backend/src/main/java/com/festago/config/SwaggerConfig.java @@ -0,0 +1,31 @@ +package com.festago.config; + +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@SecurityScheme( + name = "bearerAuth", + type = SecuritySchemeType.HTTP, + bearerFormat = "Jwt", + scheme = "bearer" +) +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(apiInfo()); + } + + private Info apiInfo() { + return new Info() + .title("Festago-API-Document") + .description("페스타고 API 문서입니다.") + .version("1.0.0"); + } +} diff --git a/backend/src/main/java/com/festago/config/TimeConfig.java b/backend/src/main/java/com/festago/config/TimeConfig.java new file mode 100644 index 000000000..3f4fe729e --- /dev/null +++ b/backend/src/main/java/com/festago/config/TimeConfig.java @@ -0,0 +1,14 @@ +package com.festago.config; + +import java.time.Clock; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TimeConfig { + + @Bean + public Clock clock() { + return Clock.systemDefaultZone(); + } +} diff --git a/backend/src/main/java/com/festago/domain/School.java b/backend/src/main/java/com/festago/domain/School.java new file mode 100644 index 000000000..6e701a961 --- /dev/null +++ b/backend/src/main/java/com/festago/domain/School.java @@ -0,0 +1,74 @@ +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 index e1b7e10e0..7d377c879 100644 --- a/backend/src/main/java/com/festago/domain/Stage.java +++ b/backend/src/main/java/com/festago/domain/Stage.java @@ -96,6 +96,10 @@ private void validateTime(LocalDateTime startTime, LocalDateTime ticketOpenTime, } } + public boolean isStart(LocalDateTime currentTime) { + return currentTime.isAfter(startTime); + } + public Long getId() { return id; } diff --git a/backend/src/main/java/com/festago/domain/Student.java b/backend/src/main/java/com/festago/domain/Student.java new file mode 100644 index 000000000..768b0e462 --- /dev/null +++ b/backend/src/main/java/com/festago/domain/Student.java @@ -0,0 +1,86 @@ +package com.festago.domain; + +import com.festago.exception.ErrorCode; +import com.festago.exception.InternalServerException; +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.OneToOne; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@Entity +public class Student { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @OneToOne(fetch = FetchType.LAZY) + private Member member; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + private School school; + + @Size(max = 255) + @NotNull + private String username; + + protected Student() { + } + + public Student(Long id, Member member, School school, String username) { + validate(member, school, username); + this.id = id; + this.member = member; + this.school = school; + this.username = username; + } + + private void validate(Member member, School school, String username) { + checkNotNull(member, school, username); + checkLength(username); + } + + private void checkNotNull(Member member, School school, String username) { + if (member == null || + school == null || + username == null) { + throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } + + private void checkLength(String username) { + if (overLength(username, 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 Member getMember() { + return member; + } + + public School getSchool() { + return school; + } + + public String getUsername() { + return username; + } +} diff --git a/backend/src/main/java/com/festago/domain/Ticket.java b/backend/src/main/java/com/festago/domain/Ticket.java index f3416126a..e31b9b0c6 100644 --- a/backend/src/main/java/com/festago/domain/Ticket.java +++ b/backend/src/main/java/com/festago/domain/Ticket.java @@ -97,7 +97,10 @@ private void validateEntryTime(LocalDateTime currentTime, LocalDateTime entryTim } } - public MemberTicket createMemberTicket(Member member, int reservationSequence) { + public MemberTicket createMemberTicket(Member member, int reservationSequence, LocalDateTime currentTime) { + if (stage.isStart(currentTime)) { + throw new BadRequestException(ErrorCode.TICKET_CANNOT_RESERVE_STAGE_START); + } LocalDateTime entryTime = calculateEntryTime(reservationSequence); return new MemberTicket(member, stage, reservationSequence, entryTime, ticketType); } diff --git a/backend/src/main/java/com/festago/exception/ErrorCode.java b/backend/src/main/java/com/festago/exception/ErrorCode.java index f292dfbaa..f56329272 100644 --- a/backend/src/main/java/com/festago/exception/ErrorCode.java +++ b/backend/src/main/java/com/festago/exception/ErrorCode.java @@ -20,12 +20,18 @@ public enum ErrorCode { OAUTH2_NOT_SUPPORTED_SOCIAL_TYPE("해당 OAuth2 제공자는 지원되지 않습니다."), RESERVE_TICKET_OVER_AMOUNT("예매 가능한 수량을 초과했습니다."), OAUTH2_INVALID_TOKEN("잘못된 OAuth2 토큰입니다."), + TICKET_CANNOT_RESERVE_STAGE_START("공연의 시작 시간 이후로 예매할 수 없습니다."), // 401 EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."), INVALID_AUTH_TOKEN("올바르지 않은 로그인 토큰입니다."), NOT_BEARER_TOKEN_TYPE("Bearer 타입의 토큰이 아닙니다."), NEED_AUTH_TOKEN("로그인이 필요한 서비스입니다."), + INCORRECT_PASSWORD_OR_ACCOUNT("비밀번호가 틀렸거나, 해당 계정이 없습니다."), + DUPLICATE_ACCOUNT_USERNAME("해당 계정이 존재합니다."), + + // 403 + NOT_ENOUGH_PERMISSION("해당 권한이 없습니다."), // 404 MEMBER_TICKET_NOT_FOUND("존재하지 않은 멤버 티켓입니다."), @@ -44,6 +50,7 @@ public enum ErrorCode { DUPLICATE_SOCIAL_TYPE("중복된 OAuth2 제공자 입니다."), OAUTH2_PROVIDER_NOT_RESPONSE("OAuth2 제공자 서버에 문제가 발생했습니다."), INVALID_ENTRY_CODE_OFFSET("올바르지 않은 입장코드 오프셋입니다."), + INVALID_ROLE_NAME("해당하는 Role이 없습니다."), FOR_TEST_ERROR("테스트용 에러입니다."), ; diff --git a/backend/src/main/java/com/festago/exception/ForbiddenException.java b/backend/src/main/java/com/festago/exception/ForbiddenException.java new file mode 100644 index 000000000..b1b9f400e --- /dev/null +++ b/backend/src/main/java/com/festago/exception/ForbiddenException.java @@ -0,0 +1,8 @@ +package com.festago.exception; + +public class ForbiddenException extends FestaGoException { + + public ForbiddenException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/src/main/java/com/festago/presentation/AdminController.java b/backend/src/main/java/com/festago/presentation/AdminController.java index 20370d257..0f64e1217 100644 --- a/backend/src/main/java/com/festago/presentation/AdminController.java +++ b/backend/src/main/java/com/festago/presentation/AdminController.java @@ -4,6 +4,12 @@ import com.festago.application.FestivalService; import com.festago.application.StageService; import com.festago.application.TicketService; +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; @@ -14,34 +20,47 @@ import com.festago.exception.ErrorCode; import com.festago.exception.InternalServerException; import jakarta.validation.Valid; +import io.swagger.v3.oas.annotations.Hidden; +import com.festago.exception.UnauthorizedException; +import jakarta.servlet.http.HttpServletResponse; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Optional; 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.GetMapping; 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") +@Hidden public class AdminController { private final FestivalService festivalService; private final StageService stageService; private final TicketService ticketService; private final AdminService adminService; + private final AdminAuthService adminAuthService; private final Optional properties; public AdminController(FestivalService festivalService, StageService stageService, TicketService ticketService, - AdminService adminService, Optional buildProperties) { + AdminService adminService, AdminAuthService adminAuthService, + Optional buildProperties) { this.festivalService = festivalService; this.stageService = stageService; this.ticketService = ticketService; this.adminService = adminService; + this.adminAuthService = adminAuthService; this.properties = buildProperties; } @@ -68,7 +87,27 @@ public ResponseEntity createTicket(@RequestBody TicketCrea @GetMapping public ModelAndView adminPage() { - return new ModelAndView("admin"); + return new ModelAndView("admin/admin-page"); + } + + @GetMapping("/login") + public ModelAndView loginPage() { + return new ModelAndView("admin/login"); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody AdminLoginRequest request) { + String token = adminAuthService.login(request); + return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, getCookie(token)) + .build(); + } + + private String getCookie(String token) { + return ResponseCookie.from("token", token) + .httpOnly(true) + .secure(true) + .path("/") + .build().toString(); } @GetMapping("/data") @@ -94,4 +133,33 @@ public ResponseEntity getError() { public ResponseEntity getWarn() { throw new InternalServerException(ErrorCode.FOR_TEST_ERROR); } + + @PostMapping("/initialize") + public ResponseEntity initializeRootAdmin(@RequestBody RootAdminInitializeRequest request) { + adminAuthService.initializeRootAdmin(request.password()); + return ResponseEntity.ok() + .build(); + } + + @GetMapping("/signup") + public ModelAndView signupPage() { + return new ModelAndView("admin/signup"); + } + + @PostMapping("/signup") + public ResponseEntity signupAdminAccount(@RequestBody AdminSignupRequest request, + @Admin Long adminId) { + AdminSignupResponse response = adminAuthService.signup(adminId, request); + return ResponseEntity.ok() + .body(response); + } + + @ExceptionHandler(UnauthorizedException.class) + public View handle(UnauthorizedException e, HttpServletResponse response) { + if (e.getErrorCode() == ErrorCode.EXPIRED_AUTH_TOKEN) { + return new RedirectView("/admin/login"); + } + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return new InternalResourceView("/error/404"); + } } diff --git a/backend/src/main/java/com/festago/presentation/ErrorLogger.java b/backend/src/main/java/com/festago/presentation/ErrorLogger.java new file mode 100644 index 000000000..cb7a285ea --- /dev/null +++ b/backend/src/main/java/com/festago/presentation/ErrorLogger.java @@ -0,0 +1,33 @@ +package com.festago.presentation; + +import java.util.EnumMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; + +public class ErrorLogger { + + private static final Logger errorLogger = LoggerFactory.getLogger("ErrorLogger"); + + private final EnumMap logFunctions = new EnumMap<>(Level.class); + + public ErrorLogger() { + this.logFunctions.put(Level.INFO, errorLogger::info); + this.logFunctions.put(Level.WARN, errorLogger::warn); + this.logFunctions.put(Level.ERROR, errorLogger::error); + } + + public LogFunction get(Level level) { + return logFunctions.get(level); + } + + public boolean isEnabledForLevel(Level level) { + return errorLogger.isEnabledForLevel(level); + } + + @FunctionalInterface + public interface LogFunction { + + void log(String format, Object... arguments); + } +} diff --git a/backend/src/main/java/com/festago/presentation/FestivalController.java b/backend/src/main/java/com/festago/presentation/FestivalController.java index 6b7e1b431..bab130bb8 100644 --- a/backend/src/main/java/com/festago/presentation/FestivalController.java +++ b/backend/src/main/java/com/festago/presentation/FestivalController.java @@ -3,6 +3,8 @@ import com.festago.application.FestivalService; import com.festago.dto.FestivalDetailResponse; import com.festago.dto.FestivalsResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -11,6 +13,7 @@ @RestController @RequestMapping("/festivals") +@Tag(name = "축제 정보 요청") public class FestivalController { private final FestivalService festivalService; @@ -20,6 +23,7 @@ public FestivalController(FestivalService festivalService) { } @GetMapping + @Operation(description = "모든 축제들을 조회한다.", summary = "축제 목록 조회") public ResponseEntity findAll() { FestivalsResponse response = festivalService.findAll(); return ResponseEntity.ok() @@ -27,6 +31,7 @@ public ResponseEntity findAll() { } @GetMapping("/{festivalId}") + @Operation(description = "해당 Id 의 축제를 조회한다.", summary = "축제 상세 정보 조회") public ResponseEntity findDetail(@PathVariable Long festivalId) { FestivalDetailResponse response = festivalService.findDetail(festivalId); return ResponseEntity.ok() diff --git a/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java b/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java index 7cbebf071..82bc674ee 100644 --- a/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java @@ -4,17 +4,11 @@ import com.festago.exception.BadRequestException; import com.festago.exception.ErrorCode; import com.festago.exception.FestaGoException; +import com.festago.exception.ForbiddenException; import com.festago.exception.InternalServerException; import com.festago.exception.NotFoundException; import com.festago.exception.UnauthorizedException; import jakarta.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.Scanner; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.slf4j.event.Level; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; @@ -27,31 +21,40 @@ @RestControllerAdvice public class GlobalExceptionHandler { - private static final String LOG_FORMAT = "\n[🚨ERROR]\n{}: {} ({} {})\n[CALLED BY] {} \n[REQUEST BODY] \n{}"; - private static final String LOG_FORMAT_WITH_TRACE = "\n[🚨ERROR]\n{} ({} {})\n[CALLED BY] {} \n[REQUEST BODY] \n{}"; - private static final Logger errorLogger = LoggerFactory.getLogger("ErrorLogger"); + private static final String LOG_FORMAT_ERROR_CODE = "\n[🚨ERROR] - ({} {})\n{}"; + private static final String LOG_FORMAT = "\n[🚨ERROR] - ({} {})"; + /* + [🚨ERROR] - (POST /admin/warn) + FOR_TEST_ERROR + com.festago.exception.InternalServerException: 테스트용 에러입니다. + */ - private static final Map logFunctions = Map.of( - Level.INFO, errorLogger::info, - Level.WARN, errorLogger::warn, - Level.ERROR, errorLogger::error - ); + private final ErrorLogger errorLogger; + + public GlobalExceptionHandler(ErrorLogger errorLogger) { + this.errorLogger = errorLogger; + } @ExceptionHandler(BadRequestException.class) - public ResponseEntity handle(BadRequestException e, HttpServletRequest request) throws IOException { + public ResponseEntity handle(BadRequestException e, HttpServletRequest request) { log(Level.INFO, e, request); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.from(e)); } @ExceptionHandler(UnauthorizedException.class) - public ResponseEntity handle(UnauthorizedException e, HttpServletRequest request) - throws IOException { + public ResponseEntity handle(UnauthorizedException e, HttpServletRequest request) { log(Level.INFO, e, request); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.from(e)); } + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handle(ForbiddenException e, HttpServletRequest request) { + log(Level.INFO, e, request); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.from(e)); + } + @ExceptionHandler(NotFoundException.class) - public ResponseEntity handle(NotFoundException e, HttpServletRequest request) throws IOException { + public ResponseEntity handle(NotFoundException e, HttpServletRequest request) { log(Level.INFO, e, request); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.from(e)); } @@ -59,68 +62,39 @@ public ResponseEntity handle(NotFoundException e, HttpServletRequ @ExceptionHandler(MethodArgumentNotValidException.class) protected ResponseEntity handleMethodArgumentNotValid( MethodArgumentNotValidException e, - HttpServletRequest request) throws IOException { + HttpServletRequest request) { log(Level.INFO, new BadRequestException(ErrorCode.INVALID_REQUEST_ARGUMENT), request); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(ErrorResponse.from(ErrorCode.INVALID_REQUEST_ARGUMENT, e)); } @ExceptionHandler(InternalServerException.class) - public ResponseEntity handle(InternalServerException e, HttpServletRequest request) - throws IOException { - logWithTrace(Level.WARN, e, request); + public ResponseEntity handle(InternalServerException e, HttpServletRequest request) { + log(Level.WARN, e, request); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ErrorResponse.from(ErrorCode.INTERNAL_SERVER_ERROR)); } @ExceptionHandler(Exception.class) - public ResponseEntity handle(Exception e, HttpServletRequest request) throws IOException { - logWithTrace(Level.ERROR, e, request); + public ResponseEntity handle(Exception e, HttpServletRequest request) { + log(Level.ERROR, e, request); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ErrorResponse.from(ErrorCode.INTERNAL_SERVER_ERROR)); } - private void log(Level logLevel, FestaGoException e, HttpServletRequest request) throws IOException { + private void log(Level logLevel, FestaGoException e, HttpServletRequest request) { if (!errorLogger.isEnabledForLevel(logLevel)) { return; } - logFunctions.get(logLevel) - .log(LOG_FORMAT, e.getErrorCode(), e.getMessage(), request.getMethod(), request.getRequestURI(), - e.getStackTrace()[0], getRequestPayload(request)); + errorLogger.get(logLevel) + .log(LOG_FORMAT_ERROR_CODE, request.getMethod(), request.getRequestURI(), e.getErrorCode(), e); } - private void logWithTrace(Level logLevel, FestaGoException e, HttpServletRequest request) throws IOException { + private void log(Level logLevel, Exception e, HttpServletRequest request) { if (!errorLogger.isEnabledForLevel(logLevel)) { return; } - logFunctions.get(logLevel) - .log(LOG_FORMAT, e.getErrorCode(), e.getMessage(), request.getMethod(), request.getRequestURI(), - e.getStackTrace()[0], getRequestPayload(request), e); - } - - private void logWithTrace(Level logLevel, Exception e, HttpServletRequest request) throws IOException { - if (!errorLogger.isEnabledForLevel(logLevel)) { - return; - } - logFunctions.get(logLevel) - .log(LOG_FORMAT_WITH_TRACE, e.getClass().getSimpleName(), request.getMethod(), request.getRequestURI(), - e.getStackTrace()[0], getRequestPayload(request), e); - } - - private String getRequestPayload(HttpServletRequest request) throws IOException { - try (InputStream inputStream = request.getInputStream(); Scanner scanner = new Scanner(inputStream, - StandardCharsets.UTF_8)) { - if (scanner.useDelimiter("\\A").hasNext()) { - return scanner.next(); - } else { - return ""; - } - } - } - - @FunctionalInterface - interface LogFunction { - - void log(String format, Object... arguments); + errorLogger.get(logLevel) + .log(LOG_FORMAT, request.getMethod(), request.getRequestURI(), e); } } diff --git a/backend/src/main/java/com/festago/presentation/MemberController.java b/backend/src/main/java/com/festago/presentation/MemberController.java index 4681cfd82..269b2ce51 100644 --- a/backend/src/main/java/com/festago/presentation/MemberController.java +++ b/backend/src/main/java/com/festago/presentation/MemberController.java @@ -1,9 +1,11 @@ package com.festago.presentation; import com.festago.application.MemberService; -import com.festago.auth.domain.Login; -import com.festago.auth.dto.LoginMember; +import com.festago.auth.annotation.Member; import com.festago.dto.MemberProfileResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -11,6 +13,7 @@ @RestController @RequestMapping("/members") +@Tag(name = "유저 정보 요청") public class MemberController { private final MemberService memberService; @@ -20,8 +23,10 @@ public MemberController(MemberService memberService) { } @GetMapping("/profile") - public ResponseEntity findMemberProfile(@Login LoginMember loginMember) { - MemberProfileResponse response = memberService.findMemberProfile(loginMember.memberId()); + @SecurityRequirement(name = "bearerAuth") + @Operation(description = "현재 로그인한 유저의 프로필 정보를 조회한다.", summary = "사용자 정보 조회") + public ResponseEntity findMemberProfile(@Member Long memberId) { + MemberProfileResponse response = memberService.findMemberProfile(memberId); return ResponseEntity.ok() .body(response); } diff --git a/backend/src/main/java/com/festago/presentation/MemberTicketController.java b/backend/src/main/java/com/festago/presentation/MemberTicketController.java index 0ca9e66d7..9ceee591d 100644 --- a/backend/src/main/java/com/festago/presentation/MemberTicketController.java +++ b/backend/src/main/java/com/festago/presentation/MemberTicketController.java @@ -2,14 +2,16 @@ import com.festago.application.EntryService; import com.festago.application.MemberTicketService; -import com.festago.application.TicketService; -import com.festago.auth.domain.Login; -import com.festago.auth.dto.LoginMember; +import com.festago.application.TicketingService; +import com.festago.auth.annotation.Member; import com.festago.dto.EntryCodeResponse; import com.festago.dto.MemberTicketResponse; import com.festago.dto.MemberTicketsResponse; import com.festago.dto.TicketingRequest; import com.festago.dto.TicketingResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -23,62 +25,68 @@ import org.springframework.web.bind.annotation.RestController; @RestController +@SecurityRequirement(name = "bearerAuth") @RequestMapping("/member-tickets") +@Tag(name = "유저 티켓 요청") public class MemberTicketController { private final EntryService entryService; private final MemberTicketService memberTicketService; - private final TicketService ticketService; + private final TicketingService ticketingService; public MemberTicketController(EntryService entryService, MemberTicketService memberTicketService, - TicketService ticketService) { + TicketingService ticketingService) { this.entryService = entryService; this.memberTicketService = memberTicketService; - this.ticketService = ticketService; + this.ticketingService = ticketingService; } @PostMapping("/{memberTicketId}/qr") - public ResponseEntity createQR( - @Login LoginMember loginMember, - @PathVariable Long memberTicketId) { - EntryCodeResponse response = entryService.createEntryCode(loginMember.memberId(), memberTicketId); + @Operation(description = "티켓 제시용 QR 코드를 생성한다.", summary = "티켓 제시용 QR 생성") + public ResponseEntity createQR(@Member Long memberId, + @PathVariable Long memberTicketId) { + EntryCodeResponse response = entryService.createEntryCode(memberId, memberTicketId); return ResponseEntity.ok() .body(response); } @PostMapping - public ResponseEntity ticketing( - @Login LoginMember loginMember, - @RequestBody TicketingRequest request) { - TicketingResponse response = ticketService.ticketing(loginMember.memberId(), request); + @Operation(description = "티켓을 예매한다.", summary = "티켓 예매") + public ResponseEntity ticketing(@Member Long memberId, + @RequestBody TicketingRequest request) { + TicketingResponse response = ticketingService.ticketing(memberId, request); return ResponseEntity.ok() .body(response); } @GetMapping("/{memberTicketId}") - public ResponseEntity findById( - @Login LoginMember loginMember, - @PathVariable Long memberTicketId) { - MemberTicketResponse response = memberTicketService.findById(loginMember.memberId(), memberTicketId); + @Operation(description = "로그인한 맴버의 특정 티켓을 조회한다.", summary = "특정 맴버 티켓 조회") + public ResponseEntity findById(@Member Long memberId, + @PathVariable Long memberTicketId) { + MemberTicketResponse response = memberTicketService.findById(memberId, memberTicketId); return ResponseEntity.ok() .body(response); } @GetMapping - public ResponseEntity findAll( - @Login LoginMember loginMember, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "100") int size) { + @Operation(description = "유저가 가진 모든 티켓을 조회한다.", summary = "예매 목록 조회") + public ResponseEntity findAll(@Member Long memberId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "100") int size) { Pageable pageRequest = PageRequest.of(page, size, Sort.by("entryTime").descending()); - MemberTicketsResponse response = memberTicketService.findAll(loginMember.memberId(), pageRequest); + MemberTicketsResponse response = memberTicketService.findAll(memberId, pageRequest); return ResponseEntity.ok() .body(response); } @GetMapping("/current") - public ResponseEntity findCurrent(@Login LoginMember loginMember) { + @Operation( + description = "유저의 티켓 중 입장 시간이 24시간이상 지나지 않은 티켓을 현재 시간에 가까운 순서대로 입장 가능, 입장 예정 티켓으로 구분하여 반환하다.", + summary = "현재 맴버 티켓 목록 조회" + ) + public ResponseEntity findCurrent(@Member Long memberId) { Pageable pageable = PageRequest.of(0, 100); - MemberTicketsResponse response = memberTicketService.findCurrent(loginMember.memberId(), pageable); + MemberTicketsResponse response = memberTicketService.findCurrent(memberId, pageable); return ResponseEntity.ok() .body(response); } diff --git a/backend/src/main/java/com/festago/presentation/StaffMemberTicketController.java b/backend/src/main/java/com/festago/presentation/StaffMemberTicketController.java index 4b8903f27..d3aeedd3b 100644 --- a/backend/src/main/java/com/festago/presentation/StaffMemberTicketController.java +++ b/backend/src/main/java/com/festago/presentation/StaffMemberTicketController.java @@ -4,6 +4,8 @@ import com.festago.application.EntryService; import com.festago.dto.TicketValidationRequest; import com.festago.dto.TicketValidationResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -12,6 +14,7 @@ @RestController @RequestMapping("/staff/member-tickets") +@Tag(name = "스태프 요청") public class StaffMemberTicketController { private final EntryService entryService; @@ -21,6 +24,7 @@ public StaffMemberTicketController(EntryService entryService) { } @PostMapping("/validation") + @Operation(description = "스태프가 티켓을 검사한다.", summary = "티켓 검사") public ResponseEntity validate(@RequestBody TicketValidationRequest request) { TicketValidationResponse response = entryService.validate(request); return ResponseEntity.ok() diff --git a/backend/src/main/java/com/festago/presentation/StageController.java b/backend/src/main/java/com/festago/presentation/StageController.java index 75a3d303f..928a190e3 100644 --- a/backend/src/main/java/com/festago/presentation/StageController.java +++ b/backend/src/main/java/com/festago/presentation/StageController.java @@ -2,6 +2,8 @@ import com.festago.application.TicketService; import com.festago.dto.StageTicketsResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -10,6 +12,7 @@ @RestController @RequestMapping("/stages") +@Tag(name = "공연 정보 요청") public class StageController { private final TicketService ticketService; @@ -19,6 +22,7 @@ public StageController(TicketService ticketService) { } @GetMapping("/{stageId}/tickets") + @Operation(description = "특정 무대의 티켓 정보를 보여준다.", summary = "무대 티켓 목록 조회") public ResponseEntity findStageTickets(@PathVariable Long stageId) { StageTicketsResponse response = ticketService.findStageTickets(stageId); return ResponseEntity.ok() diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index bcda0e17e..d3df161d3 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -2,9 +2,9 @@ + value="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] [%thread] %-5level [%C.%M:%L] - %msg %ex{5}%n"/> + value="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %green([%thread]) %highlight(%-5level) %boldWhite([%C.%M:%yellow(%L)]) - %msg %ex{5}%n"/> diff --git a/backend/src/main/resources/static/css/404.css b/backend/src/main/resources/static/css/404.css new file mode 100644 index 000000000..cddded461 --- /dev/null +++ b/backend/src/main/resources/static/css/404.css @@ -0,0 +1,33 @@ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + margin: 0; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + height: 100vh; +} + +.container { + width: 100%; + max-width: 600px; + background-color: #ffffff; + box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1); +} + +.content { + text-align: center; + padding: 50px; +} + +h3 { + font-size: 40px; + color: #37569a; + margin-bottom: 10px; +} + +p { + color: #666; + font-size: 20px; +} diff --git a/backend/src/main/resources/static/style.css b/backend/src/main/resources/static/css/admin/admin-page.css similarity index 99% rename from backend/src/main/resources/static/style.css rename to backend/src/main/resources/static/css/admin/admin-page.css index e99433e9b..df502b9df 100644 --- a/backend/src/main/resources/static/style.css +++ b/backend/src/main/resources/static/css/admin/admin-page.css @@ -1,4 +1,4 @@ -body { +body { font-family: Arial, sans-serif; margin: 0; padding: 0; diff --git a/backend/src/main/resources/static/css/admin/login.css b/backend/src/main/resources/static/css/admin/login.css new file mode 100644 index 000000000..e16f879e7 --- /dev/null +++ b/backend/src/main/resources/static/css/admin/login.css @@ -0,0 +1,43 @@ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + margin: 0; + padding: 0; +} + +.login-container { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 300px; + padding: 20px; + background-color: #ffffff; + box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.1); +} + +label, input { + width: 100%; + display: block; + margin: 5px 0; +} + +input[type="text"], +input[type="password"] { + width: 100%; + box-sizing: border-box; + padding: 10px; +} + +button { + padding: 10px 15px; + background-color: #007BFF; + color: #ffffff; + border: none; + cursor: pointer; + width: 100%; +} + +button:hover { + background-color: #0056b3; +} diff --git a/backend/src/main/resources/static/error/404.html b/backend/src/main/resources/static/error/404.html new file mode 100644 index 000000000..3b75ba2eb --- /dev/null +++ b/backend/src/main/resources/static/error/404.html @@ -0,0 +1,16 @@ + + + + + 404 - Not Found + + + + + + 해당 페이지가 없습니다. 😭 + URL이 올바른지 다시 확인해주세요! + + + + diff --git a/backend/src/main/resources/static/admin.js b/backend/src/main/resources/static/js/admin/admin-page.js similarity index 97% rename from backend/src/main/resources/static/admin.js rename to backend/src/main/resources/static/js/admin/admin-page.js index 37b24e264..603b9c3a9 100644 --- a/backend/src/main/resources/static/admin.js +++ b/backend/src/main/resources/static/js/admin/admin-page.js @@ -1,4 +1,4 @@ -// Function to fetch data and update dataSection +// Function to fetch data and update dataSection function fetchDataAndUpdateDataSection() { fetch("/admin/data") .then(response => { @@ -78,8 +78,7 @@ function createTable(data) { if (typeof item[key] === "object") { // If the value is an object (entryTimeAmount), format it with new lines - const formattedEntryTimeAmount = formatEntryTimeAmount(item[key]); - cell.textContent = formattedEntryTimeAmount; + cell.textContent = formatEntryTimeAmount(item[key]); cell.style.whiteSpace = "pre-line"; // Apply white-space: pre-line; style to allow line breaks } else { // If the value is not an object, display it normally diff --git a/backend/src/main/resources/static/js/admin/signup.js b/backend/src/main/resources/static/js/admin/signup.js new file mode 100644 index 000000000..ece7b9db4 --- /dev/null +++ b/backend/src/main/resources/static/js/admin/signup.js @@ -0,0 +1,38 @@ +document.getElementById("signupForm").addEventListener("submit", + function (event) { + event.preventDefault(); + const formData = new FormData(event.target); + const username = formData.get("username"); + const password = formData.get("password"); + const confirmPassword = formData.get("confirmPassword"); + if (password !== confirmPassword) { + alert("비밀번호와 확인 비밀번호가 맞지 않습니다!") + return; + } + + const signupRequest = { + username: username, + password: password, + }; + + fetch("/admin/signup", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(signupRequest) + }) + .then(response => { + if (response.ok) { + event.target.reset(); + alert("계정이 생성되었습니다."); + } else { + return response.json().then(data => { + throw new Error(data.message || "해당 계정이 없거나 비밀번호가 틀립니다."); + }); + } + }) + .catch(error => { + alert(error.message); + }); + }); diff --git a/backend/src/main/resources/static/js/login.js b/backend/src/main/resources/static/js/login.js new file mode 100644 index 000000000..ec127877b --- /dev/null +++ b/backend/src/main/resources/static/js/login.js @@ -0,0 +1,29 @@ +document.getElementById("loginForm").addEventListener("submit", + function (event) { + event.preventDefault(); + const formData = new FormData(event.target); + const loginRequest = { + username: formData.get("username"), + password: formData.get("password"), + }; + + fetch("/admin/login", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(loginRequest) + }) + .then(response => { + if (response.ok) { + return window.location.href = '/admin'; + } else { + return response.json().then(data => { + throw new Error(data.message || "해당 계정이 없거나 비밀번호가 틀립니다."); + }); + } + }) + .catch(error => { + alert(error.message); + }); + }); diff --git a/backend/src/main/resources/templates/admin.html b/backend/src/main/resources/templates/admin/admin-page.html similarity index 85% rename from backend/src/main/resources/templates/admin.html rename to backend/src/main/resources/templates/admin/admin-page.html index f1ac2e99f..59a07b5a6 100644 --- a/backend/src/main/resources/templates/admin.html +++ b/backend/src/main/resources/templates/admin/admin-page.html @@ -1,15 +1,15 @@ - + 어드민 페이지 - + 축제 생성 요청 - + 이름: @@ -28,7 +28,7 @@ 축제 생성 요청 공연 생성 요청 - + 시작 시간: @@ -47,7 +47,7 @@ 공연 생성 요청 티켓 생성 요청 - + 공연장 ID: @@ -69,6 +69,6 @@ 티켓 생성 요청 - + diff --git a/backend/src/main/resources/templates/admin/login.html b/backend/src/main/resources/templates/admin/login.html new file mode 100644 index 000000000..8f8deb745 --- /dev/null +++ b/backend/src/main/resources/templates/admin/login.html @@ -0,0 +1,26 @@ + + + + Login + + + + + 관리자 로그인 + + + Username: + + + + Password: + + + + Login + + + + + + diff --git a/backend/src/main/resources/templates/admin/signup.html b/backend/src/main/resources/templates/admin/signup.html new file mode 100644 index 000000000..ff53ee6ad --- /dev/null +++ b/backend/src/main/resources/templates/admin/signup.html @@ -0,0 +1,30 @@ + + + + Signup + + + + + 관리자 계정 생성 + + + Username: + + + + Password: + + + + Confirm Password: + + + + Signup + + + + + + diff --git a/backend/src/test/java/com/festago/application/integration/TicketServiceIntegrationTest.java b/backend/src/test/java/com/festago/application/integration/TicketServiceIntegrationTest.java index 5590c5d97..7a5fca505 100644 --- a/backend/src/test/java/com/festago/application/integration/TicketServiceIntegrationTest.java +++ b/backend/src/test/java/com/festago/application/integration/TicketServiceIntegrationTest.java @@ -2,39 +2,30 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.any; import static org.mockito.Mockito.doReturn; import com.festago.application.TicketService; import com.festago.domain.Festival; import com.festago.domain.FestivalRepository; -import com.festago.domain.Member; -import com.festago.domain.MemberRepository; -import com.festago.domain.MemberTicketRepository; import com.festago.domain.Stage; import com.festago.domain.StageRepository; +import com.festago.domain.TicketAmount; +import com.festago.domain.TicketAmountRepository; +import com.festago.domain.TicketRepository; import com.festago.domain.TicketType; import com.festago.dto.TicketCreateRequest; -import com.festago.dto.TicketingRequest; -import com.festago.exception.BadRequestException; +import com.festago.dto.TicketCreateResponse; import com.festago.exception.NotFoundException; import com.festago.support.FestivalFixture; -import com.festago.support.MemberFixture; import com.festago.support.StageFixture; +import java.time.Clock; import java.time.LocalDateTime; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.IntStream; +import java.time.ZoneOffset; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.jdbc.SqlConfig; -import org.springframework.test.context.jdbc.SqlConfig.TransactionMode; @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") @@ -44,26 +35,29 @@ class TicketServiceIntegrationTest extends ApplicationIntegrationTest { TicketService ticketService; @Autowired - MemberRepository memberRepository; + StageRepository stageRepository; - @SpyBean - MemberTicketRepository memberTicketRepository; + @Autowired + FestivalRepository festivalRepository; @Autowired - StageRepository stageRepository; + TicketRepository ticketRepository; @Autowired - FestivalRepository festivalRepository; + TicketAmountRepository ticketAmountRepository; + + @SpyBean + Clock clock; @Test void 공연이_없으면_예외() { // given - String entryTime = "2023-07-26T18:00:00"; + LocalDateTime entryTime = LocalDateTime.parse("2022-07-26T18:00:00"); long invalidStageId = 0L; int totalAmount = 100; TicketCreateRequest request = new TicketCreateRequest(invalidStageId, TicketType.VISITOR, - totalAmount, LocalDateTime.parse(entryTime)); + totalAmount, entryTime); // when && then assertThatThrownBy(() -> ticketService.create(request)) @@ -72,59 +66,59 @@ class TicketServiceIntegrationTest extends ApplicationIntegrationTest { } @Test - @Sql(scripts = "/ticketing-test-data.sql", - config = @SqlConfig(transactionMode = TransactionMode.ISOLATED)) - void 동시에_100명이_예약() { + void 공연에_티켓_추가() { // given - int tryCount = 100; - Member member = memberRepository.save(MemberFixture.member().build()); - TicketingRequest request = new TicketingRequest(1L); - ExecutorService executor = Executors.newFixedThreadPool(16); - doReturn(false) - .when(memberTicketRepository) - .existsByOwnerAndStage(any(Member.class), any(Stage.class)); + LocalDateTime stageStartTime = LocalDateTime.parse("2022-07-26T18:00:00"); + doReturn(stageStartTime.minusWeeks(1).toInstant(ZoneOffset.UTC)) + .when(clock) + .instant(); + Festival festival = festivalRepository.save(FestivalFixture.festival() + .startDate(stageStartTime.toLocalDate()) + .endDate(stageStartTime.toLocalDate()) + .build()); + Stage stage = stageRepository.save(StageFixture.stage() + .festival(festival) + .startTime(stageStartTime) + .ticketOpenTime(stageStartTime.minusDays(1)) + .build()); + TicketCreateRequest request = new TicketCreateRequest(stage.getId(), TicketType.VISITOR, + 100, stageStartTime.minusHours(1)); // when - List> futures = IntStream.range(0, tryCount) - .mapToObj(i -> CompletableFuture.runAsync(() -> { - ticketService.ticketing(member.getId(), request); - }, executor).exceptionally(e -> null)) - .toList(); - futures.forEach(CompletableFuture::join); + TicketCreateResponse response = ticketService.create(request); // then - assertThat(memberTicketRepository.count()).isEqualTo(50); + TicketAmount ticketAmount = ticketAmountRepository.findById(response.id()).get(); + assertThat(ticketAmount.getTotalAmount()).isEqualTo(100); } @Test - @Sql("/ticketing-test-data.sql") - void 중복으로_티켓을_예매하면_예외() { + void 티켓이_있는_공연에_티켓을_추가하면_기존_티켓의_수량이_증가() { // given - Member member = memberRepository.save(MemberFixture.member().build()); - TicketingRequest request = new TicketingRequest(1L); - Long memberId = member.getId(); - ticketService.ticketing(memberId, request); - - // when & then - assertThatThrownBy(() -> ticketService.ticketing(memberId, request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("예매 가능한 수량을 초과했습니다."); - } + LocalDateTime stageStartTime = LocalDateTime.parse("2022-07-26T18:00:00"); + doReturn(stageStartTime.minusWeeks(1).toInstant(ZoneOffset.UTC)) + .when(clock) + .instant(); + Festival festival = festivalRepository.save(FestivalFixture.festival() + .startDate(stageStartTime.toLocalDate()) + .endDate(stageStartTime.toLocalDate()) + .build()); + Stage stage = stageRepository.save(StageFixture.stage() + .festival(festival) + .startTime(stageStartTime) + .ticketOpenTime(stageStartTime.minusDays(1)) + .build()); + TicketCreateRequest request = new TicketCreateRequest(stage.getId(), TicketType.VISITOR, + 100, stageStartTime.minusHours(1)); + + ticketService.create(request); - @Test - void 티켓_생성시_입장_시간이_공연_시간보다_빠르면_예외() { - // given - Festival festival = festivalRepository.save(FestivalFixture.festival().build()); - LocalDateTime now = LocalDateTime.now(); - Stage stage = stageRepository.save(StageFixture.stage().festival(festival).startTime(now.plusHours(10)) - .ticketOpenTime(now.plusHours(1)).build()); - LocalDateTime entryTime = stage.getStartTime().plusHours(1); - Long stageId = stage.getId(); - TicketCreateRequest request = new TicketCreateRequest(stageId, TicketType.STUDENT, 100, entryTime); - - // when & then - assertThatThrownBy(() -> ticketService.create(request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("입장 시간은 공연 시간보다 빨라야합니다."); + // when + TicketCreateResponse response = ticketService.create(request); + + // then + assertThat(ticketRepository.count()).isEqualTo(1); + TicketAmount ticketAmount = ticketAmountRepository.findById(response.id()).get(); + assertThat(ticketAmount.getTotalAmount()).isEqualTo(200); } } diff --git a/backend/src/test/java/com/festago/application/integration/TicketingServiceIntegrationTest.java b/backend/src/test/java/com/festago/application/integration/TicketingServiceIntegrationTest.java new file mode 100644 index 000000000..00ecb925a --- /dev/null +++ b/backend/src/test/java/com/festago/application/integration/TicketingServiceIntegrationTest.java @@ -0,0 +1,91 @@ +package com.festago.application.integration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; + +import com.festago.application.TicketingService; +import com.festago.domain.Member; +import com.festago.domain.MemberRepository; +import com.festago.domain.MemberTicketRepository; +import com.festago.domain.Stage; +import com.festago.dto.TicketingRequest; +import com.festago.exception.BadRequestException; +import com.festago.support.MemberFixture; +import java.time.Clock; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.jdbc.Sql; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class TicketingServiceIntegrationTest extends ApplicationIntegrationTest { + + @Autowired + MemberRepository memberRepository; + + @Autowired + TicketingService ticketingService; + + @SpyBean + MemberTicketRepository memberTicketRepository; + + @SpyBean + Clock clock; + + @Test + @Sql("/ticketing-test-data.sql") + void 동시에_100명이_예약() { + // given + int tryCount = 100; + Member member = memberRepository.save(MemberFixture.member().build()); + TicketingRequest request = new TicketingRequest(1L); + ExecutorService executor = Executors.newFixedThreadPool(16); + doReturn(false) + .when(memberTicketRepository) + .existsByOwnerAndStage(any(Member.class), any(Stage.class)); + doReturn(Instant.parse("2023-07-24T03:21:31Z")) + .when(clock) + .instant(); + + // when + List> futures = IntStream.range(0, tryCount) + .mapToObj(i -> CompletableFuture.runAsync(() -> { + ticketingService.ticketing(member.getId(), request); + }, executor).exceptionally(e -> null)) + .toList(); + futures.forEach(CompletableFuture::join); + + // then + assertThat(memberTicketRepository.count()).isEqualTo(50); + } + + @Test + @Sql("/ticketing-test-data.sql") + void 하나의_공연에_중복으로_티켓을_예매하면_예외() { + // given + Member member = memberRepository.save(MemberFixture.member().build()); + TicketingRequest request = new TicketingRequest(1L); + Long memberId = member.getId(); + doReturn(Instant.parse("2023-07-24T03:21:31Z")) + .when(clock) + .instant(); + + ticketingService.ticketing(memberId, request); + + // when & then + assertThatThrownBy(() -> ticketingService.ticketing(memberId, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("예매 가능한 수량을 초과했습니다."); + } +} diff --git a/backend/src/test/java/com/festago/auth/application/AdminAuthServiceTest.java b/backend/src/test/java/com/festago/auth/application/AdminAuthServiceTest.java new file mode 100644 index 000000000..6bc83d7e1 --- /dev/null +++ b/backend/src/test/java/com/festago/auth/application/AdminAuthServiceTest.java @@ -0,0 +1,138 @@ +package com.festago.auth.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.anyLong; +import static org.mockito.BDDMockito.anyString; +import static org.mockito.BDDMockito.given; + +import com.festago.auth.domain.Admin; +import com.festago.auth.domain.AdminRepository; +import com.festago.auth.domain.AuthProvider; +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.ForbiddenException; +import com.festago.exception.UnauthorizedException; +import java.util.Optional; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@ExtendWith(MockitoExtension.class) +class AdminAuthServiceTest { + + @Mock + AuthProvider authProvider; + + @Mock + AdminRepository adminRepository; + + @InjectMocks + AdminAuthService adminAuthService; + + @Nested + class 로그인 { + + @Test + void 계정이_없으면_예외() { + // given + AdminLoginRequest request = new AdminLoginRequest("admin", "admin"); + given(adminRepository.findByUsername(anyString())) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> adminAuthService.login(request)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage("비밀번호가 틀렸거나, 해당 계정이 없습니다."); + } + + @Test + void 비밀번호가_틀리면_예외() { + // given + Admin admin = new Admin(1L, "admin", "admin"); + AdminLoginRequest request = new AdminLoginRequest("admin", "password"); + given(adminRepository.findByUsername(anyString())) + .willReturn(Optional.of(admin)); + + // when & then + assertThatThrownBy(() -> adminAuthService.login(request)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage("비밀번호가 틀렸거나, 해당 계정이 없습니다."); + } + + @Test + void 성공() { + // given + Admin admin = new Admin(1L, "admin", "admin"); + AdminLoginRequest request = new AdminLoginRequest("admin", "admin"); + given(adminRepository.findByUsername(anyString())) + .willReturn(Optional.of(admin)); + given(authProvider.provide(any())) + .willReturn("token"); + + // when + String token = adminAuthService.login(request); + + // then + assertThat(token).isEqualTo("token"); + } + } + + @Nested + class 가입 { + + @Test + void 닉네임이_중복이면_예외() { + // given + AdminSignupRequest request = new AdminSignupRequest("admin", "admin"); + given(adminRepository.existsByUsername(anyString())) + .willReturn(true); + given(adminRepository.findById(anyLong())) + .willReturn(Optional.of(new Admin("admin", "admin"))); + + // when & then + assertThatThrownBy(() -> adminAuthService.signup(1L, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("해당 계정이 존재합니다."); + } + + @Test + void Root_어드민이_아니면_예외() { + // given + AdminSignupRequest request = new AdminSignupRequest("newAdmin", "newAdmin"); + given(adminRepository.findById(anyLong())) + .willReturn(Optional.of(new Admin("mewAdmin", "newAdmin"))); + + // when & then + assertThatThrownBy(() -> adminAuthService.signup(1L, request)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("해당 권한이 없습니다."); + } + + @Test + void 성공() { + // given + AdminSignupRequest request = new AdminSignupRequest("newAdmin", "newAdmin"); + given(adminRepository.save(any(Admin.class))) + .willReturn(new Admin(1L, "newAdmin", "newAdmin")); + given(adminRepository.findById(anyLong())) + .willReturn(Optional.of(new Admin(1L, "admin", "admin"))); + + // when + AdminSignupResponse response = adminAuthService.signup(1L, request); + + // then + assertThat(response.username()).isEqualTo("newAdmin"); + } + } +} diff --git a/backend/src/test/java/com/festago/auth/infrastructure/CookieTokenExtractorTest.java b/backend/src/test/java/com/festago/auth/infrastructure/CookieTokenExtractorTest.java new file mode 100644 index 000000000..3cc1b482c --- /dev/null +++ b/backend/src/test/java/com/festago/auth/infrastructure/CookieTokenExtractorTest.java @@ -0,0 +1,54 @@ +package com.festago.auth.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@ExtendWith(MockitoExtension.class) +class CookieTokenExtractorTest { + + CookieTokenExtractor cookieTokenExtractor = new CookieTokenExtractor(); + + @Mock + HttpServletRequest request; + + @Test + void 요청에_쿠키가_없으면_empty() { + // given + given(request.getCookies()) + .willReturn(null); + + // when & then + assertThat(cookieTokenExtractor.extract(request)).isEmpty(); + } + + @Test + void 쿠키에_token_헤더가_없으면_empty() { + // given + given(request.getCookies()) + .willReturn(new Cookie[]{new Cookie("tokken", "token")}); + + // when + assertThat(cookieTokenExtractor.extract(request)).isEmpty(); + } + + @Test + void 쿠키에_token_헤더가_있으면_present() { + // given + given(request.getCookies()) + .willReturn(new Cookie[]{new Cookie("token", "token")}); + + // when + assertThat(cookieTokenExtractor.extract(request)).isPresent(); + } +} diff --git a/backend/src/test/java/com/festago/auth/infrastructure/HeaderTokenExtractorTest.java b/backend/src/test/java/com/festago/auth/infrastructure/HeaderTokenExtractorTest.java new file mode 100644 index 000000000..0b0804ac9 --- /dev/null +++ b/backend/src/test/java/com/festago/auth/infrastructure/HeaderTokenExtractorTest.java @@ -0,0 +1,58 @@ +package com.festago.auth.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +import com.festago.exception.UnauthorizedException; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@ExtendWith(MockitoExtension.class) +class HeaderTokenExtractorTest { + + HeaderTokenExtractor headerTokenExtractor = new HeaderTokenExtractor(); + + @Mock + HttpServletRequest request; + + @Test + void 요청에_Authorization_헤더가_없으면_empty() { + // given + given(request.getHeader(HttpHeaders.AUTHORIZATION)) + .willReturn(null); + + // when & then + assertThat(headerTokenExtractor.extract(request)).isEmpty(); + } + + @Test + void Bearer_토큰이_아니면_예외() { + // given + given(request.getHeader(HttpHeaders.AUTHORIZATION)) + .willReturn("Bear sampleToken"); + + // when & then + assertThatThrownBy(() -> headerTokenExtractor.extract(request)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage("Bearer 타입의 토큰이 아닙니다."); + } + + @Test + void Bearer_토큰이_아니면_present() { + // given + given(request.getHeader(HttpHeaders.AUTHORIZATION)) + .willReturn("Bearer sampleToken"); + + // when & then + assertThat(headerTokenExtractor.extract(request)).isPresent(); + } +} diff --git a/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthExtractorTest.java b/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthExtractorTest.java index 4bfdbad2d..d23eeac46 100644 --- a/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthExtractorTest.java +++ b/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthExtractorTest.java @@ -1,9 +1,9 @@ package com.festago.auth.infrastructure; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.festago.auth.domain.AuthPayload; +import com.festago.auth.domain.Role; import com.festago.exception.InternalServerException; import com.festago.exception.UnauthorizedException; import io.jsonwebtoken.Jwts; @@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.Date; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; @@ -21,11 +22,12 @@ class JwtAuthExtractorTest { private static final String MEMBER_ID_KEY = "memberId"; + private static final String ROLE_ID_KEY = "role"; private static final String SECRET_KEY = "1231231231231231223131231231231231231212312312"; private static final Key KEY = Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8)); JwtAuthExtractor jwtAuthExtractor = new JwtAuthExtractor(SECRET_KEY); - + @Test void JWT_토큰의_형식이_아니면_예외() { // given @@ -73,6 +75,7 @@ class JwtAuthExtractorTest { void memberId_필드가_없으면_예외() { // given String token = Jwts.builder() + .claim(ROLE_ID_KEY, Role.MEMBER) .setExpiration(new Date(new Date().getTime() + 10000)) .signWith(KEY, SignatureAlgorithm.HS256) .compact(); @@ -83,12 +86,36 @@ class JwtAuthExtractorTest { .hasMessage("유효하지 않은 로그인 토큰 payload 입니다."); } + @Test + void role_필드가_없으면_예외() { + // given + String token = Jwts.builder() + .claim(MEMBER_ID_KEY, 1) + .setExpiration(new Date(new Date().getTime() + 10000)) + .signWith(KEY, SignatureAlgorithm.HS256) + .compact(); + + // when & then + assertThatThrownBy(() -> jwtAuthExtractor.extract(token)) + .isInstanceOf(InternalServerException.class) + .hasMessage("해당하는 Role이 없습니다."); + } + + @Test + void token이_null이면_예외() { + // when & then + assertThatThrownBy(() -> jwtAuthExtractor.extract(null)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage("올바르지 않은 로그인 토큰입니다."); + } + @Test void 토큰_추출_성공() { // given Long memberId = 1L; String token = Jwts.builder() .claim(MEMBER_ID_KEY, memberId) + .claim(ROLE_ID_KEY, Role.MEMBER) .setExpiration(new Date(new Date().getTime() + 10000)) .signWith(KEY, SignatureAlgorithm.HS256) .compact(); @@ -97,6 +124,9 @@ class JwtAuthExtractorTest { AuthPayload payload = jwtAuthExtractor.extract(token); // then - assertThat(payload.getMemberId()).isEqualTo(memberId); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(payload.getMemberId()).isEqualTo(memberId); + softly.assertThat(payload.getRole()).isEqualTo(Role.MEMBER); + }); } } diff --git a/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthProviderTest.java b/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthProviderTest.java index 31994d786..aabcf2d22 100644 --- a/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthProviderTest.java +++ b/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthProviderTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.festago.auth.domain.AuthPayload; +import com.festago.auth.domain.Role; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import org.junit.jupiter.api.DisplayNameGeneration; @@ -19,7 +20,7 @@ class JwtAuthProviderTest { @Test void 토큰_생성_성공() { // given - AuthPayload authPayload = new AuthPayload(1L); + AuthPayload authPayload = new AuthPayload(1L, Role.MEMBER); JwtParser parser = Jwts.parserBuilder() .setSigningKey(SECRET_KEY.getBytes()) .build(); diff --git a/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java b/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java index 672042c45..c7edf6904 100644 --- a/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java +++ b/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java @@ -12,19 +12,16 @@ import com.festago.auth.domain.SocialType; import com.festago.auth.dto.LoginRequest; import com.festago.auth.dto.LoginResponse; -import com.festago.support.TestConfig; +import com.festago.support.CustomWebMvcTest; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -@WebMvcTest(AuthController.class) -@Import(TestConfig.class) +@CustomWebMvcTest(AuthController.class) @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class AuthControllerTest { diff --git a/backend/src/test/java/com/festago/auth/presentation/LoginMemberResolverTest.java b/backend/src/test/java/com/festago/auth/presentation/LoginMemberResolverTest.java deleted file mode 100644 index 7e4f4abff..000000000 --- a/backend/src/test/java/com/festago/auth/presentation/LoginMemberResolverTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.festago.auth.presentation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; - -import com.festago.auth.domain.AuthExtractor; -import com.festago.auth.domain.AuthPayload; -import com.festago.auth.dto.LoginMember; -import com.festago.exception.UnauthorizedException; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpHeaders; -import org.springframework.web.context.request.NativeWebRequest; - -@DisplayNameGeneration(ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -@ExtendWith(MockitoExtension.class) -class LoginMemberResolverTest { - - @InjectMocks - LoginMemberResolver loginMemberResolver; - - @Mock - AuthExtractor authExtractor; - - @Mock - NativeWebRequest request; - - @Test - void 토큰이_null이면_예외() { - // given - given(request.getHeader(HttpHeaders.AUTHORIZATION)) - .willReturn(null); - - // when & then - assertThatThrownBy(() -> loginMemberResolver.resolveArgument(null, null, request, null)) - .isInstanceOf(UnauthorizedException.class) - .hasMessage("로그인이 필요한 서비스입니다."); - } - - @Test - void Bearer토큰이_아니면_예외() { - // given - given(request.getHeader(HttpHeaders.AUTHORIZATION)) - .willReturn("sampleToken"); - - // when & then - assertThatThrownBy(() -> loginMemberResolver.resolveArgument(null, null, request, null)) - .isInstanceOf(UnauthorizedException.class) - .hasMessage("Bearer 타입의 토큰이 아닙니다."); - } - - @Test - void 성공() { - // given - String token = "sampleToken"; - Long memberId = 1L; - - given(request.getHeader(HttpHeaders.AUTHORIZATION)) - .willReturn("Bearer " + token); - given(authExtractor.extract(token)) - .willReturn(new AuthPayload(memberId)); - - // when - LoginMember expect = loginMemberResolver.resolveArgument(null, null, request, null); - - // then - assertThat(expect.memberId()).isEqualTo(memberId); - } -} diff --git a/backend/src/test/java/com/festago/auth/presentation/RoleArgumentResolverTest.java b/backend/src/test/java/com/festago/auth/presentation/RoleArgumentResolverTest.java new file mode 100644 index 000000000..ca04824c7 --- /dev/null +++ b/backend/src/test/java/com/festago/auth/presentation/RoleArgumentResolverTest.java @@ -0,0 +1,79 @@ +package com.festago.auth.presentation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.festago.auth.domain.Role; +import com.festago.exception.ForbiddenException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class RoleArgumentResolverTest { + + AuthenticateContext authenticateContext; + + RoleArgumentResolver roleArgumentResolver; + + @BeforeEach + void setUp() { + authenticateContext = new AuthenticateContext(); + } + + @ParameterizedTest + @ValueSource(strings = {"ADMIN", "ANONYMOUS"}) + void Role이_Member일때_Member가_아니면_예외(Role role) { + // given + roleArgumentResolver = new RoleArgumentResolver(Role.MEMBER, authenticateContext); + authenticateContext.setAuthenticate(1L, role); + + // when & then + assertThatThrownBy(() -> roleArgumentResolver.resolveArgument(null, null, null, null)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("해당 권한이 없습니다."); + } + + @Test + void Role이_Member일때_Member이면_성공() throws Exception { + // given + roleArgumentResolver = new RoleArgumentResolver(Role.MEMBER, authenticateContext); + authenticateContext.setAuthenticate(1L, Role.MEMBER); + + // when + Long memberId = roleArgumentResolver.resolveArgument(null, null, null, null); + + // then + assertThat(memberId).isEqualTo(authenticateContext.getId()); + } + + @ParameterizedTest + @ValueSource(strings = {"MEMBER", "ANONYMOUS"}) + void Role이_Admin일때_Admin이_아니면_예외(Role role) { + // given + roleArgumentResolver = new RoleArgumentResolver(Role.ADMIN, authenticateContext); + authenticateContext.setAuthenticate(1L, role); + + // when & then + assertThatThrownBy(() -> roleArgumentResolver.resolveArgument(null, null, null, null)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("해당 권한이 없습니다."); + } + + @Test + void Role이_Admin일때_Admin이면_성공() throws Exception { + // given + roleArgumentResolver = new RoleArgumentResolver(Role.ADMIN, authenticateContext); + authenticateContext.setAuthenticate(1L, Role.ADMIN); + + // when + Long memberId = roleArgumentResolver.resolveArgument(null, null, null, null); + + // then + assertThat(memberId).isEqualTo(authenticateContext.getId()); + } +} diff --git a/backend/src/test/java/com/festago/domain/TicketTest.java b/backend/src/test/java/com/festago/domain/TicketTest.java index fd8aaa434..476c02d8c 100644 --- a/backend/src/test/java/com/festago/domain/TicketTest.java +++ b/backend/src/test/java/com/festago/domain/TicketTest.java @@ -5,6 +5,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.festago.exception.BadRequestException; +import com.festago.support.FestivalFixture; import com.festago.support.MemberFixture; import com.festago.support.StageFixture; import com.festago.support.TicketFixture; @@ -20,7 +21,6 @@ @SuppressWarnings("NonAsciiCharacters") class TicketTest { - @Nested class 입장시간_추가_검증 { @@ -125,37 +125,86 @@ class 예매_티켓_생성 { @Test void 최대_수량보다_많으면_예외() { // given - Member member = MemberFixture.member() + LocalDateTime stageStartTime = LocalDateTime.parse("2022-08-12T18:00:00"); + LocalDateTime now = stageStartTime.minusHours(6); + Festival festival = FestivalFixture.festival() + .startDate(stageStartTime.toLocalDate()) + .endDate(stageStartTime.toLocalDate()) + .build(); + Stage stage = StageFixture.stage() + .startTime(stageStartTime) + .ticketOpenTime(stageStartTime.minusDays(1)) + .festival(festival) .build(); Ticket ticket = TicketFixture.ticket() + .stage(stage) + .build(); + Member member = MemberFixture.member() + .id(1L) .build(); - LocalDateTime now = LocalDateTime.now(); - LocalDateTime ticketOpenTime = ticket.getStage().getTicketOpenTime(); - ticket.addTicketEntryTime(ticketOpenTime.minusMinutes(10), now.minusHours(1), 50); - ticket.addTicketEntryTime(ticketOpenTime.minusMinutes(10), now.minusHours(2), 30); - ticket.addTicketEntryTime(ticketOpenTime.minusMinutes(10), now.minusHours(3), 20); // when & then - assertThatThrownBy(() -> ticket.createMemberTicket(member, 101)) + assertThatThrownBy(() -> ticket.createMemberTicket(member, 101, now)) .isInstanceOf(BadRequestException.class) .hasMessage("매진된 티켓입니다."); } + @Test + void 공연의_시간이_지나고_예매하면_예외() { + LocalDateTime stageStartTime = LocalDateTime.parse("2022-08-12T18:00:00"); + LocalDateTime now = stageStartTime.plusHours(1); + Festival festival = FestivalFixture.festival() + .startDate(stageStartTime.toLocalDate()) + .endDate(stageStartTime.toLocalDate()) + .build(); + Stage stage = StageFixture.stage() + .startTime(stageStartTime) + .ticketOpenTime(stageStartTime.minusDays(1)) + .festival(festival) + .build(); + Ticket ticket = TicketFixture.ticket() + .stage(stage) + .build(); + Member member = MemberFixture.member() + .id(1L) + .build(); + + ticket.addTicketEntryTime(LocalDateTime.MIN, stageStartTime.minusHours(1), 100); + + // when & then + assertThatThrownBy(() -> ticket.createMemberTicket(member, 1, now)) + .isInstanceOf(BadRequestException.class) + .hasMessage("공연의 시작 시간 이후로 예매할 수 없습니다."); + } + @ParameterizedTest @ValueSource(ints = {0, 100}) void 성공(int reservationSequence) { // given - Member member = MemberFixture.member() + LocalDateTime stageStartTime = LocalDateTime.parse("2022-08-12T18:00:00"); + LocalDateTime now = stageStartTime.minusHours(6); + Festival festival = FestivalFixture.festival() + .startDate(stageStartTime.toLocalDate()) + .endDate(stageStartTime.toLocalDate()) + .build(); + Stage stage = StageFixture.stage() + .startTime(stageStartTime) + .ticketOpenTime(stageStartTime.minusDays(1)) + .festival(festival) .build(); Ticket ticket = TicketFixture.ticket() + .stage(stage) .build(); - LocalDateTime ticketOpenTime = ticket.getStage().getTicketOpenTime(); - ticket.addTicketEntryTime(ticketOpenTime.minusMinutes(10), LocalDateTime.now().minusHours(1), 50); - ticket.addTicketEntryTime(ticketOpenTime.minusMinutes(10), LocalDateTime.now().minusHours(2), 30); - ticket.addTicketEntryTime(ticketOpenTime.minusMinutes(10), LocalDateTime.now().minusHours(3), 20); + Member member = MemberFixture.member() + .id(1L) + .build(); + + ticket.addTicketEntryTime(LocalDateTime.MIN, stageStartTime.minusHours(1), 50); + ticket.addTicketEntryTime(LocalDateTime.MIN, stageStartTime.minusHours(2), 30); + ticket.addTicketEntryTime(LocalDateTime.MIN, stageStartTime.minusHours(3), 20); // when - MemberTicket memberTicket = ticket.createMemberTicket(member, reservationSequence); + MemberTicket memberTicket = ticket.createMemberTicket(member, reservationSequence, now); // then assertThat(memberTicket.getOwner()).isEqualTo(member); diff --git a/backend/src/test/java/com/festago/presentation/AdminControllerTest.java b/backend/src/test/java/com/festago/presentation/AdminControllerTest.java index 09bb47913..17137726d 100644 --- a/backend/src/test/java/com/festago/presentation/AdminControllerTest.java +++ b/backend/src/test/java/com/festago/presentation/AdminControllerTest.java @@ -2,7 +2,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.anyString; import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -12,6 +14,9 @@ import com.festago.application.FestivalService; import com.festago.application.StageService; import com.festago.application.TicketService; +import com.festago.auth.application.AdminAuthService; +import com.festago.auth.domain.AuthExtractor; +import com.festago.auth.domain.Role; import com.festago.domain.TicketType; import com.festago.dto.ErrorResponse; import com.festago.dto.FestivalCreateRequest; @@ -22,7 +27,10 @@ import com.festago.dto.TicketCreateResponse; import com.festago.exception.ErrorCode; import com.festago.exception.NotFoundException; -import com.festago.support.TestConfig; +import com.festago.exception.UnauthorizedException; +import com.festago.support.CustomWebMvcTest; +import com.festago.support.WithMockAuth; +import jakarta.servlet.http.Cookie; import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.LocalDateTime; @@ -30,14 +38,12 @@ import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -@WebMvcTest(AdminController.class) -@Import(TestConfig.class) +@CustomWebMvcTest(AdminController.class) @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class AdminControllerTest { @@ -60,7 +66,50 @@ class AdminControllerTest { @MockBean AdminService adminService; + @MockBean + AdminAuthService adminAuthService; + + @SpyBean + AuthExtractor authExtractor; + + @Test + @WithMockAuth + void 토큰의_Role이_어드민이_아니면_404_NotFound() throws Exception { + // when & then + mockMvc.perform(get("/admin") + .cookie(new Cookie("token", "token"))) + .andExpect(status().isNotFound()); + } + + @Test + void 쿠키에_토큰이_없으면_404_NotFound() throws Exception { + // when & then + mockMvc.perform(get("/admin")) + .andExpect(status().isNotFound()); + } + + @Test + void 권한이_없어도_로그인_페이지_접속_가능() throws Exception { + // when & then + mockMvc.perform(get("/admin/login")) + .andExpect(status().isOk()); + } + + @Test + @WithMockAuth + void 토큰의_만료기간이_지나면_로그인_페이지로_리다이렉트() throws Exception { + // given + given(authExtractor.extract(anyString())) + .willThrow(new UnauthorizedException(ErrorCode.EXPIRED_AUTH_TOKEN)); + + // when & then + mockMvc.perform(get("/admin/login") + .cookie(new Cookie("token", "token"))) + .andExpect(status().isOk()); + } + @Test + @WithMockAuth(role = Role.ADMIN) void 축제_생성() throws Exception { // given String festivalName = "테코 대학교"; @@ -87,7 +136,8 @@ class AdminControllerTest { // when && then String content = mockMvc.perform(post("/admin/festivals") .content(objectMapper.writeValueAsString(request)) - .contentType(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new Cookie("token", "token"))) .andDo(print()) .andExpect(status().isOk()) .andReturn() @@ -98,6 +148,7 @@ class AdminControllerTest { } @Test + @WithMockAuth(role = Role.ADMIN) void 존재_하지_않는_축제_무대_생성_예외() throws Exception { // given String startTime = "2023-07-27T18:00:00"; @@ -120,7 +171,8 @@ class AdminControllerTest { // when && then String content = mockMvc.perform(post("/admin/stages") .content(objectMapper.writeValueAsString(request)) - .contentType(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new Cookie("token", "token"))) .andDo(print()) .andExpect(status().isNotFound()) .andReturn() @@ -131,6 +183,7 @@ class AdminControllerTest { } @Test + @WithMockAuth(role = Role.ADMIN) void 무대_생성() throws Exception { // given String startTime = "2023-07-27T18:00:00"; @@ -152,7 +205,8 @@ class AdminControllerTest { // when && then String content = mockMvc.perform(post("/admin/stages") .content(objectMapper.writeValueAsString(request)) - .contentType(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new Cookie("token", "token"))) .andDo(print()) .andExpect(status().isOk()) .andReturn() @@ -163,6 +217,7 @@ class AdminControllerTest { } @Test + @WithMockAuth(role = Role.ADMIN) void 존재_하지_않는_무대_티켓_예외() throws Exception { // given String entryTime = "2023-07-27T18:00:00"; @@ -183,7 +238,8 @@ class AdminControllerTest { // when && then String content = mockMvc.perform(post("/admin/tickets") .content(objectMapper.writeValueAsString(request)) - .contentType(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new Cookie("token", "token"))) .andDo(print()) .andExpect(status().isNotFound()) .andReturn() @@ -194,6 +250,7 @@ class AdminControllerTest { } @Test + @WithMockAuth(role = Role.ADMIN) void 티켓_생성() throws Exception { // given long ticketId = 1L; @@ -216,7 +273,8 @@ class AdminControllerTest { // when && then String content = mockMvc.perform(post("/admin/tickets") .content(objectMapper.writeValueAsString(request)) - .contentType(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new Cookie("token", "token"))) .andDo(print()) .andExpect(status().isOk()) .andReturn() diff --git a/backend/src/test/java/com/festago/presentation/FestivalControllerTest.java b/backend/src/test/java/com/festago/presentation/FestivalControllerTest.java index 73ed3a35f..4d8a3bc4a 100644 --- a/backend/src/test/java/com/festago/presentation/FestivalControllerTest.java +++ b/backend/src/test/java/com/festago/presentation/FestivalControllerTest.java @@ -12,7 +12,7 @@ import com.festago.dto.FestivalDetailResponse; import com.festago.dto.FestivalResponse; import com.festago.dto.FestivalsResponse; -import com.festago.support.TestConfig; +import com.festago.support.CustomWebMvcTest; import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.util.Collections; @@ -21,14 +21,11 @@ import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -@WebMvcTest(FestivalController.class) -@Import(TestConfig.class) +@CustomWebMvcTest(FestivalController.class) @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class FestivalControllerTest { diff --git a/backend/src/test/java/com/festago/presentation/MemberControllerTest.java b/backend/src/test/java/com/festago/presentation/MemberControllerTest.java index 4944ecfb5..b6c8d5ad4 100644 --- a/backend/src/test/java/com/festago/presentation/MemberControllerTest.java +++ b/backend/src/test/java/com/festago/presentation/MemberControllerTest.java @@ -1,7 +1,6 @@ package com.festago.presentation; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -11,22 +10,18 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.festago.application.MemberService; import com.festago.application.MemberTicketService; -import com.festago.auth.domain.AuthExtractor; -import com.festago.auth.domain.AuthPayload; import com.festago.dto.MemberProfileResponse; -import com.festago.support.TestConfig; +import com.festago.support.CustomWebMvcTest; +import com.festago.support.WithMockAuth; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; -@WebMvcTest(MemberController.class) -@Import(TestConfig.class) +@CustomWebMvcTest(MemberController.class) @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class MemberControllerTest { @@ -43,18 +38,14 @@ class MemberControllerTest { @MockBean MemberTicketService memberTicketService; - @MockBean - AuthExtractor authExtractor; - @Test + @WithMockAuth void 회원_정보_반환() throws Exception { // given String token = "sampleToken"; MemberProfileResponse expected = new MemberProfileResponse(1L, "닉네임", "www.profileImageUrl.com"); given(memberService.findMemberProfile(anyLong())) .willReturn(expected); - given(authExtractor.extract(any())) - .willReturn(new AuthPayload(1L)); // when & then String content = mockMvc.perform(get("/members/profile") diff --git a/backend/src/test/java/com/festago/presentation/MemberTicketControllerTest.java b/backend/src/test/java/com/festago/presentation/MemberTicketControllerTest.java index 4fa7baf69..c0a933f49 100644 --- a/backend/src/test/java/com/festago/presentation/MemberTicketControllerTest.java +++ b/backend/src/test/java/com/festago/presentation/MemberTicketControllerTest.java @@ -15,9 +15,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.festago.application.EntryService; import com.festago.application.MemberTicketService; -import com.festago.application.TicketService; -import com.festago.auth.domain.AuthExtractor; -import com.festago.auth.domain.AuthPayload; +import com.festago.application.TicketingService; import com.festago.domain.EntryState; import com.festago.dto.EntryCodeResponse; import com.festago.dto.MemberTicketFestivalResponse; @@ -26,7 +24,8 @@ import com.festago.dto.StageResponse; import com.festago.dto.TicketingRequest; import com.festago.dto.TicketingResponse; -import com.festago.support.TestConfig; +import com.festago.support.CustomWebMvcTest; +import com.festago.support.WithMockAuth; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.stream.LongStream; @@ -34,15 +33,12 @@ import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -@WebMvcTest(MemberTicketController.class) -@Import(TestConfig.class) +@CustomWebMvcTest(MemberTicketController.class) @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class MemberTicketControllerTest { @@ -60,12 +56,10 @@ class MemberTicketControllerTest { MemberTicketService memberTicketService; @MockBean - TicketService ticketService; - - @MockBean - AuthExtractor authExtractor; + TicketingService ticketingService; @Test + @WithMockAuth void QR을_생성한다() throws Exception { // given Long memberTicketId = 1L; @@ -77,8 +71,6 @@ class MemberTicketControllerTest { given(entryService.createEntryCode(anyLong(), anyLong())) .willReturn(expected); - given(authExtractor.extract(any())) - .willReturn(new AuthPayload(1L)); // when & then String content = mockMvc.perform(post("/member-tickets/{memberTicketId}/qr", memberTicketId) @@ -93,6 +85,7 @@ class MemberTicketControllerTest { } @Test + @WithMockAuth void 단일_티켓을_조회한다() throws Exception { // given Long memberTicketId = 1L; @@ -107,8 +100,6 @@ class MemberTicketControllerTest { given(memberTicketService.findById(memberId, memberTicketId)) .willReturn(expected); - given(authExtractor.extract(any())) - .willReturn(new AuthPayload(1L)); // when & then String content = mockMvc.perform(get("/member-tickets/{memberTicketId}", memberTicketId) @@ -124,6 +115,7 @@ class MemberTicketControllerTest { } @Test + @WithMockAuth void 회원의_모든_티켓을_조회한다() throws Exception { // given Long memberId = 1L; @@ -140,8 +132,6 @@ class MemberTicketControllerTest { given(memberTicketService.findAll(eq(memberId), any(Pageable.class))) .willReturn(expected); - given(authExtractor.extract(any())) - .willReturn(new AuthPayload(1L)); // when & then String content = mockMvc.perform(get("/member-tickets") @@ -157,6 +147,7 @@ class MemberTicketControllerTest { } @Test + @WithMockAuth void 현재_티켓_리스트를_조회한다() throws Exception { // given Long memberId = 1L; @@ -173,8 +164,6 @@ class MemberTicketControllerTest { given(memberTicketService.findCurrent(eq(memberId), any(Pageable.class))) .willReturn(expected); - given(authExtractor.extract(any())) - .willReturn(new AuthPayload(1L)); // when & then String content = mockMvc.perform(get("/member-tickets/current") @@ -190,6 +179,7 @@ class MemberTicketControllerTest { } @Test + @WithMockAuth void 티켓팅을_통해_멤버의_티켓을_생성한다() throws Exception { // given Long memberTicketId = 1L; @@ -201,10 +191,8 @@ class MemberTicketControllerTest { TicketingResponse expected = new TicketingResponse(memberTicketId, ticketNumber, ticketEntryTime); TicketingRequest request = new TicketingRequest(ticketId); - given(ticketService.ticketing(anyLong(), any())) + given(ticketingService.ticketing(anyLong(), any())) .willReturn(expected); - given(authExtractor.extract(any())) - .willReturn(new AuthPayload(1L)); // when & then String content = mockMvc.perform(post("/member-tickets") diff --git a/backend/src/test/java/com/festago/presentation/StaffMemberTicketControllerTest.java b/backend/src/test/java/com/festago/presentation/StaffMemberTicketControllerTest.java index cf0f25e0c..b36df0ebe 100644 --- a/backend/src/test/java/com/festago/presentation/StaffMemberTicketControllerTest.java +++ b/backend/src/test/java/com/festago/presentation/StaffMemberTicketControllerTest.java @@ -1,7 +1,6 @@ package com.festago.presentation; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @@ -12,21 +11,18 @@ import com.festago.domain.EntryState; import com.festago.dto.TicketValidationRequest; import com.festago.dto.TicketValidationResponse; -import com.festago.support.TestConfig; +import com.festago.support.CustomWebMvcTest; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -@WebMvcTest(StaffMemberTicketController.class) -@Import(TestConfig.class) +@CustomWebMvcTest(StaffMemberTicketController.class) @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class StaffMemberTicketControllerTest { @@ -45,7 +41,7 @@ class StaffMemberTicketControllerTest { // given TicketValidationRequest request = new TicketValidationRequest("anyCode"); TicketValidationResponse expected = new TicketValidationResponse(EntryState.AFTER_ENTRY); - given(entryService.validate(eq(request))) + given(entryService.validate(request)) .willReturn(expected); // when & then diff --git a/backend/src/test/java/com/festago/presentation/StageControllerTest.java b/backend/src/test/java/com/festago/presentation/StageControllerTest.java index 11f7d236a..90499dc8e 100644 --- a/backend/src/test/java/com/festago/presentation/StageControllerTest.java +++ b/backend/src/test/java/com/festago/presentation/StageControllerTest.java @@ -12,21 +12,18 @@ import com.festago.domain.TicketType; import com.festago.dto.StageTicketResponse; import com.festago.dto.StageTicketsResponse; -import com.festago.support.TestConfig; +import com.festago.support.CustomWebMvcTest; import java.nio.charset.StandardCharsets; import java.util.List; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -@WebMvcTest(StageController.class) -@Import(TestConfig.class) +@CustomWebMvcTest(StageController.class) @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class StageControllerTest { diff --git a/backend/src/test/java/com/festago/support/CustomWebMvcTest.java b/backend/src/test/java/com/festago/support/CustomWebMvcTest.java new file mode 100644 index 000000000..812ad7f2b --- /dev/null +++ b/backend/src/test/java/com/festago/support/CustomWebMvcTest.java @@ -0,0 +1,22 @@ +package com.festago.support; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.TestExecutionListeners.MergeMode; + +@WebMvcTest +@Import(TestAuthConfig.class) +@Retention(RetentionPolicy.RUNTIME) +@TestExecutionListeners(value = MockAuthTestExecutionListener.class, mergeMode = MergeMode.MERGE_WITH_DEFAULTS) +public @interface CustomWebMvcTest { + + @AliasFor("controllers") + Class>[] value() default {}; + + @AliasFor("value") + Class>[] controllers() default {}; +} diff --git a/backend/src/test/java/com/festago/support/MockAuthExtractor.java b/backend/src/test/java/com/festago/support/MockAuthExtractor.java new file mode 100644 index 000000000..43c432dd8 --- /dev/null +++ b/backend/src/test/java/com/festago/support/MockAuthExtractor.java @@ -0,0 +1,19 @@ +package com.festago.support; + +import com.festago.auth.domain.AuthExtractor; +import com.festago.auth.domain.AuthPayload; +import com.festago.auth.presentation.AuthenticateContext; + +public class MockAuthExtractor implements AuthExtractor { + + private final AuthenticateContext authenticateContext; + + public MockAuthExtractor(AuthenticateContext authenticateContext) { + this.authenticateContext = authenticateContext; + } + + @Override + public AuthPayload extract(String token) { + return new AuthPayload(authenticateContext.getId(), authenticateContext.getRole()); + } +} diff --git a/backend/src/test/java/com/festago/support/MockAuthTestExecutionListener.java b/backend/src/test/java/com/festago/support/MockAuthTestExecutionListener.java new file mode 100644 index 000000000..8f7a9e9e0 --- /dev/null +++ b/backend/src/test/java/com/festago/support/MockAuthTestExecutionListener.java @@ -0,0 +1,29 @@ +package com.festago.support; + +import com.festago.auth.domain.Role; +import com.festago.auth.presentation.AuthenticateContext; +import java.lang.reflect.Method; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +public class MockAuthTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + Method testMethod = testContext.getTestMethod(); + if (testMethod.isAnnotationPresent(WithMockAuth.class)) { + WithMockAuth withMockAuth = testMethod.getDeclaredAnnotation(WithMockAuth.class); + ApplicationContext applicationContext = testContext.getApplicationContext(); + AuthenticateContext authenticateContext = applicationContext.getBean(AuthenticateContext.class); + authenticateContext.setAuthenticate(withMockAuth.id(), withMockAuth.role()); + } + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + ApplicationContext applicationContext = testContext.getApplicationContext(); + AuthenticateContext authenticateContext = applicationContext.getBean(AuthenticateContext.class); + authenticateContext.setAuthenticate(null, Role.ANONYMOUS); + } +} diff --git a/backend/src/test/java/com/festago/support/TestAuthConfig.java b/backend/src/test/java/com/festago/support/TestAuthConfig.java new file mode 100644 index 000000000..f716d7a9c --- /dev/null +++ b/backend/src/test/java/com/festago/support/TestAuthConfig.java @@ -0,0 +1,20 @@ +package com.festago.support; + +import com.festago.auth.domain.AuthExtractor; +import com.festago.auth.presentation.AuthenticateContext; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestAuthConfig { + + @Bean + public AuthenticateContext authenticateContext() { + return new AuthenticateContext(); + } + + @Bean + public AuthExtractor authExtractor(AuthenticateContext authenticateContext) { + return new MockAuthExtractor(authenticateContext); + } +} diff --git a/backend/src/test/java/com/festago/support/TestConfig.java b/backend/src/test/java/com/festago/support/TestConfig.java deleted file mode 100644 index f77e364f2..000000000 --- a/backend/src/test/java/com/festago/support/TestConfig.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.festago.support; - -import com.festago.auth.domain.AuthExtractor; -import com.festago.auth.infrastructure.JwtAuthExtractor; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; - -@TestConfiguration -public class TestConfig { - - private static final String SECRET_KEY = "1231231231231231223131231231231231231212312312"; - - @Bean - public AuthExtractor testAuthExtractor() { - return new JwtAuthExtractor(SECRET_KEY); - } -} diff --git a/backend/src/test/java/com/festago/support/WithMockAuth.java b/backend/src/test/java/com/festago/support/WithMockAuth.java new file mode 100644 index 000000000..3f0b35f5f --- /dev/null +++ b/backend/src/test/java/com/festago/support/WithMockAuth.java @@ -0,0 +1,16 @@ +package com.festago.support; + +import com.festago.auth.domain.Role; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface WithMockAuth { + + long id() default 1; + + Role role() default Role.MEMBER; +}
URL이 올바른지 다시 확인해주세요!