diff --git a/README.md b/README.md index 4bd7a0b20..856c9024b 100644 --- a/README.md +++ b/README.md @@ -1 +1,73 @@ -# 2023-festa-go +# 페스타고, 대학 축제를 더욱 즐겁게! +> 대학 축제 줄서기 및 축제 정보 제공 서비스 "페스타고" + +![](https://github.com/woowacourse-teams/2023-festa-go/assets/71129059/55f0d73b-c032-4c15-9cdf-40a560af948f) + +다들 즐거워 하는 대학 축제이지만, 한 가지 걱정되는 점이 있습니다. 바로 ‘줄 서기’입니다. 티켓팅과 예매를 위한 지루한 줄서기 과정은 축제의 재미를 반감시키는 요인입니다. + +페스타고는 이러한 문제를 해결하여, 우리 모두가 대학 축제를 더 편리하게 즐기기 위해 만들어졌습니다. + +페스타고를 통해 티켓을 예매하기 위해 불편한 줄 서기 과정을 거칠 필요 없이 온라인으로 티켓을 예매하고, 복잡한 절차 없이 스마트폰의 QR 코드만으로 입장을 할 수 있습니다. + +image + +
+
+ +**▷ 📲 다운로드 |** [PlayStore](https://play.google.com/store/apps/details?id=com.festago.festago) + +**▷ 📝 팀블로그 |** [Festago 팀블로그](https://festago.github.io/) + +**▷ 📧 연락처 |** team.festago@gmail.com + +## Android +
+

프로젝트 아키텍처

+ +
+ +
+

기술 스택

+ +
+ +## Backend +
+

백엔드 인프라 아키텍처

+ +
+ + +
+

기술 스택

+ + + + + + + +
+ + + + + + + +
+
+ +## 🎉 축제 스태프를 소개합니다 + +|BackEnd|BackEnd|BackEnd|BackEnd|Android|Android|Android| +|:-:|:-:|:-:|:-:|:-:|:-:|:-:| +|![](https://avatars.githubusercontent.com/u/103228463?v=4&size=100)|![](https://avatars.githubusercontent.com/u/116627736?v=4&size=100)|![](https://avatars.githubusercontent.com/u/71129059?v=4&size=100)|![](https://avatars.githubusercontent.com/u/100915276?v=4&size=100)|![](https://avatars.githubusercontent.com/u/108349655?v=4&size=100)|![](https://avatars.githubusercontent.com/u/67777523?v=4&size=100)|![](https://avatars.githubusercontent.com/u/37167652?v=4&size=100)| +|[푸우](https://github.com/BGuga)|[글렌](https://github.com/seokjin8678)|[애쉬](https://github.com/xxeol2)|[오리](https://github.com/carsago)|[베르](https://github.com/SeongHoonC)|[해시](https://github.com/EmilyCh0)|[아크](https://github.com/re4rk)| + +
+ +## ⛔️ 공연 관람시 주의사항 +> 페스타고 팀의 그라운드 룰을 소개합니다. + +image diff --git a/android/festago/app/build.gradle.kts b/android/festago/app/build.gradle.kts index c92b8aae2..2c171794c 100644 --- a/android/festago/app/build.gradle.kts +++ b/android/festago/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "com.festago.festago" minSdk = 28 targetSdk = 34 - versionCode = 3 - versionName = "1.0.1" + versionCode = 7 + versionName = "1.2.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 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 12326dd03..d9d999223 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 @@ -4,6 +4,7 @@ import com.festago.festago.data.service.FestivalRetrofitService import com.festago.festago.data.util.onSuccessOrCatch import com.festago.festago.data.util.runCatchingResponse import com.festago.festago.model.Festival +import com.festago.festago.model.FestivalFilter import com.festago.festago.model.Reservation import com.festago.festago.repository.FestivalRepository import javax.inject.Inject @@ -11,9 +12,10 @@ import javax.inject.Inject class FestivalDefaultRepository @Inject constructor( private val festivalRetrofitService: FestivalRetrofitService, ) : FestivalRepository { - override suspend fun loadFestivals(): Result> = - runCatchingResponse { festivalRetrofitService.getFestivals() } - .onSuccessOrCatch { it.toDomain() } + override suspend fun loadFestivals(festivalFilter: FestivalFilter): Result> = + runCatchingResponse { + festivalRetrofitService.getFestivals(festivalFilter.name) + }.onSuccessOrCatch { it.toDomain() } override suspend fun loadFestivalDetail(festivalId: Long): Result = runCatchingResponse { festivalRetrofitService.getFestivalDetail(festivalId) } 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 33ac652e5..43d9197aa 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 @@ -4,6 +4,7 @@ import com.festago.festago.data.dto.ReservedTicketRequest import com.festago.festago.data.service.TicketRetrofitService import com.festago.festago.data.util.onSuccessOrCatch import com.festago.festago.data.util.runCatchingResponse +import com.festago.festago.model.ErrorCode import com.festago.festago.model.ReservedTicket import com.festago.festago.model.Ticket import com.festago.festago.model.TicketCode @@ -31,9 +32,16 @@ class TicketDefaultRepository @Inject constructor( .onSuccessOrCatch { it.toDomain() } override suspend fun reserveTicket(ticketId: Int): Result = - runCatchingResponse { - ticketRetrofitService.postReserveTicket( - ReservedTicketRequest(ticketId), - ) - }.onSuccessOrCatch { it.toDomain() } + runCatchingResponse { ticketRetrofitService.postReserveTicket(ReservedTicketRequest(ticketId)) } + .onSuccessOrCatch { it.toDomain() } + .onFailure { throwable -> + val message = throwable.message ?: "ERROR_UNKNOWN" + val error: Throwable = when { + "NEED_STUDENT_VERIFICATION" in message -> ErrorCode.NEED_STUDENT_VERIFICATION() + "RESERVE_TICKET_OVER_AMOUNT" in message -> ErrorCode.RESERVE_TICKET_OVER_AMOUNT() + "TICKET_SOLD_OUT" in message -> ErrorCode.TICKET_SOLD_OUT() + else -> throwable + } + return Result.failure(error) + } } diff --git a/android/festago/app/src/main/java/com/festago/festago/data/service/FestivalRetrofitService.kt b/android/festago/app/src/main/java/com/festago/festago/data/service/FestivalRetrofitService.kt index 0626fb95c..551442889 100644 --- a/android/festago/app/src/main/java/com/festago/festago/data/service/FestivalRetrofitService.kt +++ b/android/festago/app/src/main/java/com/festago/festago/data/service/FestivalRetrofitService.kt @@ -5,10 +5,13 @@ import com.festago.festago.data.dto.ReservationFestivalResponse import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path +import retrofit2.http.Query interface FestivalRetrofitService { @GET("/festivals") - suspend fun getFestivals(): Response + suspend fun getFestivals( + @Query("festivalFilter") festivalFilter: String, + ): Response @GET("/festivals/{festivalId}") suspend fun getFestivalDetail( 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 9b7fe0a8a..d4932ed32 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 @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Toast +import androidx.activity.addCallback import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels @@ -37,6 +38,7 @@ class HomeActivity : AppCompatActivity() { initView() initObserve() initResultLauncher() + initBackPressedDispatcher() } private fun initResultLauncher() { @@ -129,6 +131,22 @@ class HomeActivity : AppCompatActivity() { resultLauncher.launch(SignInActivity.getIntent(this)) } + private fun initBackPressedDispatcher() { + var backPressedTime = START_BACK_PRESSED_TIME + onBackPressedDispatcher.addCallback { + if ((System.currentTimeMillis() - backPressedTime) > FINISH_BACK_PRESSED_TIME) { + backPressedTime = System.currentTimeMillis() + Toast.makeText( + this@HomeActivity, + getString(R.string.home_back_pressed), + Toast.LENGTH_SHORT, + ).show() + } else { + finish() + } + } + } + private inline fun changeFragment() { val tag = T::class.java.name val fragmentTransaction = supportFragmentManager.beginTransaction() @@ -150,6 +168,8 @@ class HomeActivity : AppCompatActivity() { } companion object { + private const val START_BACK_PRESSED_TIME = 0L + private const val FINISH_BACK_PRESSED_TIME = 3000L fun getIntent(context: Context): Intent { return Intent(context, HomeActivity::class.java) } 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 04e701e44..f4e2e8ba2 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 @@ -9,6 +9,7 @@ import androidx.fragment.app.viewModels import androidx.recyclerview.widget.GridLayoutManager import com.festago.festago.R import com.festago.festago.databinding.FragmentFestivalListBinding +import com.festago.festago.model.FestivalFilter import com.festago.festago.presentation.ui.home.ticketlist.TicketListFragment import com.festago.festago.presentation.ui.ticketreserve.TicketReserveActivity import com.festago.festago.presentation.util.repeatOnStarted @@ -59,6 +60,15 @@ class FestivalListFragment : Fragment(R.layout.fragment_festival_list) { adapter = FestivalListAdapter() binding.rvFestivalList.adapter = adapter + initFestivalListSpanSize() + initRefresh() + initFestivalFilters() + if (vm.uiState.value is FestivalListUiState.Loading) { + loadFestivalsBy(binding.cgFilterOption.checkedChipId) + } + } + + private fun initFestivalListSpanSize() { binding.rvFestivalList.layoutManager.apply { if (this is GridLayoutManager) { val spanSize = (resources.displayMetrics.widthPixels.dp / 160) @@ -69,15 +79,29 @@ class FestivalListFragment : Fragment(R.layout.fragment_festival_list) { } } } + } - vm.loadFestivals() - + private fun initRefresh() { binding.srlFestivalList.setOnRefreshListener { - vm.loadFestivals() + loadFestivalsBy(binding.cgFilterOption.checkedChipId) binding.srlFestivalList.isRefreshing = false } } + private fun initFestivalFilters() { + binding.cgFilterOption.setOnCheckedStateChangeListener { group, _ -> + loadFestivalsBy(checkedChipId = group.checkedChipId) + } + } + + private fun loadFestivalsBy(checkedChipId: Int) { + when (checkedChipId) { + R.id.chipProgress -> vm.loadFestivals(FestivalFilter.PROGRESS) + R.id.chipPlanned -> vm.loadFestivals(FestivalFilter.PLANNED) + R.id.chipEnd -> vm.loadFestivals(FestivalFilter.END) + } + } + private fun updateUi(uiState: FestivalListUiState) { when (uiState) { is FestivalListUiState.Loading, 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 eda06b3a9..8ba2e7774 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 @@ -4,7 +4,7 @@ sealed interface FestivalListUiState { object Loading : FestivalListUiState data class Success( - val festivals: List + 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 78d86dc88..f69d601dd 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 @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure +import com.festago.festago.model.FestivalFilter import com.festago.festago.presentation.ui.home.festivallist.FestivalListEvent.ShowTicketReserve import com.festago.festago.repository.FestivalRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -28,9 +29,9 @@ class FestivalListViewModel @Inject constructor( private val _event = MutableSharedFlow() val event: SharedFlow = _event.asSharedFlow() - fun loadFestivals() { + fun loadFestivals(festivalFilter: FestivalFilter) { viewModelScope.launch { - festivalRepository.loadFestivals() + festivalRepository.loadFestivals(festivalFilter) .onSuccess { _uiState.value = FestivalListUiState.Success( festivals = it.map { festival -> diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationActivity.kt index 349ec05d4..54004bff5 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationActivity.kt @@ -10,6 +10,7 @@ import com.festago.festago.databinding.ActivityStudentVerificationBinding import com.festago.festago.presentation.ui.customview.OkDialogFragment import com.festago.festago.presentation.ui.home.HomeActivity import com.festago.festago.presentation.util.repeatOnStarted +import com.festago.festago.presentation.util.setOnSingleClickListener import dagger.hilt.android.AndroidEntryPoint import java.time.LocalTime import java.time.format.DateTimeFormatter @@ -43,7 +44,7 @@ class StudentVerificationActivity : AppCompatActivity() { } private fun initRequestVerificationCodeBtn(schoolId: Long) { - binding.btnRequestVerificationCode.setOnClickListener { + binding.btnRequestVerificationCode.setOnSingleClickListener { vm.sendVerificationCode(binding.tieUserName.text.toString(), schoolId) } } 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 529e364dd..df3393b94 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveActivity.kt @@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.ConcatAdapter import com.festago.festago.R import com.festago.festago.databinding.ActivityTicketReserveBinding +import com.festago.festago.model.ErrorCode import com.festago.festago.model.ReservationTicket import com.festago.festago.model.ReservedTicket import com.festago.festago.presentation.ui.customview.OkDialogFragment @@ -75,7 +76,7 @@ class TicketReserveActivity : AppCompatActivity() { ) is ReserveTicketSuccess -> handleReserveTicketSuccess(event.reservedTicket) - is ReserveTicketFailed -> handleReserveTicketFailed() + is ReserveTicketFailed -> handleReserveTicketFailed(event.errorCode) is ShowSignIn -> handleShowSignIn() } @@ -113,8 +114,14 @@ class TicketReserveActivity : AppCompatActivity() { finish() } - private fun handleReserveTicketFailed() { - OkDialogFragment.newInstance("예약에 실패하였습니다.") + private fun handleReserveTicketFailed(errorCode: ErrorCode) { + val message: String = when (errorCode) { + is ErrorCode.TICKET_SOLD_OUT -> getString(R.string.ticket_reserve_dialog_sold_out) + is ErrorCode.RESERVE_TICKET_OVER_AMOUNT -> getString(R.string.ticket_reserve_dialog_over_amount) + is ErrorCode.NEED_STUDENT_VERIFICATION -> getString(R.string.ticket_reserve_dialog_need_student_verification) + is ErrorCode.UNKNOWN -> getString(R.string.ticket_reserve_dialog_unknown) + } + OkDialogFragment.newInstance(message) .show(supportFragmentManager, OkDialogFragment::class.java.name) } 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 5cc0f3cc7..3ed05c64f 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,5 +1,6 @@ package com.festago.festago.presentation.ui.ticketreserve +import com.festago.festago.model.ErrorCode import com.festago.festago.model.ReservationTicket import com.festago.festago.model.ReservedTicket import java.time.LocalDateTime @@ -11,6 +12,6 @@ sealed interface TicketReserveEvent { ) : TicketReserveEvent class ReserveTicketSuccess(val reservedTicket: ReservedTicket) : TicketReserveEvent - object ReserveTicketFailed : TicketReserveEvent + class ReserveTicketFailed(val errorCode: ErrorCode) : TicketReserveEvent object ShowSignIn : TicketReserveEvent } 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 05c056faf..fa558fc8d 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 @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure +import com.festago.festago.model.ErrorCode import com.festago.festago.model.ReservationStage import com.festago.festago.repository.AuthRepository import com.festago.festago.repository.FestivalRepository @@ -49,12 +50,9 @@ class TicketReserveViewModel @Inject constructor( ), stages = it.reservationStages.toTicketReserveItems(), ) - }.onFailure { + }.onFailure { error -> _uiState.value = TicketReserveUiState.Error - analyticsHelper.logNetworkFailure( - KEY_LOAD_RESERVATION_LOG, - it.message.toString(), - ) + analyticsHelper.logNetworkFailure(KEY_LOAD_RESERVATION_LOG, error.message ?: "") } } } @@ -70,8 +68,12 @@ class TicketReserveViewModel @Inject constructor( reservationTickets.sortedByTicketTypes(), ), ) - }.onFailure { + }.onFailure { error -> _uiState.value = TicketReserveUiState.Error + analyticsHelper.logNetworkFailure( + KEY_SHOW_TICKET_TYPES_LOG, + error.message ?: "", + ) } } else { _event.emit(TicketReserveEvent.ShowSignIn) @@ -84,8 +86,13 @@ class TicketReserveViewModel @Inject constructor( ticketRepository.reserveTicket(ticketId) .onSuccess { _event.emit(TicketReserveEvent.ReserveTicketSuccess(it)) - }.onFailure { - _event.emit(TicketReserveEvent.ReserveTicketFailed) + }.onFailure { error -> + if (error is ErrorCode) { + _event.emit(TicketReserveEvent.ReserveTicketFailed(error)) + } else { + _event.emit(TicketReserveEvent.ReserveTicketFailed(ErrorCode.UNKNOWN())) + analyticsHelper.logNetworkFailure(KEY_RESERVE_TICKET, error.message ?: "") + } } } } @@ -108,5 +115,7 @@ class TicketReserveViewModel @Inject constructor( companion object { private const val KEY_LOAD_RESERVATION_LOG = "load_reservation" + private const val KEY_SHOW_TICKET_TYPES_LOG = "show_ticket_types" + private const val KEY_RESERVE_TICKET = "reserve_ticket" } } 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 d665e2b94..9c48dfb6e 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 @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.DividerItemDecoration import com.festago.festago.databinding.FragmentTicketReserveBottomSheetBinding import com.festago.festago.presentation.ui.ticketreserve.TicketReserveViewModel import com.festago.festago.presentation.util.getParcelableArrayListCompat @@ -22,11 +23,11 @@ class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { private val ticketTypeAdapter = TicketReserveBottomSheetAdapter { ticketId -> binding.selectedTicketTypeId = ticketId binding.btnReserveTicket.isEnabled = true + binding.tvTicketTypePrompt.visibility = View.INVISIBLE } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val viewModelProvider = ViewModelProvider(requireActivity()) vm = viewModelProvider[TicketReserveViewModel::class.java] } @@ -37,6 +38,7 @@ class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { savedInstanceState: Bundle?, ): View { _binding = FragmentTicketReserveBottomSheetBinding.inflate(inflater) + dialog?.setCanceledOnTouchOutside(false) binding.lifecycleOwner = viewLifecycleOwner return binding.root } @@ -56,6 +58,7 @@ class TicketReserveBottomSheetFragment : BottomSheetDialogFragment() { private fun initView() { binding.rvTicketTypes.adapter = ticketTypeAdapter + binding.rvTicketTypes.addItemDecoration(DividerItemDecoration(requireContext(), 1)) val onReserve: (Int) -> Unit = { id -> vm.reserveTicket(id) } binding.onReserve = onReserve binding.btnReserveTicket.isEnabled = false diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/util/Event.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/util/Event.kt deleted file mode 100644 index e9298dfbb..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/util/Event.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.festago.festago.presentation.util - -/** - * Used as a wrapper for data that is exposed via a LiveData that represents an event. - */ -open class Event(private val content: T) { - - var hasBeenHandled = false - private set // Allow external read but not write - - /** - * Returns the content and prevents its use again. - */ - fun getContentIfNotHandled(): T? { - return if (hasBeenHandled) { - null - } else { - hasBeenHandled = true - content - } - } - - /** - * Returns the content, even if it's already been handled. - */ - fun peekContent(): T = content -} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/util/MutableSingleLiveData.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/util/MutableSingleLiveData.kt deleted file mode 100644 index 68236a79c..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/util/MutableSingleLiveData.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.festago.festago.presentation.util - -class MutableSingleLiveData : SingleLiveData { - - constructor() : super() - - constructor(value: T) : super(value) - - public override fun postValue(value: T) { - super.postValue(value) - } - - public override fun setValue(value: T) { - super.setValue(value) - } -} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/util/ParcelizeUtil.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/util/ParcelizeUtil.kt index f32cd1e3e..abdef748d 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/util/ParcelizeUtil.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/util/ParcelizeUtil.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.os.Parcelable +import androidx.core.os.BundleCompat @Suppress("DEPRECATION") inline fun Bundle.getParcelableCompat(key: String): T? { @@ -23,11 +24,11 @@ inline fun Bundle.getParcelableArrayListCompat(key: Str } } -@Suppress("DEPRECATION") inline fun Intent.getParcelableExtraCompat(key: String): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getParcelableExtra(key, T::class.java) - } else { - getParcelableExtra(key) as? T + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val bundle = extras ?: return null + return BundleCompat.getParcelable(bundle, key, T::class.java) } + @Suppress("DEPRECATION") + return getParcelableExtra(key) as? T } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/util/SingleClickUtil.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/util/SingleClickUtil.kt new file mode 100644 index 000000000..ae4b96588 --- /dev/null +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/util/SingleClickUtil.kt @@ -0,0 +1,30 @@ +package com.festago.festago.presentation.util + +import android.os.SystemClock +import android.view.View +import androidx.databinding.BindingAdapter + +class OnSingleClickListener( + private var interval: Int = 600, + private var onSingleClick: (View) -> Unit, +) : View.OnClickListener { + + private var lastClickTime: Long = 0 + + override fun onClick(v: View) { + val elapsedRealtime = SystemClock.elapsedRealtime() + if ((elapsedRealtime - lastClickTime) < interval) { + return + } + lastClickTime = elapsedRealtime + onSingleClick(v) + } +} + +@BindingAdapter("onSingleClick") +fun View.setOnSingleClickListener(onSingleClick: (View) -> Unit) { + val oneClick = OnSingleClickListener { + onSingleClick(it) + } + setOnClickListener(oneClick) +} diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/util/SingleLiveData.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/util/SingleLiveData.kt deleted file mode 100644 index 434744392..000000000 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/util/SingleLiveData.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.festago.festago.presentation.util - -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.MutableLiveData - -abstract class SingleLiveData { - - private val liveData = MutableLiveData>() - - protected constructor() - - protected constructor(value: T) { - liveData.value = Event(value) - } - - protected open fun setValue(value: T) { - liveData.value = Event(value) - } - - protected open fun postValue(value: T) { - liveData.postValue(Event(value)) - } - - fun getValue() = liveData.value?.peekContent() - - fun observe(owner: LifecycleOwner, onResult: (T) -> Unit) { - liveData.observe(owner) { it.getContentIfNotHandled()?.let(onResult) } - } - - fun observePeek(owner: LifecycleOwner, onResult: (T) -> Unit) { - liveData.observe(owner) { onResult(it.peekContent()) } - } -} diff --git a/android/festago/app/src/main/res/drawable/bg_chip_festival_list_selected.xml b/android/festago/app/src/main/res/drawable/bg_chip_festival_list_selected.xml new file mode 100644 index 000000000..b92ade845 --- /dev/null +++ b/android/festago/app/src/main/res/drawable/bg_chip_festival_list_selected.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/festago/app/src/main/res/drawable/img_festago_home_logo.xml b/android/festago/app/src/main/res/drawable/img_festago_home_logo.xml new file mode 100644 index 000000000..1694c060c --- /dev/null +++ b/android/festago/app/src/main/res/drawable/img_festago_home_logo.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/android/festago/app/src/main/res/drawable/text_chip_festival_list_selected.xml b/android/festago/app/src/main/res/drawable/text_chip_festival_list_selected.xml new file mode 100644 index 000000000..f7ec5e8b0 --- /dev/null +++ b/android/festago/app/src/main/res/drawable/text_chip_festival_list_selected.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/festago/app/src/main/res/layout/fragment_festival_list.xml b/android/festago/app/src/main/res/layout/fragment_festival_list.xml index b4b4a20f3..14da2ce1b 100644 --- a/android/festago/app/src/main/res/layout/fragment_festival_list.xml +++ b/android/festago/app/src/main/res/layout/fragment_festival_list.xml @@ -15,68 +15,129 @@ android:layout_height="match_parent" tools:context=".presentation.ui.home.festivallist.FestivalListFragment"> - - - - - - + android:layout_height="0dp" + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + app:layout_constraintTop_toBottomOf="@id/glFestivalListTop" + tools:visibility="gone" /> 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 f34d68337..384feb9f2 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 @@ -47,22 +47,42 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="44dp" + android:text="@{stageStartTime}" android:textSize="18sp" android:textStyle="bold" - android:text="@{stageStartTime}" app:layout_constraintEnd_toEndOf="@id/endGuideLine" app:layout_constraintStart_toStartOf="@id/startGuideline" app:layout_constraintTop_toTopOf="parent" tools:text="07.03 (목) 17:00" /> + + + + + + @@ -73,11 +93,11 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="16dp" android:layout_marginVertical="28dp" - android:onClick="@{() -> onReserve.invoke(selectedTicketTypeId)}" android:text="@string/ticket_reserve_tv_btn_reserve_ticket" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" /> + app:layout_constraintStart_toStartOf="parent" + app:onSingleClick="@{() -> onReserve.invoke(selectedTicketTypeId)}" /> 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 c1df86cff..f1835d3db 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 @@ -111,11 +111,11 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="16dp" android:layout_marginTop="28dp" - android:onClick="@{() -> stage.onShowStageTickets.invoke(stage.id, stage.startTime)}" android:text="@string/ticket_reserve_btn_reserve_ticket" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/tvAuthGuideStudent" /> + app:layout_constraintTop_toBottomOf="@id/tvAuthGuideStudent" + app:onSingleClick="@{() -> stage.onShowStageTickets.invoke(stage.id, stage.startTime)}" /> diff --git a/android/festago/app/src/main/res/layout/item_ticket_reserve_header.xml b/android/festago/app/src/main/res/layout/item_ticket_reserve_header.xml index 221d4abaa..eb30c1b19 100644 --- a/android/festago/app/src/main/res/layout/item_ticket_reserve_header.xml +++ b/android/festago/app/src/main/res/layout/item_ticket_reserve_header.xml @@ -33,12 +33,13 @@ android:textSize="16sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvUniversityName" + android:layout_marginBottom="20dp" tools:text="2023.07.03 - 2023.07.09" /> diff --git a/android/festago/app/src/main/res/values/strings.xml b/android/festago/app/src/main/res/values/strings.xml index 35daf2b7f..8cf6589d1 100644 --- a/android/festago/app/src/main/res/values/strings.xml +++ b/android/festago/app/src/main/res/values/strings.xml @@ -39,6 +39,7 @@ 티켓 목록 마이페이지 알림 권한을 거부했습니다 :( + 한 번 더 누르면 앱이 종료됩니다 %1s ~ %1s @@ -55,11 +56,17 @@ 예매 하기 MM월 dd일 HH:mm 오픈예정 로그인 후 예매 + 예매 유형을 선택해주세요 + 공연이 매진되었습니다 + 이미 예매한 공연입니다 + 학생 인증이 필요합니다 + 알 수 없는 오류가 발생했습니다 yyyy.MM.dd - 진행중 - 지난축제 + 진행중 + 진행 예정 + 지난 축제 축제 조회에 실패했습니다. 조회된 축제가 없습니다. %s - %s diff --git a/android/festago/app/src/main/res/values/style.xml b/android/festago/app/src/main/res/values/style.xml index 004ea7295..d071718c5 100644 --- a/android/festago/app/src/main/res/values/style.xml +++ b/android/festago/app/src/main/res/values/style.xml @@ -22,4 +22,10 @@ 50% + + diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/rule/MainDispatcherRule.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/rule/MainDispatcherRule.kt new file mode 100644 index 000000000..3059da632 --- /dev/null +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/rule/MainDispatcherRule.kt @@ -0,0 +1,28 @@ +package com.festago.festago.presentation.rule + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +// Reusable JUnit4 TestRule to override the Main dispatcher +class MainDispatcherRule +@OptIn(ExperimentalCoroutinesApi::class) +constructor( + private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), +) : TestWatcher() { + + @OptIn(ExperimentalCoroutinesApi::class) + override fun starting(description: Description) { + Dispatchers.setMain(testDispatcher) + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun finished(description: Description) { + Dispatchers.resetMain() + } +} 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 6a7c5616c..b867fe986 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,38 +1,30 @@ package com.festago.festago.presentation.ui.home import app.cash.turbine.test +import com.festago.festago.presentation.rule.MainDispatcherRule import com.festago.festago.repository.AuthRepository import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain import org.assertj.core.api.Assertions.assertThat -import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test class HomeViewModelTest { + private lateinit var vm: HomeViewModel private lateinit var authRepository: AuthRepository - @OptIn(ExperimentalCoroutinesApi::class) + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Before fun setUp() { - Dispatchers.setMain(UnconfinedTestDispatcher()) authRepository = mockk() vm = HomeViewModel(authRepository) } - @OptIn(ExperimentalCoroutinesApi::class) - @After - fun finish() { - Dispatchers.resetMain() - } - private fun `사용자 인증 유무가 다음과 같을 때`(isSigned: Boolean) { every { authRepository.isSigned } returns isSigned } 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 56b5b060b..2696cbf11 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 @@ -3,20 +3,17 @@ package com.festago.festago.presentation.ui.home.festivallist import app.cash.turbine.test import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.model.Festival +import com.festago.festago.model.FestivalFilter +import com.festago.festago.presentation.rule.MainDispatcherRule import com.festago.festago.repository.FestivalRepository import io.mockk.coEvery import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.SoftAssertions -import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import java.time.LocalDate @@ -36,36 +33,31 @@ class FestivalListViewModelTest { ) } - @OptIn(ExperimentalCoroutinesApi::class) + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Before fun setUp() { - Dispatchers.setMain(UnconfinedTestDispatcher()) festivalRepository = mockk() analyticsHelper = mockk(relaxed = true) vm = FestivalListViewModel(festivalRepository, analyticsHelper) } - @OptIn(ExperimentalCoroutinesApi::class) - @After - fun finish() { - Dispatchers.resetMain() - } - private fun `축제 목록 요청 결과가 다음과 같을 때`(result: Result>) { coEvery { - festivalRepository.loadFestivals() + festivalRepository.loadFestivals(any()) } answers { result } } @Test - fun `축제 목록 받아오기에 성공하면 성공 상태이고 축제 목록을 반환한다`() { + fun `진행 예정인 축제 목록 받아오기에 성공하면 성공 상태이고 축제 목록을 반환한다`() { // given `축제 목록 요청 결과가 다음과 같을 때`(Result.success(fakeFestivals)) // when - vm.loadFestivals() + vm.loadFestivals(FestivalFilter.PLANNED) // then val softly = SoftAssertions().apply { @@ -85,12 +77,12 @@ class FestivalListViewModelTest { } @Test - fun `축제 목록 받아오기에 실패하면 에러 상태다`() { + fun `진행 예정 축제 목록 받아오기에 실패하면 에러 상태이다`() { // given `축제 목록 요청 결과가 다음과 같을 때`(Result.failure(Exception())) // when - vm.loadFestivals() + vm.loadFestivals(FestivalFilter.PLANNED) // then val softly = SoftAssertions().apply { @@ -108,14 +100,14 @@ class FestivalListViewModelTest { fun `축제 목록을 받아오는 중이면 로딩 상태다`() { // given coEvery { - festivalRepository.loadFestivals() + festivalRepository.loadFestivals(any()) } coAnswers { delay(1000) Result.success(emptyList()) } // when - vm.loadFestivals() + vm.loadFestivals(FestivalFilter.PLANNED) // then val softly = SoftAssertions().apply { @@ -131,7 +123,6 @@ class FestivalListViewModelTest { @Test fun `티켓 예매를 열면 티켓 예매 열기 이벤트가 발생한다`() = runTest { - vm.event.test { // when val fakeFestivalId = 1L 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 2eb87bdae..a93a2d8aa 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 @@ -7,26 +7,23 @@ 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.rule.MainDispatcherRule 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 -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.SoftAssertions -import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import java.time.LocalDateTime class MyPageViewModelTest { + private lateinit var vm: MyPageViewModel private lateinit var userRepository: UserRepository private lateinit var ticketRepository: TicketRepository @@ -55,10 +52,11 @@ class MyPageViewModelTest { ), ) - @OptIn(ExperimentalCoroutinesApi::class) + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Before fun setUp() { - Dispatchers.setMain(UnconfinedTestDispatcher()) userRepository = mockk(relaxed = true) ticketRepository = mockk() authRepository = mockk() @@ -66,12 +64,6 @@ class MyPageViewModelTest { vm = MyPageViewModel(userRepository, ticketRepository, authRepository, analyticsHelper) } - @OptIn(ExperimentalCoroutinesApi::class) - @After - fun finish() { - Dispatchers.resetMain() - } - private fun `로그인 상태가 다음과 같다`(result: Boolean) { coEvery { authRepository.isSigned 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 050fe50fd..2ddeb69f5 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 @@ -4,41 +4,33 @@ import app.cash.turbine.test import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.model.Ticket import com.festago.festago.presentation.fixture.TicketFixture +import com.festago.festago.presentation.rule.MainDispatcherRule import com.festago.festago.repository.TicketRepository import io.mockk.coEvery import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain import org.assertj.core.api.SoftAssertions -import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test class TicketListViewModelTest { + private lateinit var vm: TicketListViewModel private lateinit var ticketRepository: TicketRepository private lateinit var analyticsHelper: AnalyticsHelper - @OptIn(ExperimentalCoroutinesApi::class) + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Before fun setUp() { - Dispatchers.setMain(UnconfinedTestDispatcher()) ticketRepository = mockk() analyticsHelper = mockk(relaxed = true) vm = TicketListViewModel(ticketRepository, analyticsHelper) } - @OptIn(ExperimentalCoroutinesApi::class) - @After - fun finish() { - Dispatchers.resetMain() - } - private fun `현재 티켓 요청 결과가 다음과 같을 때`(result: Result>) { coEvery { ticketRepository.loadCurrentTickets() } returns result } diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModelTest.kt index d6708dc56..a898f407c 100644 --- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModelTest.kt +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModelTest.kt @@ -3,32 +3,28 @@ package com.festago.festago.presentation.ui.selectschool import app.cash.turbine.test import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.model.School +import com.festago.festago.presentation.rule.MainDispatcherRule import com.festago.festago.repository.SchoolRepository import io.mockk.coEvery import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.SoftAssertions.assertSoftly -import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test -@OptIn(ExperimentalCoroutinesApi::class) class SelectSchoolViewModelTest { - private val testDispatcher = UnconfinedTestDispatcher() private lateinit var vm: SelectSchoolViewModel private lateinit var schoolRepository: SchoolRepository private lateinit var analyticsHelper: AnalyticsHelper + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Before fun setup() { - Dispatchers.setMain(testDispatcher) schoolRepository = mockk() analyticsHelper = mockk(relaxed = true) vm = SelectSchoolViewModel( @@ -37,11 +33,6 @@ class SelectSchoolViewModelTest { ) } - @After - fun finish() { - Dispatchers.resetMain() - } - private fun `학교 목록 불러오기 요청 결과가 다음과 같을 때 `(result: Result>) { coEvery { schoolRepository.loadSchools() 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 c8f209389..5a53f69b5 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,41 +2,32 @@ package com.festago.festago.presentation.ui.signin import app.cash.turbine.test import com.festago.festago.analytics.AnalyticsHelper +import com.festago.festago.presentation.rule.MainDispatcherRule import com.festago.festago.repository.AuthRepository import io.mockk.coEvery import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain import org.assertj.core.api.Assertions.assertThat -import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test class SignInViewModelTest { + private lateinit var vm: SignInViewModel private lateinit var authRepository: AuthRepository private lateinit var analyticsHelper: AnalyticsHelper - @OptIn(ExperimentalCoroutinesApi::class) + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Before fun setUp() { - Dispatchers.setMain(UnconfinedTestDispatcher()) - authRepository = mockk(relaxed = true) analyticsHelper = mockk(relaxed = true) vm = SignInViewModel(authRepository, analyticsHelper) } - @OptIn(ExperimentalCoroutinesApi::class) - @After - fun finish() { - Dispatchers.resetMain() - } - private fun `로그인 결과가 다음과 같을 때`(result: Result) { coEvery { authRepository.signIn() } returns result } diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModelTest.kt index 672066619..2dd025261 100644 --- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModelTest.kt +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModelTest.kt @@ -3,34 +3,30 @@ package com.festago.festago.presentation.ui.studentverification import app.cash.turbine.test import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.model.StudentVerificationCode +import com.festago.festago.presentation.rule.MainDispatcherRule import com.festago.festago.repository.SchoolRepository import com.festago.festago.repository.StudentVerificationRepository import io.mockk.coEvery import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.SoftAssertions.assertSoftly -import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test -@OptIn(ExperimentalCoroutinesApi::class) class StudentVerificationViewModelTest { - private val testDispatcher = UnconfinedTestDispatcher() private lateinit var vm: StudentVerificationViewModel private lateinit var studentVerificationRepository: StudentVerificationRepository private lateinit var schoolRepository: SchoolRepository private lateinit var analyticsHelper: AnalyticsHelper + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Before fun setUp() { - Dispatchers.setMain(testDispatcher) studentVerificationRepository = mockk() schoolRepository = mockk() analyticsHelper = mockk(relaxed = true) @@ -41,12 +37,6 @@ class StudentVerificationViewModelTest { ) } - @OptIn(ExperimentalCoroutinesApi::class) - @After - fun finish() { - Dispatchers.resetMain() - } - private fun `이메일 요청 결과가 다음과 같을 때`(result: Result) { coEvery { schoolRepository.loadSchoolEmail(any()) 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 9369a2a87..356b8af93 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 @@ -4,40 +4,32 @@ import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.model.Ticket import com.festago.festago.model.TicketCode import com.festago.festago.presentation.fixture.TicketFixture +import com.festago.festago.presentation.rule.MainDispatcherRule import com.festago.festago.repository.TicketRepository import io.mockk.coEvery import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain import org.assertj.core.api.SoftAssertions -import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test class TicketEntryViewModelTest { + private lateinit var vm: TicketEntryViewModel private lateinit var ticketRepository: TicketRepository private lateinit var analyticsHelper: AnalyticsHelper - @OptIn(ExperimentalCoroutinesApi::class) + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Before fun setUp() { - Dispatchers.setMain(UnconfinedTestDispatcher()) ticketRepository = mockk() analyticsHelper = mockk(relaxed = true) vm = TicketEntryViewModel(ticketRepository, analyticsHelper) } - @OptIn(ExperimentalCoroutinesApi::class) - @After - fun finish() { - Dispatchers.resetMain() - } - private fun `티켓 요쳥 결과는 다음과 같을 때`(result: Result) { coEvery { ticketRepository.loadTicket(any()) } returns result } 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 b9c8f8fec..5611f58fe 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 @@ -3,30 +3,27 @@ package com.festago.festago.presentation.ui.tickethistory import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.model.Ticket import com.festago.festago.presentation.fixture.TicketFixture +import com.festago.festago.presentation.rule.MainDispatcherRule import com.festago.festago.repository.TicketRepository import io.mockk.coEvery import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain import org.assertj.core.api.SoftAssertions -import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test class TicketHistoryViewModelTest { - private lateinit var vm: TicketHistoryViewModel + private lateinit var vm: TicketHistoryViewModel private lateinit var ticketRepository: TicketRepository private lateinit var analyticsHelper: AnalyticsHelper - @OptIn(ExperimentalCoroutinesApi::class) + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Before fun setUp() { - Dispatchers.setMain(UnconfinedTestDispatcher()) ticketRepository = mockk() analyticsHelper = mockk(relaxed = true) @@ -36,12 +33,6 @@ class TicketHistoryViewModelTest { ) } - @OptIn(ExperimentalCoroutinesApi::class) - @After - fun finish() { - Dispatchers.resetMain() - } - private fun `티켓 기록 요청 결과가 다음과 같을 때`(result: Result>) { coEvery { ticketRepository.loadHistoryTickets(any()) 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 b6a5582c7..bc0252359 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,34 +2,32 @@ package com.festago.festago.presentation.ui.ticketreserve import app.cash.turbine.test import com.festago.festago.analytics.AnalyticsHelper +import com.festago.festago.model.ErrorCode import com.festago.festago.model.Reservation import com.festago.festago.model.ReservationStage import com.festago.festago.model.ReservationTicket import com.festago.festago.model.ReservationTickets import com.festago.festago.model.ReservedTicket import com.festago.festago.model.TicketType +import com.festago.festago.presentation.rule.MainDispatcherRule 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 -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain import org.assertj.core.api.AssertionsForClassTypes.assertThat import org.assertj.core.api.SoftAssertions -import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import java.time.LocalDate import java.time.LocalDateTime class TicketReserveViewModelTest { + private lateinit var vm: TicketReserveViewModel private lateinit var reservationTicketRepository: ReservationTicketRepository private lateinit var festivalRepository: FestivalRepository @@ -66,10 +64,11 @@ class TicketReserveViewModelTest { number = 1, ) - @OptIn(ExperimentalCoroutinesApi::class) + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Before fun setUp() { - Dispatchers.setMain(UnconfinedTestDispatcher()) reservationTicketRepository = mockk() festivalRepository = mockk() ticketRepository = mockk() @@ -84,12 +83,6 @@ class TicketReserveViewModelTest { ) } - @OptIn(ExperimentalCoroutinesApi::class) - @After - fun tearDown() { - Dispatchers.resetMain() - } - private fun `예약 정보 요청 결과가 다음과 같을 때`(result: Result) { coEvery { festivalRepository.loadFestivalDetail(any()) } returns result } @@ -196,7 +189,7 @@ class TicketReserveViewModelTest { } @Test - fun `티켓 유형을 선택하고 예약하면 예약 성공 이벤트가 발생한다`() = runTest { + fun `티켓 유형을 선택하고 예약하면 예매 성공 이벤트가 발생한다`() = runTest { // given coEvery { ticketRepository.reserveTicket(any()) @@ -214,7 +207,61 @@ class TicketReserveViewModelTest { } @Test - fun `티켓 유형을 선택하고 예약하는 것을 실패하면 예약 실패 이벤트가 발생한다`() = runTest { + fun `학생 인증하지 않아 티켓 예매에 실패하면 예매 실패 이벤트가 발생한다`() = runTest { + // given + `티켓 예약 요청 결과가 다음과 같을 때`(Result.failure(ErrorCode.NEED_STUDENT_VERIFICATION())) + + vm.event.test { + // when + vm.reserveTicket(0) + + // then + val actual = awaitItem() as? TicketReserveEvent.ReserveTicketFailed + assertThat(actual).isNotNull + + // and: 학생 인증 필요 예매 실패 코드를 가진다 + assertThat(actual?.errorCode).isExactlyInstanceOf(ErrorCode.NEED_STUDENT_VERIFICATION::class.java) + } + } + + @Test + fun `이미 예매한 티켓이라서 티켓 예매에 실패하면 예매 실패 이벤트가 발생한다`() = runTest { + // given + `티켓 예약 요청 결과가 다음과 같을 때`(Result.failure(ErrorCode.RESERVE_TICKET_OVER_AMOUNT())) + + vm.event.test { + // when + vm.reserveTicket(0) + + // then + val actual = awaitItem() as? TicketReserveEvent.ReserveTicketFailed + assertThat(actual).isNotNull + + // and: 보유 가능한 수량 초과 예매 실패 코드를 가진다 + assertThat(actual?.errorCode).isExactlyInstanceOf(ErrorCode.RESERVE_TICKET_OVER_AMOUNT::class.java) + } + } + + @Test + fun `티켓이 매진되어 티켓 예매에 실패하면 예매 실패 이벤트가 발생한다`() = runTest { + // given + `티켓 예약 요청 결과가 다음과 같을 때`(Result.failure(ErrorCode.TICKET_SOLD_OUT())) + + vm.event.test { + // when + vm.reserveTicket(0) + + // then + val actual = awaitItem() as? TicketReserveEvent.ReserveTicketFailed + assertThat(actual).isNotNull + + // and: 티켓 매진 예매 실패 코드를 가진다 + assertThat(actual?.errorCode).isExactlyInstanceOf(ErrorCode.TICKET_SOLD_OUT::class.java) + } + } + + @Test + fun `알 수 없는 오류로 티켓 예매에 실패하면 예매 실패 이벤트가 발생한다`() = runTest { // given `티켓 예약 요청 결과가 다음과 같을 때`(Result.failure(Exception())) @@ -223,7 +270,11 @@ class TicketReserveViewModelTest { vm.reserveTicket(0) // then - assertThat(awaitItem()).isExactlyInstanceOf(TicketReserveEvent.ReserveTicketFailed::class.java) + val actual = awaitItem() as? TicketReserveEvent.ReserveTicketFailed + assertThat(actual).isNotNull + + // and: 알 수 없는 예매 실패 코드를 가진다 + assertThat(actual?.errorCode).isExactlyInstanceOf(ErrorCode.UNKNOWN::class.java) } } } diff --git a/android/festago/domain/src/main/java/com/festago/festago/model/ErrorCode.kt b/android/festago/domain/src/main/java/com/festago/festago/model/ErrorCode.kt new file mode 100644 index 000000000..55baeae7c --- /dev/null +++ b/android/festago/domain/src/main/java/com/festago/festago/model/ErrorCode.kt @@ -0,0 +1,9 @@ +package com.festago.festago.model + +sealed class ErrorCode : Throwable() { + + class NEED_STUDENT_VERIFICATION : ErrorCode() + class RESERVE_TICKET_OVER_AMOUNT : ErrorCode() + class TICKET_SOLD_OUT : ErrorCode() + class UNKNOWN : ErrorCode() +} diff --git a/android/festago/domain/src/main/java/com/festago/festago/model/FestivalFilter.kt b/android/festago/domain/src/main/java/com/festago/festago/model/FestivalFilter.kt new file mode 100644 index 000000000..4f881328f --- /dev/null +++ b/android/festago/domain/src/main/java/com/festago/festago/model/FestivalFilter.kt @@ -0,0 +1,5 @@ +package com.festago.festago.model + +enum class FestivalFilter { + ALL, PROGRESS, PLANNED, END +} diff --git a/android/festago/domain/src/main/java/com/festago/festago/repository/FestivalRepository.kt b/android/festago/domain/src/main/java/com/festago/festago/repository/FestivalRepository.kt index db08d52dd..326077ec2 100644 --- a/android/festago/domain/src/main/java/com/festago/festago/repository/FestivalRepository.kt +++ b/android/festago/domain/src/main/java/com/festago/festago/repository/FestivalRepository.kt @@ -1,9 +1,10 @@ package com.festago.festago.repository import com.festago.festago.model.Festival +import com.festago.festago.model.FestivalFilter import com.festago.festago.model.Reservation interface FestivalRepository { - suspend fun loadFestivals(): Result> + suspend fun loadFestivals(festivalFilter: FestivalFilter): Result> suspend fun loadFestivalDetail(festivalId: Long): Result } diff --git a/backend/src/main/java/com/festago/admin/application/AdminService.java b/backend/src/main/java/com/festago/admin/application/AdminService.java deleted file mode 100644 index 6caf46f1a..000000000 --- a/backend/src/main/java/com/festago/admin/application/AdminService.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.festago.admin.application; - -import com.festago.admin.dto.AdminFestivalResponse; -import com.festago.admin.dto.AdminResponse; -import com.festago.admin.dto.AdminSchoolResponse; -import com.festago.admin.dto.AdminStageResponse; -import com.festago.admin.dto.AdminTicketResponse; -import com.festago.festival.domain.Festival; -import com.festago.festival.repository.FestivalRepository; -import com.festago.school.domain.School; -import com.festago.school.repository.SchoolRepository; -import com.festago.stage.domain.Stage; -import com.festago.stage.repository.StageRepository; -import com.festago.ticket.domain.Ticket; -import com.festago.ticket.domain.TicketAmount; -import com.festago.ticket.domain.TicketEntryTime; -import com.festago.ticket.repository.TicketRepository; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Transactional -@Service -@RequiredArgsConstructor -public class AdminService { - - private final FestivalRepository festivalRepository; - private final StageRepository stageRepository; - private final TicketRepository ticketRepository; - private final SchoolRepository schoolRepository; - - @Transactional(readOnly = true) - public AdminResponse getAdminResponse() { - List allSchool = schoolRepository.findAll(); - List allTicket = ticketRepository.findAll(); - List allStage = stageRepository.findAll(); - List allFestival = festivalRepository.findAll(); - return new AdminResponse( - schoolResponses(allSchool), - ticketResponses(allTicket), - stageResponses(allStage), - festivalResponses(allFestival)); - } - - private List schoolResponses(List schools) { - return schools.stream() - .map(AdminSchoolResponse::from) - .toList(); - } - - private List ticketResponses(List tickets) { - return tickets.stream() - .map(this::ticketResponse) - .toList(); - } - - private AdminTicketResponse ticketResponse(Ticket ticket) { - TicketAmount ticketAmount = ticket.getTicketAmount(); - Map amountByEntryTime = ticket.getTicketEntryTimes().stream() - .collect(Collectors.toMap( - TicketEntryTime::getEntryTime, - TicketEntryTime::getAmount - )); - return new AdminTicketResponse( - ticket.getId(), - ticket.getStage().getId(), - ticket.getTicketType(), - ticketAmount.getTotalAmount(), - ticketAmount.getReservedAmount(), - amountByEntryTime - ); - } - - private List stageResponses(List stages) { - return stages.stream() - .map(this::stageResponse) - .toList(); - } - - private AdminStageResponse stageResponse(Stage stage) { - List ticketIds = stage.getTickets().stream() - .map(Ticket::getId) - .toList(); - return new AdminStageResponse( - stage.getId(), - stage.getFestival().getId(), - stage.getStartTime(), - stage.getLineUp(), - ticketIds - ); - } - - private List festivalResponses(List festivals) { - return festivals.stream() - .map(festival -> new AdminFestivalResponse( - festival.getId(), - festival.getSchool().getId(), - festival.getName(), - festival.getStartDate(), - festival.getEndDate(), - festival.getThumbnail())) - .toList(); - } -} diff --git a/backend/src/main/java/com/festago/admin/domain/Admin.java b/backend/src/main/java/com/festago/admin/domain/Admin.java index a7937444e..009fc5319 100644 --- a/backend/src/main/java/com/festago/admin/domain/Admin.java +++ b/backend/src/main/java/com/festago/admin/domain/Admin.java @@ -1,12 +1,15 @@ package com.festago.admin.domain; import com.festago.common.domain.BaseTimeEntity; +import com.festago.common.util.Validator; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -22,12 +25,21 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Admin extends BaseTimeEntity { + private static final int MIN_USERNAME_LENGTH = 4; + private static final int MAX_USERNAME_LENGTH = 20; + private static final int MIN_PASSWORD_LENGTH = 4; + private static final int MAX_PASSWORD_LENGTH = 255; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @NotNull + @Size(min = MIN_USERNAME_LENGTH, max = MAX_USERNAME_LENGTH) private String username; + @NotNull + @Size(min = MIN_PASSWORD_LENGTH, max = MAX_PASSWORD_LENGTH) private String password; public Admin(String username, String password) { @@ -35,11 +47,31 @@ public Admin(String username, String password) { } public Admin(Long id, String username, String password) { + validate(username, password); this.id = id; this.username = username; this.password = password; } + private void validate(String username, String password) { + validateUsername(username); + validatePassword(password); + } + + private void validateUsername(String username) { + String fieldName = "username"; + Validator.hasBlank(username, fieldName); + Validator.minLength(username, MIN_USERNAME_LENGTH, fieldName); + Validator.maxLength(username, MAX_USERNAME_LENGTH, fieldName); + } + + private void validatePassword(String password) { + String fieldName = "password"; + Validator.hasBlank(password, fieldName); + Validator.minLength(password, MIN_PASSWORD_LENGTH, fieldName); + Validator.maxLength(password, MAX_PASSWORD_LENGTH, fieldName); + } + public Long getId() { return id; } diff --git a/backend/src/main/java/com/festago/admin/dto/AdminFestivalResponse.java b/backend/src/main/java/com/festago/admin/dto/AdminFestivalResponse.java deleted file mode 100644 index ea59b850e..000000000 --- a/backend/src/main/java/com/festago/admin/dto/AdminFestivalResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.festago.admin.dto; - -import java.time.LocalDate; - -public record AdminFestivalResponse( - Long id, - Long schoolId, - String name, - LocalDate startDate, - LocalDate endDate, - String thumbnail) { - -} diff --git a/backend/src/main/java/com/festago/admin/dto/AdminResponse.java b/backend/src/main/java/com/festago/admin/dto/AdminResponse.java deleted file mode 100644 index 59c473a7f..000000000 --- a/backend/src/main/java/com/festago/admin/dto/AdminResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.festago.admin.dto; - -import java.util.List; - -public record AdminResponse( - List adminSchools, - List adminTickets, - List adminStageResponse, - List adminFestivalResponse) { - -} diff --git a/backend/src/main/java/com/festago/admin/dto/AdminSchoolResponse.java b/backend/src/main/java/com/festago/admin/dto/AdminSchoolResponse.java deleted file mode 100644 index 6a898ba24..000000000 --- a/backend/src/main/java/com/festago/admin/dto/AdminSchoolResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.festago.admin.dto; - -import com.festago.school.domain.School; - -public record AdminSchoolResponse( - Long id, - String domain, - String name) { - - public static AdminSchoolResponse from(School school) { - return new AdminSchoolResponse( - school.getId(), - school.getDomain(), - school.getName() - ); - } -} diff --git a/backend/src/main/java/com/festago/admin/dto/AdminStageResponse.java b/backend/src/main/java/com/festago/admin/dto/AdminStageResponse.java deleted file mode 100644 index ab568d4c2..000000000 --- a/backend/src/main/java/com/festago/admin/dto/AdminStageResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.festago.admin.dto; - -import java.time.LocalDateTime; -import java.util.List; - -public record AdminStageResponse( - Long id, - Long festivalId, - LocalDateTime startTime, - String lineUp, - List ticketId) { - -} diff --git a/backend/src/main/java/com/festago/admin/dto/AdminTicketResponse.java b/backend/src/main/java/com/festago/admin/dto/AdminTicketResponse.java deleted file mode 100644 index 88c93754d..000000000 --- a/backend/src/main/java/com/festago/admin/dto/AdminTicketResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.festago.admin.dto; - -import com.festago.ticket.domain.TicketType; -import java.time.LocalDateTime; -import java.util.Map; - -public record AdminTicketResponse( - Long id, - Long stageId, - TicketType ticketType, - Integer totalAmount, - Integer reservedAmount, - Map entryTimeAmount) { - -} diff --git a/backend/src/main/java/com/festago/auth/application/OAuth2Clients.java b/backend/src/main/java/com/festago/auth/application/OAuth2Clients.java index 7805c57f0..8b7144d0e 100644 --- a/backend/src/main/java/com/festago/auth/application/OAuth2Clients.java +++ b/backend/src/main/java/com/festago/auth/application/OAuth2Clients.java @@ -3,12 +3,14 @@ import com.festago.auth.domain.SocialType; import com.festago.common.exception.BadRequestException; import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +@Slf4j public class OAuth2Clients { private final Map oAuth2ClientMap; @@ -43,7 +45,8 @@ public OAuth2ClientsBuilder addAll(List oAuth2Clients) { public OAuth2ClientsBuilder add(OAuth2Client oAuth2Client) { SocialType socialType = oAuth2Client.getSocialType(); if (oAuth2ClientMap.containsKey(socialType)) { - throw new InternalServerException(ErrorCode.DUPLICATE_SOCIAL_TYPE); + log.error("OAuth2 제공자는 중복될 수 없습니다."); + throw new UnexpectedException("중복된 OAuth2 제공자 입니다."); } oAuth2ClientMap.put(socialType, oAuth2Client); return this; 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 1b174f74c..615ddbeba 100644 --- a/backend/src/main/java/com/festago/auth/domain/AuthPayload.java +++ b/backend/src/main/java/com/festago/auth/domain/AuthPayload.java @@ -1,7 +1,6 @@ package com.festago.auth.domain; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; public class AuthPayload { @@ -16,7 +15,7 @@ public AuthPayload(Long memberId, Role role) { private void validate(Role role) { if (role == null) { - throw new InternalServerException(ErrorCode.INVALID_AUTH_TOKEN_PAYLOAD); + throw new UnexpectedException("role은 null이 될 수 없습니다."); } } diff --git a/backend/src/main/java/com/festago/auth/domain/Role.java b/backend/src/main/java/com/festago/auth/domain/Role.java index 69936580b..d61a3b4a8 100644 --- a/backend/src/main/java/com/festago/auth/domain/Role.java +++ b/backend/src/main/java/com/festago/auth/domain/Role.java @@ -3,8 +3,7 @@ import com.festago.auth.annotation.Admin; import com.festago.auth.annotation.Anonymous; import com.festago.auth.annotation.Member; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; import java.lang.annotation.Annotation; public enum Role { @@ -23,7 +22,7 @@ public static Role from(String role) { try { return valueOf(role); } catch (NullPointerException | IllegalArgumentException e) { - throw new InternalServerException(ErrorCode.INVALID_ROLE_NAME); + throw new UnexpectedException("해당하는 Role이 없습니다."); } } diff --git a/backend/src/main/java/com/festago/auth/dto/KakaoAccessTokenResponse.java b/backend/src/main/java/com/festago/auth/dto/KakaoAccessTokenResponse.java deleted file mode 100644 index c7f8c793d..000000000 --- a/backend/src/main/java/com/festago/auth/dto/KakaoAccessTokenResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.festago.auth.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public record KakaoAccessTokenResponse( - @JsonProperty("token_type") String tokenType, - @JsonProperty("access_token") String accessToken, - @JsonProperty("expires_in") Integer expiresIn, - @JsonProperty("refresh_token") String refreshToken, - @JsonProperty("refresh_token_expires_in") Integer refreshTokenExpiresIn -) { - -} diff --git a/backend/src/main/java/com/festago/auth/dto/LoginMember.java b/backend/src/main/java/com/festago/auth/dto/LoginMember.java deleted file mode 100644 index 48a952fa6..000000000 --- a/backend/src/main/java/com/festago/auth/dto/LoginMember.java +++ /dev/null @@ -1,10 +0,0 @@ -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/infrastructure/JwtAuthExtractor.java b/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthExtractor.java index 5eef34745..32a661e4c 100644 --- a/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthExtractor.java +++ b/backend/src/main/java/com/festago/auth/infrastructure/JwtAuthExtractor.java @@ -7,13 +7,17 @@ import com.festago.common.exception.UnauthorizedException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; import java.nio.charset.StandardCharsets; import javax.crypto.SecretKey; +import lombok.extern.slf4j.Slf4j; +@Slf4j public class JwtAuthExtractor implements AuthExtractor { private static final String MEMBER_ID_KEY = "memberId"; @@ -42,8 +46,11 @@ private Claims getClaims(String code) { .getBody(); } catch (ExpiredJwtException e) { throw new UnauthorizedException(ErrorCode.EXPIRED_AUTH_TOKEN); - } catch (JwtException | IllegalArgumentException e) { + } catch (SignatureException | IllegalArgumentException | MalformedJwtException | UnsupportedJwtException e) { throw new UnauthorizedException(ErrorCode.INVALID_AUTH_TOKEN); + } catch (Exception e) { + log.error("JWT 토큰 파싱 중에 문제가 발생했습니다."); + throw e; } } } diff --git a/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2UserInfoErrorHandler.java b/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2UserInfoErrorHandler.java index 044f4d408..f553f8bb5 100644 --- a/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2UserInfoErrorHandler.java +++ b/backend/src/main/java/com/festago/auth/infrastructure/KakaoOAuth2UserInfoErrorHandler.java @@ -4,10 +4,12 @@ import com.festago.common.exception.ErrorCode; import com.festago.common.exception.InternalServerException; import java.io.IOException; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatusCode; import org.springframework.http.client.ClientHttpResponse; import org.springframework.web.client.DefaultResponseErrorHandler; +@Slf4j public class KakaoOAuth2UserInfoErrorHandler extends DefaultResponseErrorHandler { @Override @@ -15,6 +17,7 @@ public void handleError(ClientHttpResponse response) throws IOException { HttpStatusCode statusCode = response.getStatusCode(); handle4xxError(statusCode); handle5xxError(statusCode); + log.error("카카오 OAuth2 요청 중 알 수 없는 문제가 발생했습니다."); throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); } @@ -26,6 +29,7 @@ private void handle4xxError(HttpStatusCode statusCode) { private void handle5xxError(HttpStatusCode statusCode) { if (statusCode.is5xxServerError()) { + log.warn("카카오 OAuth2 요청에 500 에러가 발생했습니다."); throw new InternalServerException(ErrorCode.OAUTH2_PROVIDER_NOT_RESPONSE); } } diff --git a/backend/src/main/java/com/festago/common/aop/LogRequestBodyAspect.java b/backend/src/main/java/com/festago/common/aop/LogRequestBodyAspect.java index e00ad0e9e..90aa094b7 100644 --- a/backend/src/main/java/com/festago/common/aop/LogRequestBodyAspect.java +++ b/backend/src/main/java/com/festago/common/aop/LogRequestBodyAspect.java @@ -1,8 +1,6 @@ package com.festago.common.aop; import com.fasterxml.jackson.databind.ObjectMapper; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; import java.lang.reflect.Method; @@ -10,6 +8,7 @@ import java.util.Map; import java.util.Objects; import java.util.function.BiConsumer; +import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -22,6 +21,7 @@ import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.util.ContentCachingRequestWrapper; +@Slf4j @Component @Aspect public class LogRequestBodyAspect { @@ -86,7 +86,8 @@ private String getRequestPayload(HttpServletRequest request) { ContentCachingRequestWrapper cachedRequest = (ContentCachingRequestWrapper) request; return objectMapper.readTree(cachedRequest.getContentAsByteArray()).toPrettyString(); } catch (IOException | ClassCastException e) { - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + log.warn("ObjectMapper에서 직렬화 중에 문제가 발생했습니다.", e); } + return "[ObjectMapper에서 직렬화 중에 문제가 발생했습니다.]"; } } diff --git a/backend/src/main/java/com/festago/common/exception/ErrorCode.java b/backend/src/main/java/com/festago/common/exception/ErrorCode.java index 9c57b4cab..5da1c9a7d 100644 --- a/backend/src/main/java/com/festago/common/exception/ErrorCode.java +++ b/backend/src/main/java/com/festago/common/exception/ErrorCode.java @@ -9,14 +9,13 @@ public enum ErrorCode { INVALID_ENTRY_CODE("올바르지 않은 입장코드입니다."), INVALID_TICKET_OPEN_TIME("티켓 오픈 시간은 공연 시작 이전 이어야 합니다."), INVALID_STAGE_START_TIME("공연은 축제 기간 중에만 진행될 수 있습니다."), - INVALID_MIN_TICKET_AMOUNT("티켓은 적어도 한장 이상 발급해야합니다."), LATE_TICKET_ENTRY_TIME("입장 시간은 공연 시간보다 빨라야합니다."), EARLY_TICKET_ENTRY_TIME("입장 시간은 공연 시작 12시간 이내여야 합니다."), EARLY_TICKET_ENTRY_THAN_OPEN("입장 시간은 티켓 오픈 시간 이후여야합니다."), TICKET_SOLD_OUT("매진된 티켓입니다."), INVALID_FESTIVAL_DURATION("축제 시작 일은 종료일 이전이어야 합니다."), - INVALID_FESTIVAL_START_DATE("축제 시작 일은 과거일 수 없습니다."), - INVALID_TICKET_CREATE_TIME("티켓 오픈 시간 이후 새롭게 티켓을 발급할 수 없습니다."), + INVALID_FESTIVAL_START_DATE("축제 시작 일자는 과거일 수 없습니다."), + INVALID_TICKET_CREATE_TIME("티켓 예매 시작 후 새롭게 티켓을 발급할 수 없습니다."), OAUTH2_NOT_SUPPORTED_SOCIAL_TYPE("해당 OAuth2 제공자는 지원되지 않습니다."), RESERVE_TICKET_OVER_AMOUNT("예매 가능한 수량을 초과했습니다."), NEED_STUDENT_VERIFICATION("학생 인증이 필요합니다."), @@ -29,6 +28,8 @@ public enum ErrorCode { DELETE_CONSTRAINT_STAGE("티켓이 등록된 공연은 삭제할 수 없습니다."), DELETE_CONSTRAINT_SCHOOL("학생 또는 축제에 등록된 학교는 삭제할 수 없습니다."), DUPLICATE_SCHOOL("이미 존재하는 학교 정보입니다."), + VALIDATION_FAIL("검증이 실패하였습니다."), + INVALID_FESTIVAL_FILTER("유효하지 않은 축제의 필터 값입니다."), // 401 @@ -55,18 +56,11 @@ public enum ErrorCode { // 500 INTERNAL_SERVER_ERROR("서버 내부에 문제가 발생했습니다."), - INVALID_ENTRY_CODE_PERIOD("올바르지 않은 입장코드 유효기간입니다."), - INVALID_ENTRY_CODE_EXPIRATION_TIME("올바르지 않은 입장코드 만료 일자입니다."), - INVALID_ENTRY_STATE_INDEX("올바르지 않은 입장상태 인덱스입니다."), - INVALID_ENTRY_CODE_PAYLOAD("유효하지 않은 입장코드 payload 입니다."), - INVALID_AUTH_TOKEN_PAYLOAD("유효하지 않은 로그인 토큰 payload 입니다."), - DUPLICATE_SOCIAL_TYPE("중복된 OAuth2 제공자 입니다."), OAUTH2_PROVIDER_NOT_RESPONSE("OAuth2 제공자 서버에 문제가 발생했습니다."), - INVALID_ENTRY_CODE_OFFSET("올바르지 않은 입장코드 오프셋입니다."), - INVALID_ROLE_NAME("해당하는 Role이 없습니다."), FOR_TEST_ERROR("테스트용 에러입니다."), FAIL_SEND_FCM_MESSAGE("FCM Message 전송에 실패했습니다."), - FCM_NOT_FOUND("유효하지 않은 MemberFCM 이 감지 되었습니다."); + FCM_NOT_FOUND("유효하지 않은 MemberFCM이 감지 되었습니다."), + ; private final String message; diff --git a/backend/src/main/java/com/festago/common/exception/FestaGoException.java b/backend/src/main/java/com/festago/common/exception/FestaGoException.java index 4bbdab694..0c3baf81c 100644 --- a/backend/src/main/java/com/festago/common/exception/FestaGoException.java +++ b/backend/src/main/java/com/festago/common/exception/FestaGoException.java @@ -16,6 +16,11 @@ protected FestaGoException(ErrorCode errorCode, Throwable cause) { this.errorCode = errorCode; } + protected FestaGoException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + public ErrorCode getErrorCode() { return errorCode; } diff --git a/backend/src/main/java/com/festago/common/exception/UnexpectedException.java b/backend/src/main/java/com/festago/common/exception/UnexpectedException.java new file mode 100644 index 000000000..a5414da39 --- /dev/null +++ b/backend/src/main/java/com/festago/common/exception/UnexpectedException.java @@ -0,0 +1,8 @@ +package com.festago.common.exception; + +public class UnexpectedException extends FestaGoException { + + public UnexpectedException(String message) { + super(ErrorCode.INTERNAL_SERVER_ERROR, message); + } +} diff --git a/backend/src/main/java/com/festago/common/exception/ValidException.java b/backend/src/main/java/com/festago/common/exception/ValidException.java new file mode 100644 index 000000000..08154be5b --- /dev/null +++ b/backend/src/main/java/com/festago/common/exception/ValidException.java @@ -0,0 +1,8 @@ +package com.festago.common.exception; + +public class ValidException extends FestaGoException { + + public ValidException(String message) { + super(ErrorCode.VALIDATION_FAIL, message); + } +} diff --git a/backend/src/main/java/com/festago/common/exception/dto/ErrorResponse.java b/backend/src/main/java/com/festago/common/exception/dto/ErrorResponse.java index a15d330fc..bf25a4b50 100644 --- a/backend/src/main/java/com/festago/common/exception/dto/ErrorResponse.java +++ b/backend/src/main/java/com/festago/common/exception/dto/ErrorResponse.java @@ -2,6 +2,7 @@ import com.festago.common.exception.ErrorCode; import com.festago.common.exception.FestaGoException; +import com.festago.common.exception.ValidException; import java.util.List; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -21,6 +22,10 @@ public static ErrorResponse from(ErrorCode errorCode) { return new ErrorResponse(errorCode, errorCode.getMessage()); } + public static ErrorResponse from(ValidException e) { + return new ErrorResponse(e.getErrorCode(), e.getMessage()); + } + public static ErrorResponse from(ErrorCode errorCode, MethodArgumentNotValidException e) { List fieldErrors = e.getBindingResult().getFieldErrors(); if (fieldErrors.isEmpty()) { diff --git a/backend/src/main/java/com/festago/common/util/Validator.java b/backend/src/main/java/com/festago/common/util/Validator.java index 481a29730..6ae4516aa 100644 --- a/backend/src/main/java/com/festago/common/util/Validator.java +++ b/backend/src/main/java/com/festago/common/util/Validator.java @@ -1,49 +1,162 @@ package com.festago.common.util; +import com.festago.common.exception.UnexpectedException; +import com.festago.common.exception.ValidException; + public final class Validator { private Validator() { } /** - * 문자열의 최대 길이를 검증합니다. null 값은 무시됩니다. 최대 길이가 0 이하이면 예외를 던집니다. 문자열의 길이가 maxLength보다 작거나 같으면 예외를 던지지 않습니다. + * 문자열이 null 또는 공백인지 검사합니다. + * + * @param input 검증할 문자열 + * @param fieldName 예외 메시지에 출력할 필드명 + * @throws ValidException input이 null 또는 공백이면 + */ + public static void hasBlank(String input, String fieldName) { + if (input == null || input.isBlank()) { + throw new ValidException("%s은/는 null 또는 공백이 될 수 없습니다.".formatted(fieldName)); + } + } + + /** + * 문자열의 최대 길이를 검증합니다. null 값은 무시됩니다. 최대 길이가 0 이하이면 예외를 던집니다. 문자열의 길이가 maxLength 이하이면 예외를 던지지 않습니다. * * @param input 검증할 문자열 * @param maxLength 검증할 문자열의 최대 길이 - * @param message 예외 메시지 - * @throws IllegalArgumentException 문자열의 길이가 초과되거나, 최대 길이가 0 이하이면 + * @param fieldName 예외 메시지에 출력할 필드명 + * @throws UnexpectedException 최대 길이가 0 이하이면 + * @throws ValidException input의 길이가 maxLength 초과하면 */ - public static void maxLength(CharSequence input, int maxLength, String message) { + public static void maxLength(CharSequence input, int maxLength, String fieldName) { if (maxLength <= 0) { - throw new IllegalArgumentException("검증 길이는 0보다 커야합니다."); + throw new UnexpectedException("최대 길이는 0 이하일 수 없습니다."); } // avoid NPE if (input == null) { return; } if (input.length() > maxLength) { - throw new IllegalArgumentException(message); + throw new ValidException("%s의 길이는 %d글자 이하여야 합니다.".formatted(fieldName, maxLength)); } } /** - * 문자열의 최소 길이를 검증합니다. null 값은 무시됩니다. 최소 길이가 0 이하이면 예외를 던집니다. 문자열의 길이가 minLength보다 크거나 같으면 예외를 던지지 않습니다. + * 문자열의 최소 길이를 검증합니다. null 값은 무시됩니다. 최소 길이가 0 이하이면 예외를 던집니다. 문자열의 길이가 minLength보다 이상이면 예외를 던지지 않습니다. * * @param input 검증할 문자열 * @param minLength 검증할 문자열의 최소 길이 - * @param message 예외 메시지 - * @throws IllegalArgumentException 문자열의 길이가 작으면, 최대 길이가 0 이하이면 + * @param fieldName 예외 메시지에 출력할 필드명 + * @throws UnexpectedException maxLength가 0 이하이면 + * @throws ValidException input의 길이가 minLength 미만이면 */ - public static void minLength(CharSequence input, int minLength, String message) { + public static void minLength(CharSequence input, int minLength, String fieldName) { if (minLength <= 0) { - throw new IllegalArgumentException("검증 길이는 0보다 커야합니다."); + throw new UnexpectedException("최소 길이는 0 이하일 수 없습니다."); } // avoid NPE if (input == null) { return; } if (input.length() < minLength) { - throw new IllegalArgumentException(message); + throw new ValidException("%s의 길이는 %d글자 이상이어야 합니다.".formatted(fieldName, minLength)); + } + } + + /** + * 객체가 null인지 검사합니다. + * + * @param object 검증할 객체 + * @param fieldName 예외 메시지에 출력할 필드명 + * @throws ValidException object가 null 이면 + */ + public static void notNull(Object object, String fieldName) { + if (object == null) { + throw new ValidException("%s은/는 null이 될 수 없습니다.".formatted(fieldName)); + } + } + + /** + * 값의 최대 값을 검증합니다. + * + * @param value 검증할 값 + * @param maxValue 검증할 값의 최대 값 + * @param fieldName 예외 메시지에 출력할 필드명 + * @throws ValidException value가 maxValue 초과하면 + */ + public static void maxValue(int value, int maxValue, String fieldName) { + if (value > maxValue) { + throw new ValidException("%s은/는 %d 이하여야 합니다.".formatted(fieldName, maxValue)); + } + } + + /** + * 값의 최대 값을 검증합니다. + * + * @param value 검증할 값 + * @param maxValue 검증할 값의 최대 값 + * @param fieldName 예외 메시지에 출력할 필드명 + * @throws ValidException value가 maxValue 초과하면 + */ + public static void maxValue(long value, long maxValue, String fieldName) { + if (value > maxValue) { + throw new ValidException("%s은/는 %d 이하여야 합니다.".formatted(fieldName, maxValue)); + } + } + + /** + * 값의 최소 값을 검증합니다. + * + * @param value 검증할 값 + * @param minValue 검증할 값의 최소 값 + * @param fieldName 예외 메시지에 출력할 필드명 + * @throws ValidException value가 minValue 미만이면 + */ + public static void minValue(int value, int minValue, String fieldName) { + if (value < minValue) { + throw new ValidException("%s은/는 %d 이상이어야 합니다.".formatted(fieldName, minValue)); + } + } + + /** + * 값의 최소 값을 검증합니다. + * + * @param value 검증할 값 + * @param minValue 검증할 값의 최소 값 + * @param fieldName 예외 메시지에 출력할 필드명 + * @throws ValidException value가 minValue 미만이면 + */ + public static void minValue(long value, long minValue, String fieldName) { + if (value < minValue) { + throw new ValidException("%s은/는 %d 이상이어야 합니다.".formatted(fieldName, minValue)); + } + } + + /** + * 값이 음수인지 검증합니다. + * + * @param value 검증할 값 + * @param fieldName 예외 메시지에 출력할 필드명 + * @throws ValidException value가 음수이면 + */ + public static void isNegative(int value, String fieldName) { + if (value < 0) { + throw new ValidException("%s은/는 음수가 될 수 없습니다.".formatted(fieldName)); + } + } + + /** + * 값이 음수인지 검증합니다. + * + * @param value 검증할 값 + * @param fieldName 예외 메시지에 출력할 필드명 + * @throws ValidException value가 음수이면 + */ + public static void isNegative(long value, String fieldName) { + if (value < 0) { + throw new ValidException("%s은/는 음수가 될 수 없습니다.".formatted(fieldName)); } } } diff --git a/backend/src/main/java/com/festago/entry/domain/EntryCode.java b/backend/src/main/java/com/festago/entry/domain/EntryCode.java index c75fde867..98da55c23 100644 --- a/backend/src/main/java/com/festago/entry/domain/EntryCode.java +++ b/backend/src/main/java/com/festago/entry/domain/EntryCode.java @@ -1,7 +1,7 @@ package com.festago.entry.domain; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; +import org.springframework.util.StringUtils; public class EntryCode { @@ -13,18 +13,21 @@ public class EntryCode { private final long offset; public EntryCode(String code, long period, long offset) { - validate(period, offset); + validate(code, period, offset); this.code = code; this.period = period; this.offset = offset; } - private void validate(long period, long offset) { + private void validate(String code, long period, long offset) { + if (!StringUtils.hasText(code)) { + throw new UnexpectedException("code는 빈 값 또는 null이 될 수 없습니다."); + } if (period <= MINIMUM_PERIOD) { - throw new InternalServerException(ErrorCode.INVALID_ENTRY_CODE_PERIOD); + throw new UnexpectedException("period는 0 또는 음수가 될 수 없습니다."); } if (isNegative(offset)) { - throw new InternalServerException(ErrorCode.INVALID_ENTRY_CODE_OFFSET); + throw new UnexpectedException("offset은 음수가 될 수 없습니다."); } } diff --git a/backend/src/main/java/com/festago/entry/domain/EntryCodePayload.java b/backend/src/main/java/com/festago/entry/domain/EntryCodePayload.java index 38ff7c411..16762a93d 100644 --- a/backend/src/main/java/com/festago/entry/domain/EntryCodePayload.java +++ b/backend/src/main/java/com/festago/entry/domain/EntryCodePayload.java @@ -1,7 +1,6 @@ package com.festago.entry.domain; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; import com.festago.ticketing.domain.EntryState; import com.festago.ticketing.domain.MemberTicket; @@ -16,16 +15,19 @@ public EntryCodePayload(Long memberTicketId, EntryState entryState) { this.entryState = entryState; } - public static EntryCodePayload from(MemberTicket memberTicket) { - return new EntryCodePayload(memberTicket.getId(), memberTicket.getEntryState()); - } - private void validate(Long memberTicketId, EntryState entryState) { - if (memberTicketId == null || entryState == null) { - throw new InternalServerException(ErrorCode.INVALID_ENTRY_CODE_PAYLOAD); + if (memberTicketId == null) { + throw new UnexpectedException("memberTicketId는 null이 될 수 없습니다."); + } + if (entryState == null) { + throw new UnexpectedException("entryState는 null이 될 수 없습니다."); } } + public static EntryCodePayload from(MemberTicket memberTicket) { + return new EntryCodePayload(memberTicket.getId(), memberTicket.getEntryState()); + } + public Long getMemberTicketId() { return memberTicketId; } diff --git a/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeExtractor.java b/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeExtractor.java index dbc0bf63c..09c4ed1f0 100644 --- a/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeExtractor.java +++ b/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeExtractor.java @@ -7,13 +7,17 @@ import com.festago.ticketing.domain.EntryState; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; import java.nio.charset.StandardCharsets; import javax.crypto.SecretKey; +import lombok.extern.slf4j.Slf4j; +@Slf4j public class JwtEntryCodeExtractor implements EntryCodeExtractor { private static final String MEMBER_TICKET_ID_KEY = "ticketId"; @@ -43,8 +47,11 @@ private Claims getClaims(String code) { .getBody(); } catch (ExpiredJwtException e) { throw new BadRequestException(ErrorCode.EXPIRED_ENTRY_CODE); - } catch (JwtException | IllegalArgumentException e) { + } catch (SignatureException | IllegalArgumentException | MalformedJwtException | UnsupportedJwtException e) { throw new BadRequestException(ErrorCode.INVALID_ENTRY_CODE); + } catch (Exception e) { + log.error("JWT 토큰 파싱 중에 문제가 발생했습니다."); + throw e; } } } diff --git a/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeProvider.java b/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeProvider.java index 852915e6c..48458f345 100644 --- a/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeProvider.java +++ b/backend/src/main/java/com/festago/entry/infrastructure/JwtEntryCodeProvider.java @@ -1,7 +1,6 @@ package com.festago.entry.infrastructure; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; import com.festago.entry.application.EntryCodeProvider; import com.festago.entry.domain.EntryCodePayload; import io.jsonwebtoken.Jwts; @@ -36,7 +35,7 @@ public String provide(EntryCodePayload entryCodePayload, Date expiredAt) { private void validate(Date expiredAt) { if (expiredAt.before(new Date())) { - throw new InternalServerException(ErrorCode.INVALID_ENTRY_CODE_EXPIRATION_TIME); + throw new UnexpectedException("입장코드 만료일자는 과거일 수 없습니다."); } } } diff --git a/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java b/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java index 8a97f7f07..852f9b615 100644 --- a/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java +++ b/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java @@ -1,17 +1,21 @@ package com.festago.fcm.domain; import com.festago.common.domain.BaseTimeEntity; +import com.festago.common.util.Validator; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; @Entity @Table(name = "member_fcm") public class MemberFCM extends BaseTimeEntity { + private static final int MAX_FCM_TOKEN_LENGTH = 255; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -20,6 +24,7 @@ public class MemberFCM extends BaseTimeEntity { private Long memberId; @NotNull + @Size(max = MAX_FCM_TOKEN_LENGTH) private String fcmToken; protected MemberFCM() { @@ -37,9 +42,18 @@ public MemberFCM(Long id, Long memberId, String fcmToken) { } private void validate(Long memberId, String fcmToken) { - if (memberId == null || fcmToken == null) { - throw new IllegalArgumentException("MemberFCM 은 허용되지 않은 null 값으로 생성할 수 없습니다."); - } + validateMemberId(memberId); + validateFcmToken(fcmToken); + } + + private void validateMemberId(Long memberId) { + Validator.notNull(memberId, "memberId"); + } + + private void validateFcmToken(String fcmToken) { + String fieldName = "fcmToken"; + Validator.hasBlank(fcmToken, fieldName); + Validator.maxLength(fcmToken, MAX_FCM_TOKEN_LENGTH, fieldName); } public Long getId() { diff --git a/backend/src/main/java/com/festago/festival/application/FestivalService.java b/backend/src/main/java/com/festago/festival/application/FestivalService.java index 4fe7c285f..5b8185718 100644 --- a/backend/src/main/java/com/festago/festival/application/FestivalService.java +++ b/backend/src/main/java/com/festago/festival/application/FestivalService.java @@ -11,6 +11,7 @@ import com.festago.festival.dto.FestivalResponse; import com.festago.festival.dto.FestivalUpdateRequest; import com.festago.festival.dto.FestivalsResponse; +import com.festago.festival.repository.FestivalFilter; import com.festago.festival.repository.FestivalRepository; import com.festago.school.domain.School; import com.festago.school.repository.SchoolRepository; @@ -19,8 +20,8 @@ import java.time.Clock; import java.time.LocalDate; import java.util.List; -import org.springframework.dao.DataIntegrityViolationException; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -49,8 +50,8 @@ private void validate(Festival festival) { } @Transactional(readOnly = true) - public FestivalsResponse findAll() { - List festivals = festivalRepository.findAll(); + public FestivalsResponse findFestivals(FestivalFilter festivalFilter) { + List festivals = festivalRepository.findAll(festivalFilter.getSpecification(LocalDate.now(clock))); return FestivalsResponse.from(festivals); } diff --git a/backend/src/main/java/com/festago/festival/domain/Festival.java b/backend/src/main/java/com/festago/festival/domain/Festival.java index 9b0522c08..1775f2b7a 100644 --- a/backend/src/main/java/com/festago/festival/domain/Festival.java +++ b/backend/src/main/java/com/festago/festival/domain/Festival.java @@ -1,6 +1,8 @@ package com.festago.festival.domain; import com.festago.common.domain.BaseTimeEntity; +import com.festago.common.exception.BadRequestException; +import com.festago.common.exception.ErrorCode; import com.festago.common.util.Validator; import com.festago.school.domain.School; import jakarta.persistence.Entity; @@ -13,7 +15,6 @@ import jakarta.validation.constraints.Size; import java.time.LocalDate; import java.time.LocalDateTime; -import org.springframework.util.Assert; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -22,13 +23,15 @@ public class Festival extends BaseTimeEntity { private static final String DEFAULT_THUMBNAIL = "https://picsum.photos/536/354"; + private static final int MAX_NAME_LENGTH = 50; + private static final int MAX_THUMBNAIL_LENGTH = 255; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotNull - @Size(max = 50) + @Size(max = MAX_NAME_LENGTH) private String name; @NotNull @@ -38,7 +41,7 @@ public class Festival extends BaseTimeEntity { private LocalDate endDate; @NotNull - @Size(max = 255) + @Size(max = MAX_THUMBNAIL_LENGTH) private String thumbnail; @NotNull @@ -70,20 +73,22 @@ private void validate(String name, LocalDate startDate, LocalDate endDate, Strin } private void validateName(String name) { - Assert.notNull(name, "name은 null 값이 될 수 없습니다."); - Validator.maxLength(name, 50, "name은 50글자를 넘을 수 없습니다."); + String fieldName = "name"; + Validator.hasBlank(name, fieldName); + Validator.maxLength(name, MAX_NAME_LENGTH, fieldName); } private void validateThumbnail(String thumbnail) { - Assert.notNull(thumbnail, "thumbnail은 null 값이 될 수 없습니다."); - Validator.maxLength(thumbnail, 255, "thumbnail은 50글자를 넘을 수 없습니다."); + String fieldName = "thumbnail"; + Validator.hasBlank(thumbnail, fieldName); + Validator.maxLength(thumbnail, MAX_THUMBNAIL_LENGTH, fieldName); } private void validateDate(LocalDate startDate, LocalDate endDate) { - Assert.notNull(startDate, "startDate는 null 값이 될 수 없습니다."); - Assert.notNull(endDate, "endDate는 null 값이 될 수 없습니다."); + Validator.notNull(startDate, "startDate"); + Validator.notNull(endDate, "endDate"); if (startDate.isAfter(endDate)) { - throw new IllegalArgumentException("축제 시작 일은 종료일 이전이어야 합니다."); + throw new BadRequestException(ErrorCode.INVALID_FESTIVAL_DURATION); } } diff --git a/backend/src/main/java/com/festago/festival/repository/FestivalFilter.java b/backend/src/main/java/com/festago/festival/repository/FestivalFilter.java new file mode 100644 index 000000000..81f60d250 --- /dev/null +++ b/backend/src/main/java/com/festago/festival/repository/FestivalFilter.java @@ -0,0 +1,33 @@ +package com.festago.festival.repository; + +import com.festago.common.exception.BadRequestException; +import com.festago.common.exception.ErrorCode; +import com.festago.festival.domain.Festival; +import java.time.LocalDate; +import java.util.function.Function; +import org.springframework.data.jpa.domain.Specification; + +public enum FestivalFilter { + PROGRESS(FestivalSpecification::progress), + PLANNED(FestivalSpecification::planned), + END(FestivalSpecification::end); + + private final Function> filter; + + FestivalFilter(Function> filter) { + this.filter = filter; + } + + public static FestivalFilter from(String filterName) { + return switch (filterName.toUpperCase()) { + case "PROGRESS" -> PROGRESS; + case "PLANNED" -> PLANNED; + case "END" -> END; + default -> throw new BadRequestException(ErrorCode.INVALID_FESTIVAL_FILTER); + }; + } + + public Specification getSpecification(LocalDate currentTime) { + return filter.apply(currentTime); + } +} diff --git a/backend/src/main/java/com/festago/festival/repository/FestivalRepository.java b/backend/src/main/java/com/festago/festival/repository/FestivalRepository.java index 10b3c02c6..032b89a68 100644 --- a/backend/src/main/java/com/festago/festival/repository/FestivalRepository.java +++ b/backend/src/main/java/com/festago/festival/repository/FestivalRepository.java @@ -2,7 +2,8 @@ import com.festago.festival.domain.Festival; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -public interface FestivalRepository extends JpaRepository { +public interface FestivalRepository extends JpaRepository, JpaSpecificationExecutor { } diff --git a/backend/src/main/java/com/festago/festival/repository/FestivalSpecification.java b/backend/src/main/java/com/festago/festival/repository/FestivalSpecification.java new file mode 100644 index 000000000..50e7efb5c --- /dev/null +++ b/backend/src/main/java/com/festago/festival/repository/FestivalSpecification.java @@ -0,0 +1,38 @@ +package com.festago.festival.repository; + +import com.festago.festival.domain.Festival; +import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.data.jpa.domain.Specification; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class FestivalSpecification { + + private static final String START_DATE = "startDate"; + private static final String END_DATE = "endDate"; + + public static Specification progress(LocalDate currentTime) { + return (root, query, criteriaBuilder) -> { + query.orderBy(criteriaBuilder.asc(root.get(START_DATE))); + return criteriaBuilder.and( + criteriaBuilder.lessThanOrEqualTo(root.get(START_DATE), currentTime), + criteriaBuilder.greaterThanOrEqualTo(root.get(END_DATE), currentTime) + ); + }; + } + + public static Specification planned(LocalDate currentTime) { + return (root, query, criteriaBuilder) -> { + query.orderBy(criteriaBuilder.asc(root.get(START_DATE))); + return criteriaBuilder.greaterThan(root.get(START_DATE), currentTime); + }; + } + + public static Specification end(LocalDate currentTime) { + return (root, query, criteriaBuilder) -> { + query.orderBy(criteriaBuilder.desc(root.get(END_DATE))); + return criteriaBuilder.lessThan(root.get(END_DATE), currentTime); + }; + } +} diff --git a/backend/src/main/java/com/festago/member/domain/Member.java b/backend/src/main/java/com/festago/member/domain/Member.java index 2ee68e879..a7f0e3378 100644 --- a/backend/src/main/java/com/festago/member/domain/Member.java +++ b/backend/src/main/java/com/festago/member/domain/Member.java @@ -2,6 +2,7 @@ import com.festago.auth.domain.SocialType; import com.festago.common.domain.BaseTimeEntity; +import com.festago.common.util.Validator; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -17,6 +18,7 @@ import lombok.NoArgsConstructor; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; +import org.springframework.util.StringUtils; @Entity @SQLDelete(sql = "UPDATE member SET deleted_at = now(), nickname = '탈퇴한 회원', profile_image = '', social_id = null WHERE id=?") @@ -36,12 +38,15 @@ public class Member extends BaseTimeEntity { private static final String DEFAULT_IMAGE_URL = "https://festa-go.site/images/default-profile.png"; + private static final int MAX_SOCIAL_ID_LENGTH = 255; + private static final int MAX_NICKNAME_LENGTH = 30; + private static final int MAX_PROFILE_IMAGE_LENGTH = 255; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Size(max = 255) + @Size(max = MAX_SOCIAL_ID_LENGTH) private String socialId; @NotNull @@ -49,11 +54,11 @@ public class Member extends BaseTimeEntity { private SocialType socialType; @NotNull - @Size(max = 30) + @Size(max = MAX_NICKNAME_LENGTH) private String nickname; @NotNull - @Size(max = 255) + @Size(max = MAX_PROFILE_IMAGE_LENGTH) private String profileImage; private LocalDateTime deletedAt = null; @@ -72,35 +77,34 @@ public Member(Long id, String socialId, SocialType socialType, String nickname, this.socialId = socialId; this.socialType = socialType; this.nickname = nickname; - this.profileImage = (profileImage != null) ? profileImage : DEFAULT_IMAGE_URL; + this.profileImage = (StringUtils.hasText(profileImage)) ? profileImage : DEFAULT_IMAGE_URL; } private void validate(String socialId, SocialType socialType, String nickname, String profileImage) { - checkNotNull(socialId, socialType, nickname); - checkLength(socialId, nickname, profileImage); + validateSocialId(socialId); + validateSocialType(socialType); + validateNickname(nickname); + validateProfileImage(profileImage); } - private void checkNotNull(String socialId, SocialType socialType, String nickname) { - if (socialId == null || - socialType == null || - nickname == null) { - throw new IllegalArgumentException("Member 는 허용되지 않은 null 값으로 생성할 수 없습니다."); - } + private void validateSocialId(String socialId) { + String fieldName = "socialId"; + Validator.hasBlank(socialId, fieldName); + Validator.maxLength(socialId, MAX_SOCIAL_ID_LENGTH, fieldName); } - private void checkLength(String socialId, String nickname, String profileImage) { - if (overLength(socialId, 255) || - overLength(nickname, 30) || - overLength(profileImage, 255)) { - throw new IllegalArgumentException("Member 의 필드로 허용된 길이를 넘은 column 을 넣을 수 없습니다."); - } + private void validateSocialType(SocialType socialType) { + Validator.notNull(socialType, "socialType"); } - private boolean overLength(String target, int maxLength) { - if (target == null) { - return false; - } - return target.length() > maxLength; + private void validateNickname(String nickname) { + String fieldName = "nickname"; + Validator.hasBlank(nickname, fieldName); + Validator.maxLength(nickname, MAX_NICKNAME_LENGTH, fieldName); + } + + private void validateProfileImage(String profileImage) { + Validator.maxLength(profileImage, MAX_PROFILE_IMAGE_LENGTH, "profileImage"); } public Long getId() { diff --git a/backend/src/main/java/com/festago/presentation/AdminController.java b/backend/src/main/java/com/festago/presentation/AdminController.java index adb63cff4..62f74a362 100644 --- a/backend/src/main/java/com/festago/presentation/AdminController.java +++ b/backend/src/main/java/com/festago/presentation/AdminController.java @@ -1,7 +1,5 @@ package com.festago.presentation; -import com.festago.admin.application.AdminService; -import com.festago.admin.dto.AdminResponse; import com.festago.auth.annotation.Admin; import com.festago.auth.application.AdminAuthService; import com.festago.auth.dto.AdminLoginRequest; @@ -54,7 +52,6 @@ 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 SchoolService schoolService; private final Optional properties; @@ -147,13 +144,6 @@ private String getCookie(String token) { .build().toString(); } - @GetMapping("/data") - public ResponseEntity adminData() { - AdminResponse response = adminService.getAdminResponse(); - return ResponseEntity.ok() - .body(response); - } - @GetMapping("/version") public ResponseEntity getVersion() { return properties.map(it -> ResponseEntity.ok(it.getTime().atZone(ZoneId.of("Asia/Seoul")).toString())) diff --git a/backend/src/main/java/com/festago/presentation/AdminViewController.java b/backend/src/main/java/com/festago/presentation/AdminViewController.java index 994e2e614..eba8e66c7 100644 --- a/backend/src/main/java/com/festago/presentation/AdminViewController.java +++ b/backend/src/main/java/com/festago/presentation/AdminViewController.java @@ -60,7 +60,6 @@ public String manageStagePage(@PathVariable String stageId) { @ExceptionHandler(UnauthorizedException.class) public View handle(UnauthorizedException e, HttpServletResponse response) { - if (e.getErrorCode() == ErrorCode.EXPIRED_AUTH_TOKEN) { return new RedirectView("/admin/login"); } diff --git a/backend/src/main/java/com/festago/presentation/FestivalController.java b/backend/src/main/java/com/festago/presentation/FestivalController.java index 67b9c94fd..197914043 100644 --- a/backend/src/main/java/com/festago/presentation/FestivalController.java +++ b/backend/src/main/java/com/festago/presentation/FestivalController.java @@ -3,6 +3,7 @@ import com.festago.festival.application.FestivalService; import com.festago.festival.dto.FestivalDetailResponse; import com.festago.festival.dto.FestivalsResponse; +import com.festago.festival.repository.FestivalFilter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -10,6 +11,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -21,9 +23,10 @@ public class FestivalController { private final FestivalService festivalService; @GetMapping - @Operation(description = "모든 축제들을 조회한다.", summary = "축제 목록 조회") - public ResponseEntity findAll() { - FestivalsResponse response = festivalService.findAll(); + @Operation(description = "축제를 조건별로 조회한다. PROGRESS: 진행 중, PLANNED: 진행 예정, END: 종료, 기본값 -> 진행 중", summary = "축제 목록 조회") + public ResponseEntity findFestivals( + @RequestParam(defaultValue = "PROGRESS") String festivalFilter) { + FestivalsResponse response = festivalService.findFestivals(FestivalFilter.from(festivalFilter)); return ResponseEntity.ok() .body(response); } diff --git a/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java b/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java index 8c13f7a10..943b39657 100644 --- a/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java @@ -8,6 +8,8 @@ import com.festago.common.exception.NotFoundException; import com.festago.common.exception.TooManyRequestException; import com.festago.common.exception.UnauthorizedException; +import com.festago.common.exception.UnexpectedException; +import com.festago.common.exception.ValidException; import com.festago.common.exception.dto.ErrorResponse; import com.festago.presentation.auth.AuthenticateContext; import jakarta.servlet.http.HttpServletRequest; @@ -29,7 +31,7 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { private static final String LOG_FORMAT_INFO = "\n[🔵INFO] - ({} {})\n(id: {}, role: {})\n{}\n {}: {}"; - private static final String LOG_FORMAT_WARN = "\n[🟠WARN] - ({} {})\n(id: {}, role: {})\n{}\n {}: {}"; + private static final String LOG_FORMAT_WARN = "\n[🟠WARN] - ({} {})\n(id: {}, role: {})"; private static final String LOG_FORMAT_ERROR = "\n[🔴ERROR] - ({} {})\n(id: {}, role: {})"; // INFO /* @@ -45,6 +47,7 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { (id: 1, role: MEMBER) FOR_TEST_ERROR com.festago.exception.InternalServerException: 테스트용 에러입니다. + at com.festago.presentation.AdminController.getWarn(AdminController.java:129) */ // ERROR @@ -63,6 +66,16 @@ public ResponseEntity handle(InvalidMediaTypeException e) { return ResponseEntity.badRequest().build(); } + @ExceptionHandler(ValidException.class) + public ResponseEntity handle(ValidException e) { + return ResponseEntity.badRequest().body(ErrorResponse.from(e)); + } + + @ExceptionHandler(UnexpectedException.class) + public ResponseEntity handle(UnexpectedException e) { + return ResponseEntity.internalServerError().body(ErrorResponse.from(e)); + } + @ExceptionHandler(BadRequestException.class) public ResponseEntity handle(BadRequestException e, HttpServletRequest request) { logInfo(e, request); @@ -124,8 +137,8 @@ private void logInfo(FestaGoException e, HttpServletRequest request) { private void logWarn(FestaGoException e, HttpServletRequest request) { if (errorLogger.isWarnEnabled()) { - errorLogger.warn(LOG_FORMAT_WARN, request.getMethod(), request.getRequestURI(), authenticateContext.getId(), - authenticateContext.getRole(), e.getErrorCode(), e.getClass().getName(), e.getMessage()); + errorLogger.warn(LOG_FORMAT_WARN, request.getMethod(), request.getRequestURI(), + authenticateContext.getId(), authenticateContext.getRole(), e); } } diff --git a/backend/src/main/java/com/festago/presentation/SchoolController.java b/backend/src/main/java/com/festago/presentation/SchoolController.java index 8667e4597..630f4a270 100644 --- a/backend/src/main/java/com/festago/presentation/SchoolController.java +++ b/backend/src/main/java/com/festago/presentation/SchoolController.java @@ -3,6 +3,8 @@ import com.festago.school.application.SchoolService; import com.festago.school.dto.SchoolResponse; import com.festago.school.dto.SchoolsResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -12,18 +14,21 @@ @RestController @RequestMapping("/schools") +@Tag(name = "학교 정보 요청") @RequiredArgsConstructor public class SchoolController { private final SchoolService schoolService; @GetMapping + @Operation(description = "모든 학교 정보를 조회한다.", summary = "전체 학교 조회") public ResponseEntity findAll() { return ResponseEntity.ok() .body(schoolService.findAll()); } @GetMapping("/{schoolId}") + @Operation(description = "단일 학교 정보를 조회한다.", summary = "단일 학교 조회") public ResponseEntity findById(@PathVariable Long schoolId) { return ResponseEntity.ok() .body(schoolService.findById(schoolId)); diff --git a/backend/src/main/java/com/festago/presentation/StudentController.java b/backend/src/main/java/com/festago/presentation/StudentController.java index 96c19f53d..5af069f7e 100644 --- a/backend/src/main/java/com/festago/presentation/StudentController.java +++ b/backend/src/main/java/com/festago/presentation/StudentController.java @@ -2,6 +2,7 @@ import com.festago.auth.annotation.Member; import com.festago.student.application.StudentService; +import com.festago.student.dto.StudentResponse; import com.festago.student.dto.StudentSendMailRequest; import com.festago.student.dto.StudentVerificateRequest; import io.swagger.v3.oas.annotations.Operation; @@ -9,6 +10,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +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; @@ -33,10 +35,18 @@ public ResponseEntity sendEmail(@Member Long memberId, @PostMapping("/verification") @Operation(description = "학교 인증을 수행한다.", summary = "학생 인증 수행") - public ResponseEntity verificate(@Member Long memberId, - @RequestBody @Valid StudentVerificateRequest request) { - studentService.verificate(memberId, request); + public ResponseEntity verify(@Member Long memberId, + @RequestBody @Valid StudentVerificateRequest request) { + studentService.verify(memberId, request); return ResponseEntity.ok() .build(); } + + @GetMapping + @Operation(description = "학생 인증 정보를 조회한다.", summary = "학생 인증 정보 조회") + public ResponseEntity findVerification(@Member Long memberId) { + StudentResponse response = studentService.findVerification(memberId); + return ResponseEntity.ok() + .body(response); + } } diff --git a/backend/src/main/java/com/festago/school/domain/School.java b/backend/src/main/java/com/festago/school/domain/School.java index a261c838e..f9bcbae33 100644 --- a/backend/src/main/java/com/festago/school/domain/School.java +++ b/backend/src/main/java/com/festago/school/domain/School.java @@ -11,23 +11,25 @@ import jakarta.validation.constraints.Size; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import org.springframework.util.Assert; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) public class School extends BaseTimeEntity { + private static final int MAX_DOMAIN_LENGTH = 50; + private static final int MAX_NAME_LENGTH = 255; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotNull - @Size(max = 50) + @Size(max = MAX_DOMAIN_LENGTH) @Column(unique = true) private String domain; @NotNull - @Size(max = 255) + @Size(max = MAX_NAME_LENGTH) @Column(unique = true) private String name; @@ -48,13 +50,15 @@ private void validate(String domain, String name) { } private void validateDomain(String domain) { - Assert.notNull(domain, "domain은 null 값이 될 수 없습니다."); - Validator.maxLength(domain, 50, "domain은 50글자를 넘을 수 없습니다."); + String fieldName = "domain"; + Validator.hasBlank(domain, fieldName); + Validator.maxLength(domain, MAX_DOMAIN_LENGTH, fieldName); } private void validateName(String name) { - Assert.notNull(name, "name은 null 값이 될 수 없습니다."); - Validator.maxLength(name, 255, "name은 255글자를 넘을 수 없습니다."); + String fieldName = "name"; + Validator.hasBlank(name, fieldName); + Validator.maxLength(name, MAX_NAME_LENGTH, fieldName); } public void changeDomain(String domain) { diff --git a/backend/src/main/java/com/festago/stage/domain/Stage.java b/backend/src/main/java/com/festago/stage/domain/Stage.java index 5c5cea7d7..6f0ffd4ea 100644 --- a/backend/src/main/java/com/festago/stage/domain/Stage.java +++ b/backend/src/main/java/com/festago/stage/domain/Stage.java @@ -20,12 +20,13 @@ import java.util.List; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import org.springframework.util.Assert; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Stage extends BaseTimeEntity { + private static final int MAX_LINEUP_LENGTH = 255; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -33,7 +34,7 @@ public class Stage extends BaseTimeEntity { @NotNull private LocalDateTime startTime; - @Size(max = 255) + @Size(max = MAX_LINEUP_LENGTH) private String lineUp; @NotNull @@ -71,18 +72,18 @@ private void validate(LocalDateTime startTime, String lineUp, LocalDateTime tick } private void validateLineUp(String lineUp) { - Validator.maxLength(lineUp, 255, "lineUp은 50글자를 넘을 수 없습니다."); + Validator.maxLength(lineUp, MAX_LINEUP_LENGTH, "lineUp"); } private void validateFestival(Festival festival) { - Assert.notNull(festival, "festival은 null 값이 될 수 없습니다."); + Validator.notNull(festival, "festival"); } private void validateTime(LocalDateTime startTime, LocalDateTime ticketOpenTime, Festival festival) { - Assert.notNull(startTime, "startTime은 null 값이 될 수 없습니다."); - Assert.notNull(ticketOpenTime, "ticketOpenTime은 null 값이 될 수 없습니다."); + Validator.notNull(startTime, "startTime"); + Validator.notNull(ticketOpenTime, "ticketOpenTime"); if (ticketOpenTime.isAfter(startTime) || ticketOpenTime.isEqual(startTime)) { - throw new IllegalArgumentException("티켓 오픈 시간은 공연 시작 이전 이어야 합니다."); + throw new BadRequestException(ErrorCode.INVALID_TICKET_OPEN_TIME); } if (festival.isNotInDuration(startTime)) { throw new BadRequestException(ErrorCode.INVALID_STAGE_START_TIME); diff --git a/backend/src/main/java/com/festago/student/application/StudentService.java b/backend/src/main/java/com/festago/student/application/StudentService.java index 82c1ba037..6d0b9104f 100644 --- a/backend/src/main/java/com/festago/student/application/StudentService.java +++ b/backend/src/main/java/com/festago/student/application/StudentService.java @@ -12,6 +12,7 @@ import com.festago.student.domain.StudentCode; import com.festago.student.domain.VerificationCode; import com.festago.student.domain.VerificationMailPayload; +import com.festago.student.dto.StudentResponse; import com.festago.student.dto.StudentSendMailRequest; import com.festago.student.dto.StudentVerificateRequest; import com.festago.student.repository.StudentCodeRepository; @@ -89,7 +90,7 @@ private void saveStudentCode(VerificationCode code, Member member, School school ); } - public void verificate(Long memberId, StudentVerificateRequest request) { + public void verify(Long memberId, StudentVerificateRequest request) { validateStudent(memberId); Member member = findMember(memberId); StudentCode studentCode = studentCodeRepository.findByCodeAndMember(new VerificationCode(request.code()), @@ -98,4 +99,10 @@ public void verificate(Long memberId, StudentVerificateRequest request) { studentRepository.save(new Student(member, studentCode.getSchool(), studentCode.getUsername())); studentCodeRepository.deleteByMember(member); } + + public StudentResponse findVerification(Long memberId) { + return studentRepository.findByMemberIdWithFetch(memberId) + .map(value -> StudentResponse.verified(value.getSchool())) + .orElseGet(StudentResponse::notVerified); + } } diff --git a/backend/src/main/java/com/festago/student/domain/Student.java b/backend/src/main/java/com/festago/student/domain/Student.java index b1559d660..7857d6a8d 100644 --- a/backend/src/main/java/com/festago/student/domain/Student.java +++ b/backend/src/main/java/com/festago/student/domain/Student.java @@ -1,8 +1,7 @@ package com.festago.student.domain; import com.festago.common.domain.BaseTimeEntity; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.util.Validator; import com.festago.member.domain.Member; import com.festago.school.domain.School; import jakarta.persistence.Entity; @@ -21,6 +20,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Student extends BaseTimeEntity { + private static final int MAX_USERNAME_LENGTH = 255; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -33,7 +34,7 @@ public class Student extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) private School school; - @Size(max = 255) + @Size(max = MAX_USERNAME_LENGTH) @NotNull private String username; @@ -50,29 +51,23 @@ public Student(Long id, Member member, School school, String username) { } private void validate(Member member, School school, String username) { - checkNotNull(member, school, username); - checkLength(username); + validateMember(member); + validateSchool(school); + validateUsername(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 validateMember(Member member) { + Validator.notNull(member, "member"); } - private void checkLength(String username) { - if (overLength(username, 255)) { - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); - } + private void validateSchool(School school) { + Validator.notNull(school, "school"); } - private boolean overLength(String target, int maxLength) { - if (target == null) { - return false; - } - return target.length() > maxLength; + private void validateUsername(String username) { + String fieldName = "username"; + Validator.hasBlank(username, fieldName); + Validator.maxLength(username, MAX_USERNAME_LENGTH, fieldName); } public Long getId() { diff --git a/backend/src/main/java/com/festago/student/domain/StudentCode.java b/backend/src/main/java/com/festago/student/domain/StudentCode.java index 899993530..6a2468b15 100644 --- a/backend/src/main/java/com/festago/student/domain/StudentCode.java +++ b/backend/src/main/java/com/festago/student/domain/StudentCode.java @@ -2,8 +2,7 @@ import static java.time.temporal.ChronoUnit.SECONDS; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.util.Validator; import com.festago.member.domain.Member; import com.festago.school.domain.School; import jakarta.persistence.Embedded; @@ -17,12 +16,12 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToOne; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import org.springframework.util.StringUtils; @Entity @EntityListeners(AuditingEntityListener.class) @@ -30,6 +29,7 @@ public class StudentCode { private static final int MIN_REQUEST_TERM_SECONDS = 30; + private static final int MAX_USERNAME_LENGTH = 255; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -45,6 +45,8 @@ public class StudentCode { @JoinColumn(unique = true) private Member member; + @NotNull + @Size(max = MAX_USERNAME_LENGTH) private String username; @NotNull @@ -57,7 +59,7 @@ public StudentCode(VerificationCode code, School school, Member member, String u public StudentCode(Long id, VerificationCode code, School school, Member member, String username, LocalDateTime issuedAt) { - validate(username); + validate(code, school, member, username); this.id = id; this.code = code; this.school = school; @@ -66,14 +68,29 @@ public StudentCode(Long id, VerificationCode code, School school, Member member, this.issuedAt = issuedAt; } - private void validate(String username) { - if (!StringUtils.hasText(username)) { - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); - } + private void validate(VerificationCode code, School school, Member member, String username) { + validateVerificationCode(code); + validateSchool(school); + validateMember(member); + validateUsername(username); + } + + private void validateVerificationCode(VerificationCode code) { + Validator.notNull(code, "validateVerificationCode"); + } + + private void validateSchool(School school) { + Validator.notNull(school, "school"); + } + + private void validateMember(Member member) { + Validator.notNull(member, "member"); + } - if (username.length() > 255) { - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); - } + private void validateUsername(String username) { + String fieldName = "username"; + Validator.hasBlank(username, fieldName); + Validator.maxLength(username, MAX_USERNAME_LENGTH, fieldName); } public boolean canReissue(LocalDateTime currentTime) { diff --git a/backend/src/main/java/com/festago/student/domain/VerificationCode.java b/backend/src/main/java/com/festago/student/domain/VerificationCode.java index 4c5f751be..359e6889a 100644 --- a/backend/src/main/java/com/festago/student/domain/VerificationCode.java +++ b/backend/src/main/java/com/festago/student/domain/VerificationCode.java @@ -1,12 +1,12 @@ package com.festago.student.domain; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.util.regex.Pattern; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import org.springframework.util.StringUtils; @Embeddable @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -23,26 +23,26 @@ public VerificationCode(String value) { } private void validate(String value) { - validateNull(value); + validateBlank(value); validateLength(value); validatePositive(value); } - private void validateNull(String value) { - if (value == null) { - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + private void validateBlank(String value) { + if (!StringUtils.hasText(value)) { + throw new UnexpectedException("VerificationCode는 null 또는 공백이 될 수 없습니다."); } } private void validateLength(String value) { if (value.length() != LENGTH) { - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + throw new UnexpectedException("VerificationCode의 길이는 %d 이어야 합니다.".formatted(LENGTH)); } } private void validatePositive(String value) { if (!POSITIVE_REGEX.matcher(value).matches()) { - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + throw new UnexpectedException("VerificationCode는 0~9의 양수 형식이어야 합니다."); } } diff --git a/backend/src/main/java/com/festago/student/domain/VerificationMailPayload.java b/backend/src/main/java/com/festago/student/domain/VerificationMailPayload.java index e6e159d06..6a07dc575 100644 --- a/backend/src/main/java/com/festago/student/domain/VerificationMailPayload.java +++ b/backend/src/main/java/com/festago/student/domain/VerificationMailPayload.java @@ -1,13 +1,14 @@ package com.festago.student.domain; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; +import com.festago.common.exception.ValidException; +import org.springframework.util.StringUtils; public class VerificationMailPayload { - private VerificationCode code; - private String username; - private String domain; + private final VerificationCode code; + private final String username; + private final String domain; public VerificationMailPayload(VerificationCode code, String username, String domain) { validate(code, username, domain); @@ -17,8 +18,14 @@ public VerificationMailPayload(VerificationCode code, String username, String do } private void validate(VerificationCode code, String username, String domain) { - if (code == null || username == null || domain == null) { - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + if (code == null) { + throw new UnexpectedException("code는 null이 될 수 없습니다."); + } + if (!StringUtils.hasText(username)) { + throw new ValidException("username은 null 또는 공백이 될 수 없습니다."); + } + if (!StringUtils.hasText(domain)) { + throw new UnexpectedException("domain은 null 또는 공백이 될 수 없습니다."); } } diff --git a/backend/src/main/java/com/festago/student/dto/StudentResponse.java b/backend/src/main/java/com/festago/student/dto/StudentResponse.java new file mode 100644 index 000000000..e870ae88a --- /dev/null +++ b/backend/src/main/java/com/festago/student/dto/StudentResponse.java @@ -0,0 +1,22 @@ +package com.festago.student.dto; + +import com.festago.school.domain.School; + +public record StudentResponse( + boolean isVerified, + StudentSchoolResponse school +) { + + private static final StudentResponse NOT_VERIFIED = new StudentResponse(false, null); + + public static StudentResponse verified(School school) { + return new StudentResponse( + true, + StudentSchoolResponse.from(school) + ); + } + + public static StudentResponse notVerified() { + return NOT_VERIFIED; + } +} diff --git a/backend/src/main/java/com/festago/student/dto/StudentSchoolResponse.java b/backend/src/main/java/com/festago/student/dto/StudentSchoolResponse.java new file mode 100644 index 000000000..479fc5cd9 --- /dev/null +++ b/backend/src/main/java/com/festago/student/dto/StudentSchoolResponse.java @@ -0,0 +1,18 @@ +package com.festago.student.dto; + +import com.festago.school.domain.School; + +public record StudentSchoolResponse( + Long id, + String schoolName, + String domain +) { + + public static StudentSchoolResponse from(School school) { + return new StudentSchoolResponse( + school.getId(), + school.getName(), + school.getDomain() + ); + } +} diff --git a/backend/src/main/java/com/festago/student/repository/StudentRepository.java b/backend/src/main/java/com/festago/student/repository/StudentRepository.java index f196c95ba..9db2ab470 100644 --- a/backend/src/main/java/com/festago/student/repository/StudentRepository.java +++ b/backend/src/main/java/com/festago/student/repository/StudentRepository.java @@ -2,7 +2,10 @@ import com.festago.member.domain.Member; import com.festago.student.domain.Student; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface StudentRepository extends JpaRepository { @@ -11,4 +14,12 @@ public interface StudentRepository extends JpaRepository { boolean existsByUsernameAndSchoolId(String username, Long id); boolean existsByMemberId(Long id); + + @Query(""" + SELECT st + FROM Student st + INNER JOIN FETCH st.school sc + WHERE st.member.id = :memberId + """) + Optional findByMemberIdWithFetch(@Param("memberId") Long memberId); } diff --git a/backend/src/main/java/com/festago/ticket/application/TicketService.java b/backend/src/main/java/com/festago/ticket/application/TicketService.java index 74e72d5f4..fe9424e52 100644 --- a/backend/src/main/java/com/festago/ticket/application/TicketService.java +++ b/backend/src/main/java/com/festago/ticket/application/TicketService.java @@ -46,6 +46,6 @@ private Stage findStageById(Long stageId) { @Transactional(readOnly = true) public StageTicketsResponse findStageTickets(Long stageId) { - return StageTicketsResponse.from(ticketRepository.findAllByStageId(stageId)); + return StageTicketsResponse.from(ticketRepository.findAllByStageIdWithFetch(stageId)); } } diff --git a/backend/src/main/java/com/festago/ticket/domain/Ticket.java b/backend/src/main/java/com/festago/ticket/domain/Ticket.java index b371db699..6c0e76abb 100644 --- a/backend/src/main/java/com/festago/ticket/domain/Ticket.java +++ b/backend/src/main/java/com/festago/ticket/domain/Ticket.java @@ -3,6 +3,7 @@ import com.festago.common.domain.BaseTimeEntity; import com.festago.common.exception.BadRequestException; import com.festago.common.exception.ErrorCode; +import com.festago.common.util.Validator; import com.festago.member.domain.Member; import com.festago.school.domain.School; import com.festago.stage.domain.Stage; @@ -70,7 +71,7 @@ public Ticket(Long id, Stage stage, TicketType ticketType, School school) { } public Ticket(Long id, Stage stage, TicketType ticketType, Long schoolId) { - validate(stage, ticketType); + validate(stage, ticketType, schoolId); this.id = id; this.stage = stage; this.ticketType = ticketType; @@ -78,15 +79,22 @@ public Ticket(Long id, Stage stage, TicketType ticketType, Long schoolId) { this.schoolId = schoolId; } - private void validate(Stage stage, TicketType ticketType) { - checkNotNull(stage, ticketType); + private void validate(Stage stage, TicketType ticketType, Long schoolId) { + validateStage(stage); + validateTicketType(ticketType); + validateSchoolId(schoolId); } - private void checkNotNull(Stage stage, TicketType ticketType) { - if (stage == null || - ticketType == null) { - throw new IllegalArgumentException("Ticket 은 허용되지 않은 null 값으로 생성할 수 없습니다."); - } + private void validateStage(Stage stage) { + Validator.notNull(stage, "stage"); + } + + private void validateTicketType(TicketType ticketType) { + Validator.notNull(ticketType, "ticketType"); + } + + private void validateSchoolId(Long schoolId) { + Validator.notNull(schoolId, "schoolId"); } public void addTicketEntryTime(LocalDateTime currentTime, LocalDateTime entryTime, int amount) { diff --git a/backend/src/main/java/com/festago/ticket/domain/TicketAmount.java b/backend/src/main/java/com/festago/ticket/domain/TicketAmount.java index 738ccfc6c..9665b4517 100644 --- a/backend/src/main/java/com/festago/ticket/domain/TicketAmount.java +++ b/backend/src/main/java/com/festago/ticket/domain/TicketAmount.java @@ -3,6 +3,7 @@ import com.festago.common.domain.BaseTimeEntity; import com.festago.common.exception.BadRequestException; import com.festago.common.exception.ErrorCode; +import com.festago.common.util.Validator; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.Id; @@ -36,13 +37,7 @@ public TicketAmount(Ticket ticket) { } private void validate(Ticket ticket) { - checkNotNull(ticket); - } - - private void checkNotNull(Ticket ticket) { - if (ticket == null) { - throw new IllegalArgumentException("TicketAmount 는 허용되지 않은 null 값으로 생성할 수 없습니다."); - } + Validator.notNull(ticket, "ticket"); } public void increaseReservedAmount() { diff --git a/backend/src/main/java/com/festago/ticket/domain/TicketEntryTime.java b/backend/src/main/java/com/festago/ticket/domain/TicketEntryTime.java index f26cf7d4b..baf1dbb99 100644 --- a/backend/src/main/java/com/festago/ticket/domain/TicketEntryTime.java +++ b/backend/src/main/java/com/festago/ticket/domain/TicketEntryTime.java @@ -1,8 +1,7 @@ package com.festago.ticket.domain; import com.festago.common.domain.BaseTimeEntity; -import com.festago.common.exception.BadRequestException; -import com.festago.common.exception.ErrorCode; +import com.festago.common.util.Validator; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -17,7 +16,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class TicketEntryTime extends BaseTimeEntity implements Comparable { - private static final int MIN_TOTAL_AMOUNT = 1; + private static final int MIN_AMOUNT_VALUE = 1; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -26,7 +25,7 @@ public class TicketEntryTime extends BaseTimeEntity implements Comparable { - List findAllByStageId(Long stageId); + @Query(""" + SELECT t from Ticket t + INNER JOIN FETCH t.ticketAmount + WHERE t.stage.id = :stageId + """) + List findAllByStageIdWithFetch(@Param("stageId") Long stageId); Optional findByTicketTypeAndStage(TicketType ticketType, Stage stage); diff --git a/backend/src/main/java/com/festago/ticketing/domain/EntryState.java b/backend/src/main/java/com/festago/ticketing/domain/EntryState.java index 76c68a288..a0f172ddc 100644 --- a/backend/src/main/java/com/festago/ticketing/domain/EntryState.java +++ b/backend/src/main/java/com/festago/ticketing/domain/EntryState.java @@ -1,7 +1,6 @@ package com.festago.ticketing.domain; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; public enum EntryState { BEFORE_ENTRY(0), @@ -16,18 +15,18 @@ public enum EntryState { } public static EntryState from(Integer index) { - validate(index); + validateNull(index); return switch (index) { case 0 -> BEFORE_ENTRY; case 1 -> AFTER_ENTRY; case 2 -> AWAY; - default -> throw new InternalServerException(ErrorCode.INVALID_ENTRY_STATE_INDEX); + default -> throw new UnexpectedException("entryState의 인덱스가 올바르지 않습니다. index: " + index); }; } - private static void validate(Integer index) { + private static void validateNull(Integer index) { if (index == null) { - throw new InternalServerException(ErrorCode.INVALID_ENTRY_STATE_INDEX); + throw new UnexpectedException("entryState의 인덱스는 null이 될 수 없습니다."); } } diff --git a/backend/src/main/java/com/festago/ticketing/domain/MemberTicket.java b/backend/src/main/java/com/festago/ticketing/domain/MemberTicket.java index ec5f881f2..b7b31cf9e 100644 --- a/backend/src/main/java/com/festago/ticketing/domain/MemberTicket.java +++ b/backend/src/main/java/com/festago/ticketing/domain/MemberTicket.java @@ -1,6 +1,7 @@ package com.festago.ticketing.domain; import com.festago.common.domain.BaseTimeEntity; +import com.festago.common.util.Validator; import com.festago.member.domain.Member; import com.festago.stage.domain.Stage; import com.festago.ticket.domain.TicketType; @@ -24,6 +25,7 @@ public class MemberTicket extends BaseTimeEntity { private static final long ENTRY_LIMIT_HOUR = 24; + private static final int MIN_NUMBER_VALUE = 1; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -41,7 +43,7 @@ public class MemberTicket extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) private Stage stage; - @Min(value = 0) + @Min(value = MIN_NUMBER_VALUE) private int number; @NotNull @@ -67,23 +69,31 @@ public MemberTicket(Long id, Member owner, Stage stage, int number, LocalDateTim } private void validate(Member owner, Stage stage, int number, LocalDateTime entryTime, TicketType ticketType) { - checkNotNull(owner, stage, entryTime, ticketType); - checkScope(number); + validateOwner(owner); + validateStage(stage); + validateNumber(number); + validateEntryTime(entryTime); + validateTicketType(ticketType); } - private void checkNotNull(Member owner, Stage stage, LocalDateTime entryTime, TicketType ticketType) { - if (owner == null || - stage == null || - entryTime == null || - ticketType == null) { - throw new IllegalArgumentException("MemberTicket 은 허용되지 않은 null 값으로 생성할 수 없습니다."); - } + private void validateOwner(Member owner) { + Validator.notNull(owner, "owner"); } - private void checkScope(int number) { - if (number < 0) { - throw new IllegalArgumentException("MemberTicket 의 필드로 허용된 범위를 넘은 column 을 넣을 수 없습니다."); - } + private void validateStage(Stage stage) { + Validator.notNull(stage, "stage"); + } + + private void validateNumber(int number) { + Validator.minValue(number, MIN_NUMBER_VALUE, "number"); + } + + private void validateEntryTime(LocalDateTime entryTime) { + Validator.notNull(entryTime, "entryTime"); + } + + private void validateTicketType(TicketType ticketType) { + Validator.notNull(ticketType, "ticketType"); } public void changeState(EntryState originState) { diff --git a/backend/src/main/resources/db/migration/V9__festival_date_index.sql b/backend/src/main/resources/db/migration/V9__festival_date_index.sql new file mode 100644 index 000000000..e52df8ad8 --- /dev/null +++ b/backend/src/main/resources/db/migration/V9__festival_date_index.sql @@ -0,0 +1,4 @@ +create index festival_start_date_index + on festival (start_date); +create index festival_end_date_index + on festival (end_date desc); diff --git a/backend/src/main/resources/festago-config b/backend/src/main/resources/festago-config index 3214a7b3a..0eec697d8 160000 --- a/backend/src/main/resources/festago-config +++ b/backend/src/main/resources/festago-config @@ -1 +1 @@ -Subproject commit 3214a7b3a466f5878fd957c130f47e94f36e5893 +Subproject commit 0eec697d85125fa4c6526365fde41f66e8e348e4 diff --git a/backend/src/test/java/com/festago/admin/domain/AdminTest.java b/backend/src/test/java/com/festago/admin/domain/AdminTest.java new file mode 100644 index 000000000..46492486b --- /dev/null +++ b/backend/src/test/java/com/festago/admin/domain/AdminTest.java @@ -0,0 +1,64 @@ +package com.festago.admin.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import com.festago.common.exception.ValidException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AdminTest { + + @Test + void 어드민_생성_성공() { + // given + Admin admin = new Admin("admin", "password"); + + // when & then + assertThat(admin.getUsername()).isEqualTo("admin"); + assertThat(admin.getPassword()).isEqualTo("password"); + } + + @Test + void username이_4글자_미만이면_예외() { + // given + String username = "1".repeat(3); + + // when & then + assertThatThrownBy(() -> new Admin(username, "password")) + .isInstanceOf(ValidException.class); + } + + @Test + void username이_20글자_초과하면_예외() { + // given + String username = "1".repeat(21); + + // when & then + assertThatThrownBy(() -> new Admin(username, "password")) + .isInstanceOf(ValidException.class); + } + + @Test + void password가_4글자_미만이면_예외() { + // given + String password = "1".repeat(3); + + // when & then + assertThatThrownBy(() -> new Admin("admin", password)) + .isInstanceOf(ValidException.class); + } + + @Test + void password가_255글자_초과하면_예외() { + // given + String password = "1".repeat(256); + + // when & then + assertThatThrownBy(() -> new Admin("admin", password)) + .isInstanceOf(ValidException.class); + } +} diff --git a/backend/src/test/java/com/festago/auth/application/integration/AuthFacadeServiceIntegrationTest.java b/backend/src/test/java/com/festago/auth/application/integration/AuthFacadeServiceIntegrationTest.java index 4024fe487..d894f8a96 100644 --- a/backend/src/test/java/com/festago/auth/application/integration/AuthFacadeServiceIntegrationTest.java +++ b/backend/src/test/java/com/festago/auth/application/integration/AuthFacadeServiceIntegrationTest.java @@ -3,11 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; -import com.festago.application.integration.ApplicationIntegrationTest; import com.festago.auth.application.AuthFacadeService; import com.festago.auth.domain.SocialType; import com.festago.member.domain.Member; import com.festago.member.repository.MemberRepository; +import com.festago.support.ApplicationIntegrationTest; import com.festago.support.MemberFixture; import jakarta.persistence.EntityManager; import java.util.List; diff --git a/backend/src/test/java/com/festago/auth/domain/OAuth2ClientsTest.java b/backend/src/test/java/com/festago/auth/domain/OAuth2ClientsTest.java index f1df89a40..ee021dc4f 100644 --- a/backend/src/test/java/com/festago/auth/domain/OAuth2ClientsTest.java +++ b/backend/src/test/java/com/festago/auth/domain/OAuth2ClientsTest.java @@ -1,6 +1,5 @@ package com.festago.auth.domain; -import static com.festago.common.exception.ErrorCode.DUPLICATE_SOCIAL_TYPE; import static com.festago.common.exception.ErrorCode.OAUTH2_NOT_SUPPORTED_SOCIAL_TYPE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -13,7 +12,7 @@ import com.festago.auth.infrastructure.KakaoOAuth2Client; import com.festago.auth.infrastructure.KakaoOAuth2UserInfoClient; import com.festago.common.exception.BadRequestException; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; @@ -33,8 +32,8 @@ class OAuth2ClientsTest { // when & then assertThatThrownBy(() -> builder.add(new FestagoOAuth2Client())) - .isInstanceOf(InternalServerException.class) - .hasMessage(DUPLICATE_SOCIAL_TYPE.getMessage()); + .isInstanceOf(UnexpectedException.class) + .hasMessage("중복된 OAuth2 제공자 입니다."); } @Test 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 2af3d66f8..7835cdf74 100644 --- a/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthExtractorTest.java +++ b/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthExtractorTest.java @@ -2,13 +2,12 @@ import static com.festago.common.exception.ErrorCode.EXPIRED_AUTH_TOKEN; import static com.festago.common.exception.ErrorCode.INVALID_AUTH_TOKEN; -import static com.festago.common.exception.ErrorCode.INVALID_ROLE_NAME; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.festago.auth.domain.AuthPayload; import com.festago.auth.domain.Role; -import com.festago.common.exception.InternalServerException; import com.festago.common.exception.UnauthorizedException; +import com.festago.common.exception.UnexpectedException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; @@ -85,8 +84,8 @@ class JwtAuthExtractorTest { // when & then assertThatThrownBy(() -> jwtAuthExtractor.extract(token)) - .isInstanceOf(InternalServerException.class) - .hasMessage(INVALID_ROLE_NAME.getMessage()); + .isInstanceOf(UnexpectedException.class) + .hasMessage("해당하는 Role이 없습니다."); } @Test diff --git a/backend/src/test/java/com/festago/exception/FestaGoExceptionTest.java b/backend/src/test/java/com/festago/common/exception/FestaGoExceptionTest.java similarity index 92% rename from backend/src/test/java/com/festago/exception/FestaGoExceptionTest.java rename to backend/src/test/java/com/festago/common/exception/FestaGoExceptionTest.java index b71a58dea..eb14ace99 100644 --- a/backend/src/test/java/com/festago/exception/FestaGoExceptionTest.java +++ b/backend/src/test/java/com/festago/common/exception/FestaGoExceptionTest.java @@ -1,10 +1,7 @@ -package com.festago.exception; +package com.festago.common.exception; import static org.assertj.core.api.Assertions.assertThat; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.FestaGoException; -import com.festago.common.exception.InternalServerException; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/festago/common/util/ValidatorTest.java b/backend/src/test/java/com/festago/common/util/ValidatorTest.java index 8b8cf9abd..dad4a00dd 100644 --- a/backend/src/test/java/com/festago/common/util/ValidatorTest.java +++ b/backend/src/test/java/com/festago/common/util/ValidatorTest.java @@ -3,65 +3,235 @@ import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.festago.common.exception.UnexpectedException; +import com.festago.common.exception.ValidException; 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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class ValidatorTest { - @Test - void 문자열_maxLength_검증_성공() { - // given - String input = "1234567890"; // 10 + @Nested + class hasBlank { - // when & then - assertThatNoException() - .isThrownBy(() -> Validator.maxLength(input, 10, "")); + @ParameterizedTest + @NullSource + void 문자열이_null이면_예외(String input) { + // when & then + assertThatThrownBy(() -> Validator.hasBlank(input, "")) + .isInstanceOf(ValidException.class); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "\t", "\n"}) + void 문자열이_공백이면_예외(String input) { + // when & then + assertThatThrownBy(() -> Validator.hasBlank(input, "")) + .isInstanceOf(ValidException.class); + } + + @Test + void 문자열이_공백이_아니면_통과() { + // given + String input = "1"; + + // when & then + assertThatNoException() + .isThrownBy(() -> Validator.hasBlank(input, "")); + } + } + + + @Nested + class maxLength { + + @Test + void 문자열의_길이가_10이고_최대_길이가_10이면_통과() { + // given + String input = "1234567890"; // 10 + int maxLength = 10; + + // when & then + assertThatNoException() + .isThrownBy(() -> Validator.maxLength(input, maxLength, "")); + } + + @Test + void 문자열의_길이가_11이고_최대_길이가_10이면_예외() { + // given + String input = "12345678901"; // 11 + int maxLength = 10; + + // when & then + assertThatThrownBy(() -> Validator.maxLength(input, maxLength, "")) + .isInstanceOf(ValidException.class); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1}) + void 최대_길이가_0이하면_UnexpectedException(int maxLength) { + // given + String input = "1234567890"; // 10 + + // when & then + assertThatThrownBy(() -> Validator.maxLength(input, maxLength, "")) + .isInstanceOf(UnexpectedException.class); + } + + @ParameterizedTest + @NullSource + void 문자열이_null이면_통과(String input) { + // when & then + assertThatNoException() + .isThrownBy(() -> Validator.maxLength(input, 10, "")); + } } - @Test - void 문자열_maxLength_검증_실패() { - // given - String input = "12345678901"; // 11 + @Nested + class minLength { + + @Test + void 문자열의_길이가_10이고_최소_길이가_10이면_통과() { + // given + String input = "1234567890"; // 10 + int minLength = 10; + + // when & then + assertThatNoException() + .isThrownBy(() -> Validator.minLength(input, minLength, "")); + } + + @Test + void 문자열의_길이가_9이고_최소_길이가_10이면_예외() { + // given + String input = "123456789"; // 9 + int minLength = 10; - // when & then - assertThatThrownBy(() -> Validator.maxLength(input, 10, "")) - .isInstanceOf(IllegalArgumentException.class); + // when & then + assertThatThrownBy(() -> Validator.minLength(input, minLength, "")) + .isInstanceOf(ValidException.class); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1}) + void 최소_길이가_0이하면_UnexpectedException(int minLength) { + // given + String input = "1234567890"; // 10 + + // when & then + assertThatThrownBy(() -> Validator.minLength(input, minLength, "")) + .isInstanceOf(UnexpectedException.class); + } + + @ParameterizedTest + @NullSource + void 문자열이_null이면_통과(String input) { + // when & then + assertThatNoException() + .isThrownBy(() -> Validator.minLength(input, 10, "")); + } } - @Test - void 문자열_maxLength_검증_null_성공() { - // when & then - assertThatNoException() - .isThrownBy(() -> Validator.maxLength(null, 10, "")); + + @Nested + class notNull { + + @ParameterizedTest + @NullSource + void 객체가_null이면_예외(Object object) { + // when & then + assertThatThrownBy(() -> Validator.notNull(object, "")) + .isInstanceOf(ValidException.class); + } + + @Test + void 객체가_null이_아니면_통과() { + // given + Object object = ""; + + // when & then + assertThatNoException() + .isThrownBy(() -> Validator.notNull(object, "")); + } } - @Test - void 문자열_minLength_검증_성공() { - // given - String input = "1234567890"; // 10 + @Nested + class maxValue { + + @Test + void 값이_100이고_최대_값이_100이면_통과() { + // given + int value = 100; + int maxValue = 100; - // when & then - assertThatNoException() - .isThrownBy(() -> Validator.minLength(input, 10, "")); + // when & then + assertThatNoException() + .isThrownBy(() -> Validator.maxValue(value, maxValue, "")); + } + + @Test + void 값이_101이고_최대_값이_100이면_예외() { + // given + int value = 101; + int maxValue = 100; + + // when & then + assertThatThrownBy(() -> Validator.maxValue(value, maxValue, "")) + .isInstanceOf(ValidException.class); + } } - @Test - void 문자열_minLength_검증_실패() { - // given - String input = "123456789"; // 9 + @Nested + class minValue { + + @Test + void 값이_100이고_최소_값이_100이면_통과() { + // given + int value = 100; + int minValue = 100; + + // when & then + assertThatNoException() + .isThrownBy(() -> Validator.minValue(value, minValue, "")); + } - // when & then - assertThatThrownBy(() -> Validator.minLength(input, 10, "")) - .isInstanceOf(IllegalArgumentException.class); + @Test + void 값이_99이고_최소_값이_100이면_예외() { + // given + int value = 99; + int minValue = 100; + + // when & then + assertThatThrownBy(() -> Validator.minValue(value, minValue, "")) + .isInstanceOf(ValidException.class); + } } - @Test - void 문자열_minLength_검증_null_성공() { - // when & then - assertThatNoException() - .isThrownBy(() -> Validator.minLength(null, 1, "")); + @Nested + class isNegative { + + @Test + void 값이_음수이면_예외() { + // given + int value = -1; + + // when & then + assertThatThrownBy(() -> Validator.isNegative(value, "")) + .isInstanceOf(ValidException.class); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1}) + void 값이_음수가_아니면_통과(int value) { + // when & then + assertThatNoException() + .isThrownBy(() -> Validator.isNegative(value, "")); + } } } diff --git a/backend/src/test/java/com/festago/domain/MemberTest.java b/backend/src/test/java/com/festago/domain/MemberTest.java deleted file mode 100644 index e88b98641..000000000 --- a/backend/src/test/java/com/festago/domain/MemberTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.festago.domain; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.festago.member.domain.Member; -import com.festago.support.MemberFixture; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; - -@DisplayNameGeneration(ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -class MemberTest { - - @Test - void 프로필_이미지가_null이면_기본값이_할당된다() { - // when - Member actual = MemberFixture.member() - .profileImage(null) - .build(); - - // then - assertThat(actual.getProfileImage()).isNotNull(); - } -} diff --git a/backend/src/test/java/com/festago/domain/TicketEntryTimeTest.java b/backend/src/test/java/com/festago/domain/TicketEntryTimeTest.java deleted file mode 100644 index 3c2025d6b..000000000 --- a/backend/src/test/java/com/festago/domain/TicketEntryTimeTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.festago.domain; - -import static com.festago.common.exception.ErrorCode.INVALID_MIN_TICKET_AMOUNT; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.festago.common.exception.BadRequestException; -import com.festago.ticket.domain.TicketEntryTime; -import java.time.LocalDateTime; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; - -@DisplayNameGeneration(ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -class TicketEntryTimeTest { - - @Test - void 총_수량이_음수일_경우_에외() { - // given - LocalDateTime now = LocalDateTime.now(); - - // when & then - assertThatThrownBy(() -> new TicketEntryTime(now, -1)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("TicketEntryTime 의 필드로 허용된 범위를 넘은 column 을 넣을 수 없습니다."); - } - - @Test - void 총_수량이_1_미만이면_예외() { - // given - LocalDateTime now = LocalDateTime.now(); - - // when & then - assertThatThrownBy(() -> new TicketEntryTime(now, 0)) - .isInstanceOf(BadRequestException.class) - .hasMessage(INVALID_MIN_TICKET_AMOUNT.getMessage()); - } -} diff --git a/backend/src/test/java/com/festago/application/EntryServiceTest.java b/backend/src/test/java/com/festago/entry/application/EntryServiceTest.java similarity index 98% rename from backend/src/test/java/com/festago/application/EntryServiceTest.java rename to backend/src/test/java/com/festago/entry/application/EntryServiceTest.java index 8786f8de6..0bb7cbc69 100644 --- a/backend/src/test/java/com/festago/application/EntryServiceTest.java +++ b/backend/src/test/java/com/festago/entry/application/EntryServiceTest.java @@ -1,4 +1,4 @@ -package com.festago.application; +package com.festago.entry.application; import static com.festago.common.exception.ErrorCode.MEMBER_TICKET_NOT_FOUND; import static com.festago.common.exception.ErrorCode.NOT_ENTRY_TIME; @@ -6,16 +6,14 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.festago.common.exception.BadRequestException; import com.festago.common.exception.NotFoundException; -import com.festago.entry.application.EntryCodeManager; -import com.festago.entry.application.EntryService; import com.festago.entry.domain.EntryCode; import com.festago.entry.domain.EntryCodePayload; import com.festago.entry.dto.EntryCodeResponse; @@ -50,9 +48,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationEventPublisher; -@ExtendWith(MockitoExtension.class) @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") +@ExtendWith(MockitoExtension.class) class EntryServiceTest { @Mock diff --git a/backend/src/test/java/com/festago/domain/EntryCodeTest.java b/backend/src/test/java/com/festago/entry/domain/EntryCodeTest.java similarity index 59% rename from backend/src/test/java/com/festago/domain/EntryCodeTest.java rename to backend/src/test/java/com/festago/entry/domain/EntryCodeTest.java index b85b744ad..b01ddcfb9 100644 --- a/backend/src/test/java/com/festago/domain/EntryCodeTest.java +++ b/backend/src/test/java/com/festago/entry/domain/EntryCodeTest.java @@ -1,37 +1,44 @@ -package com.festago.domain; +package com.festago.entry.domain; -import static com.festago.common.exception.ErrorCode.INVALID_ENTRY_CODE_OFFSET; -import static com.festago.common.exception.ErrorCode.INVALID_ENTRY_CODE_PERIOD; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import com.festago.common.exception.InternalServerException; -import com.festago.entry.domain.EntryCode; +import com.festago.common.exception.UnexpectedException; 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.NullSource; import org.junit.jupiter.params.provider.ValueSource; @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class EntryCodeTest { + @ParameterizedTest + @NullSource + @ValueSource(strings = {""}) + void 입장_코드의_코드가_null_또는_공백이면_예외(String code) { + assertThatThrownBy(() -> new EntryCode(code, 30, 10)) + .isInstanceOf(UnexpectedException.class) + .hasMessage("code는 빈 값 또는 null이 될 수 없습니다."); + } + @ParameterizedTest @ValueSource(longs = {0, -1}) void 입장_코드의_수명이_0_또는_음수이면_예외(long period) { // when & then assertThatThrownBy(() -> new EntryCode("code", period, 0)) - .isInstanceOf(InternalServerException.class) - .hasMessage(INVALID_ENTRY_CODE_PERIOD.getMessage()); + .isInstanceOf(UnexpectedException.class) + .hasMessage("period는 0 또는 음수가 될 수 없습니다."); } @Test void 입장_코드의_오프셋이_음수이면_예외() { - // when & tehn + // when & then assertThatThrownBy(() -> new EntryCode("code", 30, -1)) - .isInstanceOf(InternalServerException.class) - .hasMessage(INVALID_ENTRY_CODE_OFFSET.getMessage()); + .isInstanceOf(UnexpectedException.class) + .hasMessage("offset은 음수가 될 수 없습니다."); } @ParameterizedTest diff --git a/backend/src/test/java/com/festago/infrastructure/JwtEntryCodeExtractorTest.java b/backend/src/test/java/com/festago/entry/infrastructure/JwtEntryCodeExtractorTest.java similarity index 89% rename from backend/src/test/java/com/festago/infrastructure/JwtEntryCodeExtractorTest.java rename to backend/src/test/java/com/festago/entry/infrastructure/JwtEntryCodeExtractorTest.java index cd362d593..c0e8d54c7 100644 --- a/backend/src/test/java/com/festago/infrastructure/JwtEntryCodeExtractorTest.java +++ b/backend/src/test/java/com/festago/entry/infrastructure/JwtEntryCodeExtractorTest.java @@ -1,16 +1,13 @@ -package com.festago.infrastructure; +package com.festago.entry.infrastructure; import static com.festago.common.exception.ErrorCode.EXPIRED_ENTRY_CODE; import static com.festago.common.exception.ErrorCode.INVALID_ENTRY_CODE; -import static com.festago.common.exception.ErrorCode.INVALID_ENTRY_CODE_PAYLOAD; -import static com.festago.common.exception.ErrorCode.INVALID_ENTRY_STATE_INDEX; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.festago.common.exception.BadRequestException; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; import com.festago.entry.domain.EntryCodePayload; -import com.festago.entry.infrastructure.JwtEntryCodeExtractor; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; @@ -98,8 +95,8 @@ class JwtEntryCodeExtractorTest { // when & then assertThatThrownBy(() -> jwtEntryCodeExtractor.extract(code)) - .isInstanceOf(InternalServerException.class) - .hasMessage(INVALID_ENTRY_CODE_PAYLOAD.getMessage()); + .isInstanceOf(UnexpectedException.class) + .hasMessage("memberTicketId는 null이 될 수 없습니다."); } @Test @@ -113,8 +110,8 @@ class JwtEntryCodeExtractorTest { // when & then assertThatThrownBy(() -> jwtEntryCodeExtractor.extract(code)) - .isInstanceOf(InternalServerException.class) - .hasMessage(INVALID_ENTRY_STATE_INDEX.getMessage()); + .isInstanceOf(UnexpectedException.class) + .hasMessage("entryState의 인덱스는 null이 될 수 없습니다."); } @Test diff --git a/backend/src/test/java/com/festago/infrastructure/JwtEntryCodeProviderTest.java b/backend/src/test/java/com/festago/entry/infrastructure/JwtEntryCodeProviderTest.java similarity index 86% rename from backend/src/test/java/com/festago/infrastructure/JwtEntryCodeProviderTest.java rename to backend/src/test/java/com/festago/entry/infrastructure/JwtEntryCodeProviderTest.java index f1509b032..b11ea8ad1 100644 --- a/backend/src/test/java/com/festago/infrastructure/JwtEntryCodeProviderTest.java +++ b/backend/src/test/java/com/festago/entry/infrastructure/JwtEntryCodeProviderTest.java @@ -1,13 +1,11 @@ -package com.festago.infrastructure; +package com.festago.entry.infrastructure; -import static com.festago.common.exception.ErrorCode.INVALID_ENTRY_CODE_EXPIRATION_TIME; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; -import com.festago.common.exception.InternalServerException; +import com.festago.common.exception.UnexpectedException; import com.festago.entry.application.EntryCodeProvider; import com.festago.entry.domain.EntryCodePayload; -import com.festago.entry.infrastructure.JwtEntryCodeProvider; import com.festago.support.MemberTicketFixture; import com.festago.ticketing.domain.MemberTicket; import io.jsonwebtoken.Claims; @@ -37,8 +35,8 @@ class JwtEntryCodeProviderTest { // when & then assertThatThrownBy(() -> entryCodeProvider.provide(entryCodePayload, expiredAt)) - .isInstanceOf(InternalServerException.class) - .hasMessage(INVALID_ENTRY_CODE_EXPIRATION_TIME.getMessage()); + .isInstanceOf(UnexpectedException.class) + .hasMessage("입장코드 만료일자는 과거일 수 없습니다."); } @Test diff --git a/backend/src/test/java/com/festago/fcm/domain/MemberFCMTest.java b/backend/src/test/java/com/festago/fcm/domain/MemberFCMTest.java new file mode 100644 index 000000000..194885c73 --- /dev/null +++ b/backend/src/test/java/com/festago/fcm/domain/MemberFCMTest.java @@ -0,0 +1,54 @@ +package com.festago.fcm.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.festago.common.exception.ValidException; +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.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class MemberFCMTest { + + @Test + void MemberFCM_생성_성공() { + // given + MemberFCM memberFCM = new MemberFCM(1L, "token"); + + // when & then + assertThat(memberFCM.getMemberId()).isEqualTo(1L); + assertThat(memberFCM.getFcmToken()).isEqualTo("token"); + } + + @ParameterizedTest + @NullSource + void memberId가_null이면_예외(Long memberId) { + // when & then + assertThatThrownBy(() -> new MemberFCM(memberId, "token")) + .isInstanceOf(ValidException.class); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " ", "\t", "\n"}) + void token이_null_또는_공백이면_예외(String token) { + // when & then + assertThatThrownBy(() -> new MemberFCM(1L, token)) + .isInstanceOf(ValidException.class); + } + + @Test + void token의_길이가_255자를_초과하면_예외() { + // given + String token = "1".repeat(256); + + // when & then + assertThatThrownBy(() -> new MemberFCM(1L, token)) + .isInstanceOf(ValidException.class); + } +} diff --git a/backend/src/test/java/com/festago/application/FestivalServiceTest.java b/backend/src/test/java/com/festago/festival/application/FestivalServiceTest.java similarity index 89% rename from backend/src/test/java/com/festago/application/FestivalServiceTest.java rename to backend/src/test/java/com/festago/festival/application/FestivalServiceTest.java index c8c5c3387..d6a078ae1 100644 --- a/backend/src/test/java/com/festago/application/FestivalServiceTest.java +++ b/backend/src/test/java/com/festago/festival/application/FestivalServiceTest.java @@ -1,4 +1,4 @@ -package com.festago.application; +package com.festago.festival.application; import static com.festago.common.exception.ErrorCode.FESTIVAL_NOT_FOUND; import static com.festago.common.exception.ErrorCode.INVALID_FESTIVAL_START_DATE; @@ -11,13 +11,11 @@ import com.festago.common.exception.BadRequestException; import com.festago.common.exception.NotFoundException; -import com.festago.festival.application.FestivalService; import com.festago.festival.domain.Festival; import com.festago.festival.dto.FestivalCreateRequest; import com.festago.festival.dto.FestivalDetailResponse; import com.festago.festival.dto.FestivalDetailStageResponse; import com.festago.festival.dto.FestivalResponse; -import com.festago.festival.dto.FestivalsResponse; import com.festago.festival.repository.FestivalRepository; import com.festago.school.domain.School; import com.festago.school.repository.SchoolRepository; @@ -41,9 +39,9 @@ import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -@ExtendWith(MockitoExtension.class) @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") +@ExtendWith(MockitoExtension.class) class FestivalServiceTest { @Mock @@ -61,22 +59,6 @@ class FestivalServiceTest { @InjectMocks FestivalService festivalService; - @Test - void 모든_축제_조회() { - // given - Festival festival1 = FestivalFixture.festival().id(1L).build(); - Festival festival2 = FestivalFixture.festival().id(2L).build(); - given(festivalRepository.findAll()).willReturn(List.of(festival1, festival2)); - - // when - FestivalsResponse response = festivalService.findAll(); - - // then - List festivalIds = response.festivals().stream().map(FestivalResponse::id).toList(); - - assertThat(festivalIds).containsExactly(1L, 2L); - } - @Nested class 축제_생성 { diff --git a/backend/src/test/java/com/festago/application/integration/FestivalServiceIntegrationTest.java b/backend/src/test/java/com/festago/festival/application/integration/FestivalServiceIntegrationTest.java similarity index 97% rename from backend/src/test/java/com/festago/application/integration/FestivalServiceIntegrationTest.java rename to backend/src/test/java/com/festago/festival/application/integration/FestivalServiceIntegrationTest.java index d7d850f87..8094925d8 100644 --- a/backend/src/test/java/com/festago/application/integration/FestivalServiceIntegrationTest.java +++ b/backend/src/test/java/com/festago/festival/application/integration/FestivalServiceIntegrationTest.java @@ -1,4 +1,4 @@ -package com.festago.application.integration; +package com.festago.festival.application.integration; import static org.assertj.core.api.Assertions.assertThat; @@ -14,6 +14,7 @@ import com.festago.school.repository.SchoolRepository; import com.festago.stage.domain.Stage; import com.festago.stage.repository.StageRepository; +import com.festago.support.ApplicationIntegrationTest; import com.festago.support.FestivalFixture; import com.festago.support.SchoolFixture; import com.festago.support.StageFixture; diff --git a/backend/src/test/java/com/festago/festival/domain/FestivalFilterTest.java b/backend/src/test/java/com/festago/festival/domain/FestivalFilterTest.java new file mode 100644 index 000000000..8c7ea210f --- /dev/null +++ b/backend/src/test/java/com/festago/festival/domain/FestivalFilterTest.java @@ -0,0 +1,54 @@ +package com.festago.festival.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.festago.common.exception.BadRequestException; +import com.festago.festival.repository.FestivalFilter; +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 FestivalFilterTest { + + @Test + void 유효하지_않은_name_이면_예외() { + // given && when && then + assertThatThrownBy(() -> FestivalFilter.from("unvalid")) + .isInstanceOf(BadRequestException.class); + } + + @ValueSource(strings = {"progress", "Progress", "PROGRESS"}) + @ParameterizedTest + void PROGRESS_반환(String value) { + // given && when + FestivalFilter filter = FestivalFilter.from(value); + + // then + assertThat(filter).isEqualTo(FestivalFilter.PROGRESS); + } + + @ValueSource(strings = {"planned", "Planned", "PLANNED"}) + @ParameterizedTest + void PLANNED_반환(String value) { + // given && when + FestivalFilter filter = FestivalFilter.from(value); + + // then + assertThat(filter).isEqualTo(FestivalFilter.PLANNED); + } + + @ValueSource(strings = {"end", "End", "END"}) + @ParameterizedTest + void END_반환(String value) { + // given && when + FestivalFilter filter = FestivalFilter.from(value); + + // then + assertThat(filter).isEqualTo(FestivalFilter.END); + } +} diff --git a/backend/src/test/java/com/festago/domain/FestivalTest.java b/backend/src/test/java/com/festago/festival/domain/FestivalTest.java similarity index 94% rename from backend/src/test/java/com/festago/domain/FestivalTest.java rename to backend/src/test/java/com/festago/festival/domain/FestivalTest.java index ac29a3627..0baad5c55 100644 --- a/backend/src/test/java/com/festago/domain/FestivalTest.java +++ b/backend/src/test/java/com/festago/festival/domain/FestivalTest.java @@ -1,10 +1,10 @@ -package com.festago.domain; +package com.festago.festival.domain; import static com.festago.common.exception.ErrorCode.INVALID_FESTIVAL_DURATION; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.festago.festival.domain.Festival; +import com.festago.common.exception.BadRequestException; import com.festago.school.domain.School; import com.festago.support.FestivalFixture; import com.festago.support.SchoolFixture; @@ -28,7 +28,7 @@ class FestivalTest { // when & then assertThatThrownBy(() -> new Festival("테코대학교", tomorrow, today, school)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessage(INVALID_FESTIVAL_DURATION.getMessage()); } diff --git a/backend/src/test/java/com/festago/dto/FestivalCreateRequestTest.java b/backend/src/test/java/com/festago/festival/dto/FestivalCreateRequestTest.java similarity index 95% rename from backend/src/test/java/com/festago/dto/FestivalCreateRequestTest.java rename to backend/src/test/java/com/festago/festival/dto/FestivalCreateRequestTest.java index 9252f0d1c..e7677b266 100644 --- a/backend/src/test/java/com/festago/dto/FestivalCreateRequestTest.java +++ b/backend/src/test/java/com/festago/festival/dto/FestivalCreateRequestTest.java @@ -1,9 +1,8 @@ -package com.festago.dto; +package com.festago.festival.dto; import static org.assertj.core.api.Assertions.assertThat; import com.festago.festival.domain.Festival; -import com.festago.festival.dto.FestivalCreateRequest; import com.festago.school.domain.School; import com.festago.support.SchoolFixture; import java.time.LocalDate; @@ -50,5 +49,4 @@ class FestivalCreateRequestTest { // then assertThat(festival.getThumbnail()).isEqualTo("img"); } - } diff --git a/backend/src/test/java/com/festago/festival/repository/FestivalRepositoryTest.java b/backend/src/test/java/com/festago/festival/repository/FestivalRepositoryTest.java new file mode 100644 index 000000000..81e13a050 --- /dev/null +++ b/backend/src/test/java/com/festago/festival/repository/FestivalRepositoryTest.java @@ -0,0 +1,162 @@ +package com.festago.festival.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import com.festago.festival.domain.Festival; +import com.festago.school.domain.School; +import com.festago.school.repository.SchoolRepository; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@DataJpaTest +class FestivalRepositoryTest { + + private static final String CURRENT_FESTIVAL = "현재 축제"; + private static final String PAST_FESTIVAL = "과거 축제"; + private static final String FUTURE_FESTIVAL = "미래 축제"; + + @Autowired + FestivalRepository festivalRepository; + + @Autowired + SchoolRepository schoolRepository; + + LocalDate now = LocalDate.parse("2023-06-30"); + + School school; + + @BeforeEach + public void setting() { + this.school = schoolRepository.save(new School("domain", "name")); + } + + private void prepareNotOrderedFestivals() { + festivalRepository.save(new Festival(FUTURE_FESTIVAL, now.plusDays(1L), now.plusDays(3L), school)); + festivalRepository.save(new Festival(CURRENT_FESTIVAL, now, now, school)); + festivalRepository.save(new Festival(PAST_FESTIVAL, now.minusDays(3L), now.minusDays(1L), school)); + } + + @Nested + class 진행_에정_축제_반환 { + + @Test + void 성공() { + // given + FestivalFilter filter = FestivalFilter.PLANNED; + prepareNotOrderedFestivals(); + + // when + List actual = festivalRepository.findAll(filter.getSpecification(now)); + + // then + assertSoftly(softAssertions -> { + assertThat(actual).hasSize(1); + assertThat(actual.get(0)).matches(festival -> festival.getName().equals(FUTURE_FESTIVAL)); + }); + } + + @Test + void 은_시작_시점이_빠른_순서로_반환된다() { + // given + FestivalFilter filter = FestivalFilter.PLANNED; + Festival festival2 = festivalRepository.save( + new Festival("festival2", now.plusDays(2), now.plusDays(10), school)); + Festival festival3 = festivalRepository.save( + new Festival("festival3", now.plusDays(3), now.plusDays(10), school)); + Festival festival1 = festivalRepository.save( + new Festival("festival1", now.plusDays(1), now.plusDays(10), school)); + + // when + List actual = festivalRepository.findAll(filter.getSpecification(now)); + + // then + assertThat(actual).isEqualTo(List.of(festival1, festival2, festival3)); + } + } + + @Nested + class 진행_축제_반환 { + + @Test + void 성공() { + // given + FestivalFilter filter = FestivalFilter.PROGRESS; + prepareNotOrderedFestivals(); + + // when + List actual = festivalRepository.findAll(filter.getSpecification(now)); + + // then + assertSoftly(softAssertions -> { + assertThat(actual).hasSize(1); + assertThat(actual.get(0)).matches(festival -> festival.getName().equals(CURRENT_FESTIVAL)); + }); + } + + @Test + void 은_시작_시점이_빠른_순서로_반환된다() { + // given + FestivalFilter filter = FestivalFilter.PROGRESS; + Festival festival2 = festivalRepository.save( + new Festival("festival2", now.minusDays(2), now.plusDays(10), school)); + Festival festival3 = festivalRepository.save( + new Festival("festival3", now.minusDays(1), now.plusDays(10), school)); + Festival festival1 = festivalRepository.save( + new Festival("festival1", now.minusDays(3), now.plusDays(10), school)); + + // when + List actual = festivalRepository.findAll(filter.getSpecification(now)); + + // then + assertThat(actual).isEqualTo(List.of(festival1, festival2, festival3)); + } + } + + @Nested + class 종료_축제_반환 { + + @Test + void 성공() { + // given + FestivalFilter filter = FestivalFilter.END; + prepareNotOrderedFestivals(); + + // when + List actual = festivalRepository.findAll(filter.getSpecification(now)); + + // then + assertSoftly(softAssertions -> { + assertThat(actual).hasSize(1); + assertThat(actual.get(0)).matches(festival -> festival.getName().equals(PAST_FESTIVAL)); + }); + } + + @Test + void 은_종료_시점이_느린_순서로_반환된다() { + // given + FestivalFilter filter = FestivalFilter.END; + Festival festival2 = festivalRepository.save( + new Festival("festival2", now.minusDays(10), now.minusDays(2), school)); + Festival festival3 = festivalRepository.save( + new Festival("festival3", now.minusDays(10), now.minusDays(1), school)); + Festival festival1 = festivalRepository.save( + new Festival("festival1", now.minusDays(10), now.minusDays(3), school)); + + // when + List actual = festivalRepository.findAll(filter.getSpecification(now)); + + // then + assertThat(actual).isEqualTo(List.of(festival3, festival2, festival1)); + } + } +} diff --git a/backend/src/test/java/com/festago/member/domain/MemberTest.java b/backend/src/test/java/com/festago/member/domain/MemberTest.java new file mode 100644 index 000000000..43ccf576e --- /dev/null +++ b/backend/src/test/java/com/festago/member/domain/MemberTest.java @@ -0,0 +1,95 @@ +package com.festago.member.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.festago.auth.domain.SocialType; +import com.festago.common.exception.ValidException; +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.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class MemberTest { + + @Test + void Member_생성_성공() { + // given + Member member = new Member(1L, "12345", SocialType.FESTAGO, "nickname", "profileImage.png"); + + // when & then + assertThat(member.getId()).isEqualTo(1L); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " ", "\t", "\n"}) + void socialId가_null_또는_공백이면_예외(String socialId) { + // when & then + assertThatThrownBy(() -> new Member(1L, socialId, SocialType.FESTAGO, "nickname", "profileImage.png")) + .isInstanceOf(ValidException.class); + } + + @Test + void socialId의_길이가_255자를_초과하면_예외() { + // given + String socialId = "1".repeat(256); + + // when & then + assertThatThrownBy(() -> new Member(1L, socialId, SocialType.FESTAGO, "nickname", "profileImage.png")) + .isInstanceOf(ValidException.class); + } + + @ParameterizedTest + @NullSource + void socialType이_null이면_예외(SocialType socialType) { + // when & then + assertThatThrownBy(() -> new Member(1L, "12345", socialType, "nickname", "profileImage.png")) + .isInstanceOf(ValidException.class); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " ", "\t", "\n"}) + void nickname이_null_또는_공백이면_예외(String nickname) { + // when & then + assertThatThrownBy(() -> new Member(1L, "12345", SocialType.FESTAGO, nickname, "profileImage.png")) + .isInstanceOf(ValidException.class); + } + + @Test + void nickname의_길이가_30자를_초과하면_예외() { + // given + String nickname = "1".repeat(31); + + // when & then + assertThatThrownBy(() -> new Member(1L, "12345", SocialType.FESTAGO, nickname, "profileImage.png")) + .isInstanceOf(ValidException.class); + } + + @Test + void profileImage의_길이가_255자를_초과하면_예외() { + // given + String profileImage = "1".repeat(256); + + // when & then + assertThatThrownBy(() -> new Member(1L, "12345", SocialType.FESTAGO, "nickname", profileImage)) + .isInstanceOf(ValidException.class); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " ", "\t", "\n"}) + void profileImage가_null_또는_공백이면_기본값이_할당된다(String profileImage) { + // given + Member actual = new Member("12345", SocialType.FESTAGO, "nickname", profileImage); + + // when & then + assertThat(actual.getProfileImage()).isNotNull(); + assertThat(actual.getProfileImage()).isNotBlank(); + } +} diff --git a/backend/src/test/java/com/festago/domain/MemberRepositoryTest.java b/backend/src/test/java/com/festago/member/repository/MemberRepositoryTest.java similarity index 95% rename from backend/src/test/java/com/festago/domain/MemberRepositoryTest.java rename to backend/src/test/java/com/festago/member/repository/MemberRepositoryTest.java index 6e6e2e927..c66c3678c 100644 --- a/backend/src/test/java/com/festago/domain/MemberRepositoryTest.java +++ b/backend/src/test/java/com/festago/member/repository/MemberRepositoryTest.java @@ -1,10 +1,9 @@ -package com.festago.domain; +package com.festago.member.repository; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.festago.member.domain.Member; -import com.festago.member.repository.MemberRepository; import com.festago.support.MemberFixture; import jakarta.persistence.EntityManager; import org.junit.jupiter.api.DisplayNameGeneration; diff --git a/backend/src/test/java/com/festago/presentation/AdminControllerTest.java b/backend/src/test/java/com/festago/presentation/AdminControllerTest.java index 2fc07ecbd..d4a9b6644 100644 --- a/backend/src/test/java/com/festago/presentation/AdminControllerTest.java +++ b/backend/src/test/java/com/festago/presentation/AdminControllerTest.java @@ -13,7 +13,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import com.festago.admin.application.AdminService; import com.festago.auth.application.AdminAuthService; import com.festago.auth.application.AuthExtractor; import com.festago.auth.domain.Role; @@ -70,9 +69,6 @@ class AdminControllerTest { @MockBean TicketService ticketService; - @MockBean - AdminService adminService; - @MockBean AdminAuthService adminAuthService; diff --git a/backend/src/test/java/com/festago/presentation/FestivalControllerTest.java b/backend/src/test/java/com/festago/presentation/FestivalControllerTest.java index 12f038314..8fef4e0a3 100644 --- a/backend/src/test/java/com/festago/presentation/FestivalControllerTest.java +++ b/backend/src/test/java/com/festago/presentation/FestivalControllerTest.java @@ -1,8 +1,12 @@ package com.festago.presentation; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -12,6 +16,7 @@ import com.festago.festival.dto.FestivalDetailResponse; import com.festago.festival.dto.FestivalResponse; import com.festago.festival.dto.FestivalsResponse; +import com.festago.festival.repository.FestivalFilter; import com.festago.support.CustomWebMvcTest; import java.nio.charset.StandardCharsets; import java.time.LocalDate; @@ -20,6 +25,8 @@ import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; @@ -39,15 +46,18 @@ class FestivalControllerTest { @MockBean FestivalService festivalService; + @Captor + ArgumentCaptor festivalFilterCaptor; + @Test - void 모든_축제를_조회한다() throws Exception { + void 축제를_조회한다() throws Exception { // given FestivalResponse festivalResponse1 = new FestivalResponse(1L, 1L, "테코대학교", LocalDate.now(), LocalDate.now().plusDays(3), "https://image1.png"); FestivalResponse festivalResponse2 = new FestivalResponse(2L, 2L, "우테대학교", LocalDate.now().minusDays(3), LocalDate.now(), "https://image2.png"); FestivalsResponse expected = new FestivalsResponse(List.of(festivalResponse1, festivalResponse2)); - given(festivalService.findAll()) + given(festivalService.findFestivals(any(FestivalFilter.class))) .willReturn(expected); // when & then @@ -59,7 +69,12 @@ class FestivalControllerTest { .getResponse() .getContentAsString(StandardCharsets.UTF_8); FestivalsResponse actual = objectMapper.readValue(content, FestivalsResponse.class); - assertThat(actual).isEqualTo(expected); + assertSoftly(softAssertions -> { + verify(festivalService, times(1)).findFestivals(festivalFilterCaptor.capture()); + assertThat(festivalFilterCaptor.getValue()).isEqualTo(FestivalFilter.PROGRESS); + assertThat(actual).isEqualTo(expected); + } + ); } @Test diff --git a/backend/src/test/java/com/festago/presentation/StudentControllerTest.java b/backend/src/test/java/com/festago/presentation/StudentControllerTest.java index 541d79f40..8dfd17dae 100644 --- a/backend/src/test/java/com/festago/presentation/StudentControllerTest.java +++ b/backend/src/test/java/com/festago/presentation/StudentControllerTest.java @@ -1,14 +1,22 @@ package com.festago.presentation; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +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; import com.fasterxml.jackson.databind.ObjectMapper; import com.festago.student.application.StudentService; +import com.festago.student.dto.StudentResponse; +import com.festago.student.dto.StudentSchoolResponse; import com.festago.student.dto.StudentSendMailRequest; import com.festago.student.dto.StudentVerificateRequest; 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.Nested; @@ -93,4 +101,42 @@ class 학생_인증 { .andExpect(status().isOk()); } } + + @Nested + class 학생_인증_정보_조회 { + + @Test + void 인증이_되지_않으면_401() throws Exception { + // given + StudentVerificateRequest request = new StudentVerificateRequest("123456"); + + // when & then + mockMvc.perform(get("/students") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockAuth + void 학생_인증_조회() throws Exception { + // given + StudentResponse expected = new StudentResponse(true, new StudentSchoolResponse(1L, "테코대학교", "teco.ac.kr")); + given(studentService.findVerification(anyLong())) + .willReturn(expected); + + // when & then + String content = mockMvc.perform(get("/students") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer token") + ) + .andExpect(status().isOk()) + .andDo(print()) + .andReturn() + .getResponse() + .getContentAsString(StandardCharsets.UTF_8); + StudentResponse actual = objectMapper.readValue(content, StudentResponse.class); + assertThat(actual).isEqualTo(expected); + } + } } diff --git a/backend/src/test/java/com/festago/school/domain/SchoolTest.java b/backend/src/test/java/com/festago/school/domain/SchoolTest.java index f3305223f..45150b9ef 100644 --- a/backend/src/test/java/com/festago/school/domain/SchoolTest.java +++ b/backend/src/test/java/com/festago/school/domain/SchoolTest.java @@ -3,10 +3,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.festago.common.exception.ValidException; 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.NullSource; import org.junit.jupiter.params.provider.ValueSource; @DisplayNameGeneration(ReplaceUnderscores.class) @@ -16,43 +18,95 @@ class SchoolTest { @Test void 학교의_도메인_길이가_50자를_넘으면_예외() { // given - String domain = "1234567890".repeat(5) + "1"; + String domain = "1".repeat(51); // when & then - assertThatThrownBy(() -> new School(domain, "name")) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> new School(domain, "테코대학교")) + .isInstanceOf(ValidException.class); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " ", "\t", "\n"}) + void 학교의_도메인이_null_또는_공백이면_예외(String domain) { + // when & then + assertThatThrownBy(() -> new School(domain, "테코대학교")) + .isInstanceOf(ValidException.class); } @Test void 학교의_이름이_255자를_넘으면_예외() { // given - String name = "1234567890".repeat(25) + "123456"; + String name = "1".repeat(256); + + // when & then + assertThatThrownBy(() -> new School("teco.ac.kr", name)) + .isInstanceOf(ValidException.class); + } + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " ", "\t", "\n"}) + void 학교의_이름이_null_또는_공백이면_예외(String name) { // when & then - assertThatThrownBy(() -> new School("domain", name)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> new School("teco.ac.kr", name)) + .isInstanceOf(ValidException.class); } @Test void 학교의_도메인을_수정할때_255자를_넘으면_예외() { // given - School school = new School("domain", "name"); + School school = new School("teco.ac.kr", "테코대학교"); + + // when & then + String domain = "1".repeat(256); + assertThatThrownBy(() -> school.changeDomain(domain)) + .isInstanceOf(ValidException.class); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " ", "\t", "\n"}) + void 학교의_도메인을_수정할때_null_또는_공백이면_예외(String domain) { + // given + School school = new School("teco.ac.kr", "테코대학교"); // when & then - String domain = "1234567890".repeat(5) + "1"; assertThatThrownBy(() -> school.changeDomain(domain)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(ValidException.class); } @Test void 학교의_이름을_수정할때_255자를_넘으면_예외() { // given - School school = new School("domain", "name"); + School school = new School("teco.ac.kr", "테코대학교"); + + // when & then + String name = "1".repeat(256); + assertThatThrownBy(() -> school.changeName(name)) + .isInstanceOf(ValidException.class); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " ", "\t", "\n"}) + void 학교의_이름을_수정할때_null_또는_공백이면_예외(String name) { + // given + School school = new School("teco.ac.kr", "테코대학교"); // when & then - String name = "1234567890".repeat(25) + "123456"; assertThatThrownBy(() -> school.changeName(name)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(ValidException.class); + } + + @Test + void 학교_생성_성공() { + // given + School school = new School("teco.ac.kr", "테코대학교"); + + // when & then + assertThat(school.getName()).isEqualTo("테코대학교"); + assertThat(school.getDomain()).isEqualTo("teco.ac.kr"); } @ParameterizedTest @@ -62,7 +116,7 @@ class SchoolTest { String domain = "1".repeat(length); // when - School school = new School(domain, "name"); + School school = new School(domain, "테코대학교"); // then assertThat(school.getDomain()).isEqualTo(domain); @@ -75,7 +129,7 @@ class SchoolTest { String name = "1".repeat(length); // when - School school = new School("domain", name); + School school = new School("teco.ac.kr", name); // then assertThat(school.getName()).isEqualTo(name); diff --git a/backend/src/test/java/com/festago/application/StageServiceTest.java b/backend/src/test/java/com/festago/stage/application/StageServiceTest.java similarity index 91% rename from backend/src/test/java/com/festago/application/StageServiceTest.java rename to backend/src/test/java/com/festago/stage/application/StageServiceTest.java index a2261cd22..3fb4d7da3 100644 --- a/backend/src/test/java/com/festago/application/StageServiceTest.java +++ b/backend/src/test/java/com/festago/stage/application/StageServiceTest.java @@ -1,13 +1,12 @@ -package com.festago.application; +package com.festago.stage.application; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.any; -import static org.mockito.BDDMockito.anyLong; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import com.festago.festival.domain.Festival; import com.festago.festival.repository.FestivalRepository; -import com.festago.stage.application.StageService; import com.festago.stage.domain.Stage; import com.festago.stage.dto.StageCreateRequest; import com.festago.stage.dto.StageResponse; diff --git a/backend/src/test/java/com/festago/application/integration/StageServiceIntegrationTest.java b/backend/src/test/java/com/festago/stage/application/integration/StageServiceIntegrationTest.java similarity index 93% rename from backend/src/test/java/com/festago/application/integration/StageServiceIntegrationTest.java rename to backend/src/test/java/com/festago/stage/application/integration/StageServiceIntegrationTest.java index 31ecc5b58..0a43cd783 100644 --- a/backend/src/test/java/com/festago/application/integration/StageServiceIntegrationTest.java +++ b/backend/src/test/java/com/festago/stage/application/integration/StageServiceIntegrationTest.java @@ -1,4 +1,4 @@ -package com.festago.application.integration; +package com.festago.stage.application.integration; import static com.festago.common.exception.ErrorCode.FESTIVAL_NOT_FOUND; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -7,6 +7,7 @@ import com.festago.festival.repository.FestivalRepository; import com.festago.stage.application.StageService; import com.festago.stage.dto.StageCreateRequest; +import com.festago.support.ApplicationIntegrationTest; import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; diff --git a/backend/src/test/java/com/festago/domain/StageTest.java b/backend/src/test/java/com/festago/stage/domain/StageTest.java similarity index 97% rename from backend/src/test/java/com/festago/domain/StageTest.java rename to backend/src/test/java/com/festago/stage/domain/StageTest.java index b73afd416..6c8304b16 100644 --- a/backend/src/test/java/com/festago/domain/StageTest.java +++ b/backend/src/test/java/com/festago/stage/domain/StageTest.java @@ -1,4 +1,4 @@ -package com.festago.domain; +package com.festago.stage.domain; import static com.festago.common.exception.ErrorCode.INVALID_STAGE_START_TIME; import static com.festago.common.exception.ErrorCode.INVALID_TICKET_OPEN_TIME; @@ -79,7 +79,7 @@ class StageTest { .festival(festival) .build() ) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessage(INVALID_TICKET_OPEN_TIME.getMessage()); } @@ -100,5 +100,4 @@ class StageTest { .festival(festival) .build()); } - } diff --git a/backend/src/test/java/com/festago/domain/StageRepositoryTest.java b/backend/src/test/java/com/festago/stage/repository/StageRepositoryTest.java similarity index 97% rename from backend/src/test/java/com/festago/domain/StageRepositoryTest.java rename to backend/src/test/java/com/festago/stage/repository/StageRepositoryTest.java index c88bb4cf9..7f90511d1 100644 --- a/backend/src/test/java/com/festago/domain/StageRepositoryTest.java +++ b/backend/src/test/java/com/festago/stage/repository/StageRepositoryTest.java @@ -1,4 +1,4 @@ -package com.festago.domain; +package com.festago.stage.repository; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; @@ -8,7 +8,6 @@ import com.festago.school.domain.School; import com.festago.school.repository.SchoolRepository; import com.festago.stage.domain.Stage; -import com.festago.stage.repository.StageRepository; import com.festago.support.FestivalFixture; import com.festago.support.SchoolFixture; import com.festago.support.StageFixture; @@ -24,9 +23,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -@DataJpaTest @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") +@DataJpaTest class StageRepositoryTest { @Autowired @@ -73,7 +72,7 @@ class StageRepositoryTest { List actual = stageRepository.findAllDetailByFestivalId(festival.getId()); // then - assertThat(actual.size()).isEqualTo(1); + assertThat(actual).hasSize(1); } @Test diff --git a/backend/src/test/java/com/festago/student/application/StudentServiceTest.java b/backend/src/test/java/com/festago/student/application/StudentServiceTest.java index f7ef57a17..35671209f 100644 --- a/backend/src/test/java/com/festago/student/application/StudentServiceTest.java +++ b/backend/src/test/java/com/festago/student/application/StudentServiceTest.java @@ -8,6 +8,7 @@ import static com.festago.common.exception.ErrorCode.TOO_FREQUENT_REQUESTS; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; @@ -20,8 +21,10 @@ import com.festago.member.repository.MemberRepository; import com.festago.school.domain.School; import com.festago.school.repository.SchoolRepository; +import com.festago.student.domain.Student; import com.festago.student.domain.StudentCode; import com.festago.student.domain.VerificationCode; +import com.festago.student.dto.StudentResponse; import com.festago.student.dto.StudentSendMailRequest; import com.festago.student.dto.StudentVerificateRequest; import com.festago.student.infrastructure.MockMailClient; @@ -32,6 +35,7 @@ import com.festago.support.SchoolFixture; import com.festago.support.SetUpMockito; import com.festago.support.StudentCodeFixture; +import com.festago.support.StudentFixture; import java.time.Clock; import java.time.LocalDateTime; import java.util.Optional; @@ -186,7 +190,7 @@ class 학생_인증 { .willReturn(true); // when & then - assertThatThrownBy(() -> studentService.verificate(memberId, request)) + assertThatThrownBy(() -> studentService.verify(memberId, request)) .isInstanceOf(BadRequestException.class) .hasMessage(ALREADY_STUDENT_VERIFIED.getMessage()); } @@ -202,7 +206,7 @@ class 학생_인증 { .willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> studentService.verificate(memberId, request)) + assertThatThrownBy(() -> studentService.verify(memberId, request)) .isInstanceOf(BadRequestException.class) .hasMessage(INVALID_STUDENT_VERIFICATION_CODE.getMessage()); } @@ -225,7 +229,47 @@ class 학생_인증 { // when & then assertThatNoException() - .isThrownBy(() -> studentService.verificate(memberId, request)); + .isThrownBy(() -> studentService.verify(memberId, request)); + } + } + + @Nested + class 멤버_아이디로_인증정보_조회 { + + @Test + void 학생_인증된_멤버의_경우() { + // given + Long memberId = 1L; + School school = SchoolFixture.school().id(2L).build(); + Student student = StudentFixture.student().id(3L).school(school).build(); + given(studentRepository.findByMemberIdWithFetch(memberId)) + .willReturn(Optional.of(student)); + + // when + StudentResponse actual = studentService.findVerification(memberId); + + // then + assertSoftly(softly -> { + softly.assertThat(actual.isVerified()).isTrue(); + softly.assertThat(actual.school().id()).isEqualTo(school.getId()); + }); + } + + @Test + void 학생_인증되지_않은_사용자의_경우() { + // given + Long memberId = 1L; + given(studentRepository.findByMemberIdWithFetch(memberId)) + .willReturn(Optional.empty()); + + // when + StudentResponse actual = studentService.findVerification(memberId); + + // then + assertSoftly(softly -> { + softly.assertThat(actual.isVerified()).isFalse(); + softly.assertThat(actual.school()).isNull(); + }); } } } diff --git a/backend/src/test/java/com/festago/domain/VerificationCodeTest.java b/backend/src/test/java/com/festago/student/domain/VerificationCodeTest.java similarity index 67% rename from backend/src/test/java/com/festago/domain/VerificationCodeTest.java rename to backend/src/test/java/com/festago/student/domain/VerificationCodeTest.java index 4cccf3fc4..c9c4da5c0 100644 --- a/backend/src/test/java/com/festago/domain/VerificationCodeTest.java +++ b/backend/src/test/java/com/festago/student/domain/VerificationCodeTest.java @@ -1,10 +1,9 @@ -package com.festago.domain; +package com.festago.student.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.festago.common.exception.InternalServerException; -import com.festago.student.domain.VerificationCode; +import com.festago.common.exception.UnexpectedException; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; @@ -19,7 +18,8 @@ class VerificationCodeTest { void null_이면_예외() { // when & then assertThatThrownBy(() -> new VerificationCode(null)) - .isInstanceOf(InternalServerException.class); + .isInstanceOf(UnexpectedException.class) + .hasMessage("VerificationCode는 null 또는 공백이 될 수 없습니다."); } @ParameterizedTest @@ -27,21 +27,24 @@ class VerificationCodeTest { void 길이가_6자리가_아니면_예외(String code) { // when & then assertThatThrownBy(() -> new VerificationCode(code)) - .isInstanceOf(InternalServerException.class); + .isInstanceOf(UnexpectedException.class) + .hasMessage("VerificationCode의 길이는 6 이어야 합니다."); } @Test void 숫자가_아니면_예외() { // when & then assertThatThrownBy(() -> new VerificationCode("일이삼사오육")) - .isInstanceOf(InternalServerException.class); + .isInstanceOf(UnexpectedException.class) + .hasMessage("VerificationCode는 0~9의 양수 형식이어야 합니다."); } @Test void 음수이면_예외() { // when & then assertThatThrownBy(() -> new VerificationCode("-12345")) - .isInstanceOf(InternalServerException.class); + .isInstanceOf(UnexpectedException.class) + .hasMessage("VerificationCode는 0~9의 양수 형식이어야 합니다."); } @Test diff --git a/backend/src/test/java/com/festago/domain/RandomVerificationCodeProviderTest.java b/backend/src/test/java/com/festago/student/infrastructure/RandomVerificationCodeProviderTest.java similarity index 86% rename from backend/src/test/java/com/festago/domain/RandomVerificationCodeProviderTest.java rename to backend/src/test/java/com/festago/student/infrastructure/RandomVerificationCodeProviderTest.java index 1bf3e8a23..f070b0363 100644 --- a/backend/src/test/java/com/festago/domain/RandomVerificationCodeProviderTest.java +++ b/backend/src/test/java/com/festago/student/infrastructure/RandomVerificationCodeProviderTest.java @@ -1,9 +1,8 @@ -package com.festago.domain; +package com.festago.student.infrastructure; import static org.assertj.core.api.Assertions.assertThat; import com.festago.student.domain.VerificationCode; -import com.festago.student.infrastructure.RandomVerificationCodeProvider; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/festago/student/repository/StudentRepositoryTest.java b/backend/src/test/java/com/festago/student/repository/StudentRepositoryTest.java new file mode 100644 index 000000000..6076dab94 --- /dev/null +++ b/backend/src/test/java/com/festago/student/repository/StudentRepositoryTest.java @@ -0,0 +1,50 @@ +package com.festago.student.repository; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import com.festago.member.domain.Member; +import com.festago.member.repository.MemberRepository; +import com.festago.school.domain.School; +import com.festago.school.repository.SchoolRepository; +import com.festago.student.domain.Student; +import com.festago.support.MemberFixture; +import com.festago.support.SchoolFixture; +import com.festago.support.StudentFixture; +import java.util.Optional; +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.orm.jpa.DataJpaTest; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@DataJpaTest +class StudentRepositoryTest { + + @Autowired + StudentRepository studentRepository; + + @Autowired + SchoolRepository schoolRepository; + + @Autowired + MemberRepository memberRepository; + + @Test + void 멤버id로_조회() { + // given + Member member = memberRepository.save(MemberFixture.member().build()); + School school = schoolRepository.save(SchoolFixture.school().build()); + Student student = studentRepository.save(StudentFixture.student().school(school).member(member).build()); + + // when + Optional actual = studentRepository.findByMemberIdWithFetch(member.getId()); + + // then + assertSoftly(softly -> { + softly.assertThat(actual).isNotEmpty(); + softly.assertThat(actual.get().getId()).isEqualTo(student.getId()); + }); + } +} diff --git a/backend/src/test/java/com/festago/application/integration/ApplicationIntegrationTest.java b/backend/src/test/java/com/festago/support/ApplicationIntegrationTest.java similarity index 77% rename from backend/src/test/java/com/festago/application/integration/ApplicationIntegrationTest.java rename to backend/src/test/java/com/festago/support/ApplicationIntegrationTest.java index e24b3510d..6bd69a561 100644 --- a/backend/src/test/java/com/festago/application/integration/ApplicationIntegrationTest.java +++ b/backend/src/test/java/com/festago/support/ApplicationIntegrationTest.java @@ -1,6 +1,5 @@ -package com.festago.application.integration; +package com.festago.support; -import com.festago.support.DatabaseClearExtension; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; diff --git a/backend/src/test/java/com/festago/support/StudentFixture.java b/backend/src/test/java/com/festago/support/StudentFixture.java new file mode 100644 index 000000000..d57e0fa8a --- /dev/null +++ b/backend/src/test/java/com/festago/support/StudentFixture.java @@ -0,0 +1,44 @@ +package com.festago.support; + +import com.festago.member.domain.Member; +import com.festago.school.domain.School; +import com.festago.student.domain.Student; + +public class StudentFixture { + + private Long id; + private Member member = MemberFixture.member().build(); + private School school = SchoolFixture.school().build(); + private String username = "xxeol2"; + + private StudentFixture() { + } + + public static StudentFixture student() { + return new StudentFixture(); + } + + public StudentFixture id(Long id) { + this.id = id; + return this; + } + + public StudentFixture member(Member member) { + this.member = member; + return this; + } + + public StudentFixture school(School school) { + this.school = school; + return this; + } + + public StudentFixture username(String username) { + this.username = username; + return this; + } + + public Student build() { + return new Student(id, member, school, username); + } +} diff --git a/backend/src/test/java/com/festago/application/TicketServiceTest.java b/backend/src/test/java/com/festago/ticket/application/TicketServiceTest.java similarity index 92% rename from backend/src/test/java/com/festago/application/TicketServiceTest.java rename to backend/src/test/java/com/festago/ticket/application/TicketServiceTest.java index cca7c198a..5ba1bc2fe 100644 --- a/backend/src/test/java/com/festago/application/TicketServiceTest.java +++ b/backend/src/test/java/com/festago/ticket/application/TicketServiceTest.java @@ -1,4 +1,4 @@ -package com.festago.application; +package com.festago.ticket.application; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -6,7 +6,6 @@ import com.festago.stage.domain.Stage; import com.festago.support.StageFixture; import com.festago.support.TicketFixture; -import com.festago.ticket.application.TicketService; import com.festago.ticket.domain.Ticket; import com.festago.ticket.domain.TicketType; import com.festago.ticket.dto.StageTicketResponse; @@ -21,9 +20,9 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -@ExtendWith(MockitoExtension.class) @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") +@ExtendWith(MockitoExtension.class) class TicketServiceTest { @Mock @@ -41,7 +40,7 @@ class TicketServiceTest { TicketFixture.ticket().id(1L).ticketType(TicketType.STUDENT).stage(stage).build(), TicketFixture.ticket().id(2L).ticketType(TicketType.VISITOR).stage(stage).build() ); - given(ticketRepository.findAllByStageId(stageId)) + given(ticketRepository.findAllByStageIdWithFetch(stageId)) .willReturn(tickets); // when diff --git a/backend/src/test/java/com/festago/application/integration/TicketServiceIntegrationTest.java b/backend/src/test/java/com/festago/ticket/application/integration/TicketServiceIntegrationTest.java similarity index 97% rename from backend/src/test/java/com/festago/application/integration/TicketServiceIntegrationTest.java rename to backend/src/test/java/com/festago/ticket/application/integration/TicketServiceIntegrationTest.java index 8f50652b7..b77516f87 100644 --- a/backend/src/test/java/com/festago/application/integration/TicketServiceIntegrationTest.java +++ b/backend/src/test/java/com/festago/ticket/application/integration/TicketServiceIntegrationTest.java @@ -1,4 +1,4 @@ -package com.festago.application.integration; +package com.festago.ticket.application.integration; import static com.festago.common.exception.ErrorCode.STAGE_NOT_FOUND; import static org.assertj.core.api.Assertions.assertThat; @@ -12,6 +12,7 @@ import com.festago.school.repository.SchoolRepository; import com.festago.stage.domain.Stage; import com.festago.stage.repository.StageRepository; +import com.festago.support.ApplicationIntegrationTest; import com.festago.support.FestivalFixture; import com.festago.support.SchoolFixture; import com.festago.support.StageFixture; diff --git a/backend/src/test/java/com/festago/domain/TicketAmountTest.java b/backend/src/test/java/com/festago/ticket/domain/TicketAmountTest.java similarity index 92% rename from backend/src/test/java/com/festago/domain/TicketAmountTest.java rename to backend/src/test/java/com/festago/ticket/domain/TicketAmountTest.java index 167eff3c6..2e4828884 100644 --- a/backend/src/test/java/com/festago/domain/TicketAmountTest.java +++ b/backend/src/test/java/com/festago/ticket/domain/TicketAmountTest.java @@ -1,10 +1,9 @@ -package com.festago.domain; +package com.festago.ticket.domain; import static com.festago.common.exception.ErrorCode.TICKET_SOLD_OUT; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.festago.common.exception.BadRequestException; -import com.festago.ticket.domain.TicketAmount; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/festago/ticket/domain/TicketEntryTimeTest.java b/backend/src/test/java/com/festago/ticket/domain/TicketEntryTimeTest.java new file mode 100644 index 000000000..d4aa10e4d --- /dev/null +++ b/backend/src/test/java/com/festago/ticket/domain/TicketEntryTimeTest.java @@ -0,0 +1,27 @@ +package com.festago.ticket.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.festago.common.exception.ValidException; +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class TicketEntryTimeTest { + + @ParameterizedTest + @ValueSource(ints = {0, -1}) + void 총_수량이_0_또는_음수일_경우_에외(int amount) { + // given + LocalDateTime now = LocalDateTime.now(); + + // when & then + assertThatThrownBy(() -> new TicketEntryTime(now, amount)) + .isInstanceOf(ValidException.class) + .hasMessage("amount은/는 1 이상이어야 합니다."); + } +} diff --git a/backend/src/test/java/com/festago/domain/TicketTest.java b/backend/src/test/java/com/festago/ticket/domain/TicketTest.java similarity index 98% rename from backend/src/test/java/com/festago/domain/TicketTest.java rename to backend/src/test/java/com/festago/ticket/domain/TicketTest.java index d22c54593..c63bf3b87 100644 --- a/backend/src/test/java/com/festago/domain/TicketTest.java +++ b/backend/src/test/java/com/festago/ticket/domain/TicketTest.java @@ -1,4 +1,4 @@ -package com.festago.domain; +package com.festago.ticket.domain; import static com.festago.common.exception.ErrorCode.EARLY_TICKET_ENTRY_THAN_OPEN; import static com.festago.common.exception.ErrorCode.EARLY_TICKET_ENTRY_TIME; @@ -18,7 +18,6 @@ import com.festago.support.MemberFixture; import com.festago.support.StageFixture; import com.festago.support.TicketFixture; -import com.festago.ticket.domain.Ticket; import com.festago.ticketing.domain.MemberTicket; import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayNameGeneration; @@ -189,7 +188,7 @@ class 예매_티켓_생성 { } @ParameterizedTest - @ValueSource(ints = {0, 100}) + @ValueSource(ints = {1, 100}) void 성공(int reservationSequence) { // given LocalDateTime stageStartTime = LocalDateTime.parse("2022-08-12T18:00:00"); diff --git a/backend/src/test/java/com/festago/domain/TicketRepositoryTest.java b/backend/src/test/java/com/festago/ticket/repository/TicketRepositoryTest.java similarity index 92% rename from backend/src/test/java/com/festago/domain/TicketRepositoryTest.java rename to backend/src/test/java/com/festago/ticket/repository/TicketRepositoryTest.java index 9c9361b71..c093a5b69 100644 --- a/backend/src/test/java/com/festago/domain/TicketRepositoryTest.java +++ b/backend/src/test/java/com/festago/ticket/repository/TicketRepositoryTest.java @@ -1,4 +1,4 @@ -package com.festago.domain; +package com.festago.ticket.repository; import static org.assertj.core.api.Assertions.assertThat; @@ -14,7 +14,6 @@ import com.festago.support.TicketFixture; import com.festago.ticket.domain.Ticket; import com.festago.ticket.domain.TicketType; -import com.festago.ticket.repository.TicketRepository; import java.util.List; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; @@ -52,7 +51,7 @@ class TicketRepositoryTest { ticketRepository.save(TicketFixture.ticket().stage(otherStage).build()); // when - List actual = ticketRepository.findAllByStageId(stage.getId()); + List actual = ticketRepository.findAllByStageIdWithFetch(stage.getId()); // then assertThat(actual).hasSize(2); diff --git a/backend/src/test/java/com/festago/application/MemberTicketServiceTest.java b/backend/src/test/java/com/festago/ticketing/application/MemberTicketServiceTest.java similarity index 98% rename from backend/src/test/java/com/festago/application/MemberTicketServiceTest.java rename to backend/src/test/java/com/festago/ticketing/application/MemberTicketServiceTest.java index 31f515d3f..3cd3fdbcf 100644 --- a/backend/src/test/java/com/festago/application/MemberTicketServiceTest.java +++ b/backend/src/test/java/com/festago/ticketing/application/MemberTicketServiceTest.java @@ -1,4 +1,4 @@ -package com.festago.application; +package com.festago.ticketing.application; import static com.festago.common.exception.ErrorCode.MEMBER_NOT_FOUND; import static com.festago.common.exception.ErrorCode.MEMBER_TICKET_NOT_FOUND; @@ -6,8 +6,8 @@ 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.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.anyLong; import static org.mockito.BDDMockito.given; import com.festago.common.exception.BadRequestException; @@ -18,7 +18,6 @@ import com.festago.support.MemberFixture; import com.festago.support.MemberTicketFixture; import com.festago.support.StageFixture; -import com.festago.ticketing.application.MemberTicketService; import com.festago.ticketing.domain.MemberTicket; import com.festago.ticketing.dto.MemberTicketResponse; import com.festago.ticketing.dto.MemberTicketsResponse; @@ -39,9 +38,9 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -@ExtendWith(MockitoExtension.class) @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") +@ExtendWith(MockitoExtension.class) class MemberTicketServiceTest { @Mock diff --git a/backend/src/test/java/com/festago/application/integration/MemberTicketIntegrationTest.java b/backend/src/test/java/com/festago/ticketing/application/integration/MemberTicketIntegrationTest.java similarity index 95% rename from backend/src/test/java/com/festago/application/integration/MemberTicketIntegrationTest.java rename to backend/src/test/java/com/festago/ticketing/application/integration/MemberTicketIntegrationTest.java index 275dc210b..5a0c814e2 100644 --- a/backend/src/test/java/com/festago/application/integration/MemberTicketIntegrationTest.java +++ b/backend/src/test/java/com/festago/ticketing/application/integration/MemberTicketIntegrationTest.java @@ -1,4 +1,4 @@ -package com.festago.application.integration; +package com.festago.ticketing.application.integration; import static org.assertj.core.api.Assertions.assertThat; @@ -10,6 +10,7 @@ import com.festago.school.repository.SchoolRepository; import com.festago.stage.domain.Stage; import com.festago.stage.repository.StageRepository; +import com.festago.support.ApplicationIntegrationTest; import com.festago.support.FestivalFixture; import com.festago.support.MemberFixture; import com.festago.support.MemberTicketFixture; diff --git a/backend/src/test/java/com/festago/application/integration/TicketingServiceIntegrationTest.java b/backend/src/test/java/com/festago/ticketing/application/integration/TicketingServiceIntegrationTest.java similarity index 96% rename from backend/src/test/java/com/festago/application/integration/TicketingServiceIntegrationTest.java rename to backend/src/test/java/com/festago/ticketing/application/integration/TicketingServiceIntegrationTest.java index 78fc657a8..adbae8124 100644 --- a/backend/src/test/java/com/festago/application/integration/TicketingServiceIntegrationTest.java +++ b/backend/src/test/java/com/festago/ticketing/application/integration/TicketingServiceIntegrationTest.java @@ -1,4 +1,4 @@ -package com.festago.application.integration; +package com.festago.ticketing.application.integration; import static com.festago.common.exception.ErrorCode.RESERVE_TICKET_OVER_AMOUNT; import static org.assertj.core.api.Assertions.assertThat; @@ -11,6 +11,7 @@ import com.festago.member.repository.MemberRepository; import com.festago.school.repository.SchoolRepository; import com.festago.stage.domain.Stage; +import com.festago.support.ApplicationIntegrationTest; import com.festago.support.MemberFixture; import com.festago.ticketing.application.TicketingService; import com.festago.ticketing.dto.TicketingRequest; diff --git a/backend/src/test/java/com/festago/domain/EntryStateTest.java b/backend/src/test/java/com/festago/ticketing/domain/EntryStateTest.java similarity index 70% rename from backend/src/test/java/com/festago/domain/EntryStateTest.java rename to backend/src/test/java/com/festago/ticketing/domain/EntryStateTest.java index 1ea7bc851..23352d449 100644 --- a/backend/src/test/java/com/festago/domain/EntryStateTest.java +++ b/backend/src/test/java/com/festago/ticketing/domain/EntryStateTest.java @@ -1,11 +1,9 @@ -package com.festago.domain; +package com.festago.ticketing.domain; -import static com.festago.common.exception.ErrorCode.INVALID_ENTRY_STATE_INDEX; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.festago.common.exception.InternalServerException; -import com.festago.ticketing.domain.EntryState; +import com.festago.common.exception.UnexpectedException; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; @@ -21,16 +19,16 @@ class EntryStateTest { void 유효하지않은_인덱스로_생성시_예외(int index) { // when & then assertThatThrownBy(() -> EntryState.from(index)) - .isInstanceOf(InternalServerException.class) - .hasMessage(INVALID_ENTRY_STATE_INDEX.getMessage()); + .isInstanceOf(UnexpectedException.class) + .hasMessageStartingWith("entryState의 인덱스가 올바르지 않습니다."); } @Test void 인덱스로_생성시_인자가_null이면_예외() { // when & then assertThatThrownBy(() -> EntryState.from(null)) - .isInstanceOf(InternalServerException.class) - .hasMessage(INVALID_ENTRY_STATE_INDEX.getMessage()); + .isInstanceOf(UnexpectedException.class) + .hasMessage("entryState의 인덱스는 null이 될 수 없습니다."); } @ParameterizedTest diff --git a/backend/src/test/java/com/festago/domain/MemberTicketTest.java b/backend/src/test/java/com/festago/ticketing/domain/MemberTicketTest.java similarity index 98% rename from backend/src/test/java/com/festago/domain/MemberTicketTest.java rename to backend/src/test/java/com/festago/ticketing/domain/MemberTicketTest.java index 9189725ca..97858e740 100644 --- a/backend/src/test/java/com/festago/domain/MemberTicketTest.java +++ b/backend/src/test/java/com/festago/ticketing/domain/MemberTicketTest.java @@ -1,4 +1,4 @@ -package com.festago.domain; +package com.festago.ticketing.domain; import static com.festago.ticketing.domain.EntryState.AFTER_ENTRY; import static com.festago.ticketing.domain.EntryState.AWAY; @@ -11,7 +11,6 @@ import com.festago.support.FestivalFixture; import com.festago.support.MemberTicketFixture; import com.festago.support.StageFixture; -import com.festago.ticketing.domain.MemberTicket; import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; @@ -203,6 +202,5 @@ class 티켓_주인_검사 { // when && then assertThat(memberTicket.isOwner(memberId)).isFalse(); } - } } diff --git a/backend/src/test/java/com/festago/domain/MemberTicketRepositoryTest.java b/backend/src/test/java/com/festago/ticketing/repository/MemberTicketRepositoryTest.java similarity index 98% rename from backend/src/test/java/com/festago/domain/MemberTicketRepositoryTest.java rename to backend/src/test/java/com/festago/ticketing/repository/MemberTicketRepositoryTest.java index bafb2b838..5e78bc0c7 100644 --- a/backend/src/test/java/com/festago/domain/MemberTicketRepositoryTest.java +++ b/backend/src/test/java/com/festago/ticketing/repository/MemberTicketRepositoryTest.java @@ -1,4 +1,4 @@ -package com.festago.domain; +package com.festago.ticketing.repository; import static org.assertj.core.api.Assertions.assertThat; @@ -19,7 +19,6 @@ import com.festago.support.StageFixture; import com.festago.ticket.repository.TicketRepository; import com.festago.ticketing.domain.MemberTicket; -import com.festago.ticketing.repository.MemberTicketRepository; import java.util.ArrayList; import java.util.Comparator; import java.util.List; diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml index 44d7f0166..6895504ad 100644 --- a/backend/src/test/resources/application-test.yml +++ b/backend/src/test/resources/application-test.yml @@ -2,10 +2,7 @@ spring: datasource: url: jdbc:h2:mem:test;MODE=MYSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE jpa: - properties: - hibernate: - format_sql: true - show-sql: true + show-sql: false hibernate: ddl-auto: create open-in-view: false @@ -15,12 +12,6 @@ spring: logging: file: path: ./ - level: - org: - hibernate: - orm: - jdbc: - bind: trace festago: qr-secret-key: festagofestagofestagofestagofestagofestagofestagofestagofestagofestagofestagofestagofestagofestago