diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4cd178d07..2171681ed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,6 +36,7 @@ android { } buildFeatures { viewBinding = true + dataBinding = true } } @@ -50,4 +51,8 @@ dependencies { testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + debugImplementation("androidx.fragment:fragment-testing:1.5.7") + debugImplementation("androidx.fragment:fragment-ktx:1.6.0-beta01") + testImplementation("io.mockk:mockk-android:1.13.5") + testImplementation("io.mockk:mockk-agent:1.13.5") } diff --git a/app/src/androidTest/java/woowacourse/movie/view/MovieMainActivityTest.kt b/app/src/androidTest/java/woowacourse/movie/view/MovieMainActivityTest.kt index c755a402b..ac03331b4 100644 --- a/app/src/androidTest/java/woowacourse/movie/view/MovieMainActivityTest.kt +++ b/app/src/androidTest/java/woowacourse/movie/view/MovieMainActivityTest.kt @@ -2,16 +2,18 @@ package woowacourse.movie.view import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import woowacourse.movie.R import woowacourse.movie.view.moviemain.MovieMainActivity +import woowacourse.movie.view.moviemain.movielist.MovieListFragment +import woowacourse.movie.view.moviemain.reservationlist.ReservationListFragment +import woowacourse.movie.view.moviemain.setting.SettingFragment @RunWith(AndroidJUnit4::class) class MovieMainActivityTest { @@ -21,18 +23,27 @@ class MovieMainActivityTest { @Test fun 예매내역_버튼을_누르면_예매내역_Fragment로_바뀐다() { onView(withId(R.id.action_reservation_list)).perform(click()) - onView(withId(R.id.recyclerview)).check(matches(isDisplayed())) + mActivityTestRule.scenario.onActivity { + val fragment = it.supportFragmentManager.findFragmentByTag(ReservationListFragment.TAG_RESERVATION_LIST) + assertTrue(fragment != null && fragment.isVisible) + } } @Test fun 홈_버튼을_누르면_홈_Fragment로_바뀐다() { onView(withId(R.id.action_home)).perform(click()) - onView(withId(R.id.movie_recyclerview)).check(matches(isDisplayed())) + mActivityTestRule.scenario.onActivity { + val fragment = it.supportFragmentManager.findFragmentByTag(MovieListFragment.TAG_MOVIE_LIST) + assertTrue(fragment != null && fragment.isVisible) + } } @Test fun 설정_버튼을_누르면_설정_Fragment로_바뀐다() { onView(withId(R.id.action_setting)).perform(click()) - onView(withId(R.id.setting_toggle)).check(matches(isDisplayed())) + mActivityTestRule.scenario.onActivity { + val fragment = it.supportFragmentManager.findFragmentByTag(SettingFragment.TAG_SETTING) + assertTrue(fragment != null && fragment.isVisible) + } } } diff --git a/app/src/androidTest/java/woowacourse/movie/view/ReservationActivityTest.kt b/app/src/androidTest/java/woowacourse/movie/view/ReservationActivityTest.kt index c8d160553..f1b7bfa68 100644 --- a/app/src/androidTest/java/woowacourse/movie/view/ReservationActivityTest.kt +++ b/app/src/androidTest/java/woowacourse/movie/view/ReservationActivityTest.kt @@ -12,24 +12,40 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import woowacourse.movie.R -import woowacourse.movie.view.model.MovieListModel +import woowacourse.movie.domain.movie.Minute +import woowacourse.movie.domain.movie.Movie +import woowacourse.movie.domain.movie.Schedule +import woowacourse.movie.view.mapper.toUiModel +import woowacourse.movie.view.reservation.ReservationActivity import java.time.LocalDate +import java.time.LocalTime @RunWith(AndroidJUnit4::class) class ReservationActivityTest { - private val movie = MovieListModel.MovieUiModel( - "해리 포터와 마법사의 돌", + private val movie = Movie( + "스즈메의 문단속", LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 31), - 152, - R.drawable.harry_potter1_poster, - "《해리 포터와 마법사의 돌》은 2001년 J. K. 롤링의 동명 소설을 원작으로 하여 만든, 영국과 미국 합작, 판타지 영화이다. 해리포터 시리즈 영화 8부작 중 첫 번째에 해당하는 작품이다. 크리스 콜럼버스가 감독을 맡았다." + Minute(122), + "“이 근처에 폐허 없니? 문을 찾고 있어” 규슈의 한적한 마을에 살고 있는 소녀 ‘스즈메’는 문을 찾아 여행 중인 청년 ‘소타’를 만난다. " + + "그의 뒤를 쫓아 산속 폐허에서 발견한 낡은 문. ‘스즈메’가 무언가에 이끌리듯 문을 열자 마을에 재난의 위기가 닥쳐오고 가문 대대로 문 너머의 재난을 봉인하는 ‘소타’를 도와 간신히 문을 닫는다. " + + "“닫아야만 하잖아요, 여기를!” 재난을 막았다는 안도감도 잠시, 수수께끼의 고양이 ‘다이진’이 나타나 ‘소타’를 의자로 바꿔 버리고 일본 각지의 폐허에 재난을 부르는 문이 열리기 시작하자 ‘스즈메’는 의자가 된 ‘소타’와 함께 재난을 막기 위한 여정에 나선다." + + "“꿈이 아니었어” 규슈, 시코쿠, 고베, 도쿄 재난을 막기 위해 일본 전역을 돌며 필사적으로 문을 닫아가던 중 어릴 적 고향에 닿은 ‘스즈메’는 잊고 있던 진실과 마주하게 되는데…", + Schedule( + mapOf( + "정말아주아주아주아주아주아주아주긴극장이름" to listOf(LocalTime.of(13, 0), LocalTime.of(15, 0), LocalTime.of(17, 0), LocalTime.of(19, 0)), + "선릉 극장" to listOf(LocalTime.of(13, 0), LocalTime.of(15, 0), LocalTime.of(17, 0), LocalTime.of(19, 0)), + "잠실 극장" to listOf(LocalTime.of(9, 0)), + "강남 극장" to listOf(LocalTime.of(12, 0), LocalTime.of(14, 0), LocalTime.of(16, 0)), + ), + ), ) private val intent = ReservationActivity.newIntent( ApplicationProvider.getApplicationContext(), - movie + movie.toUiModel(), + "강남 극장", ) @get:Rule @@ -37,7 +53,7 @@ class ReservationActivityTest { @Test fun 영화_제목을_표시한다() { - onView(withId(R.id.movie_title)).check(matches(withText("해리 포터와 마법사의 돌"))) + onView(withId(R.id.movie_title)).check(matches(withText("스즈메의 문단속"))) } @Test diff --git a/app/src/androidTest/java/woowacourse/movie/view/SeatSelectionActivityTest.kt b/app/src/androidTest/java/woowacourse/movie/view/SeatSelectionActivityTest.kt index e328fbf1f..f351185fa 100644 --- a/app/src/androidTest/java/woowacourse/movie/view/SeatSelectionActivityTest.kt +++ b/app/src/androidTest/java/woowacourse/movie/view/SeatSelectionActivityTest.kt @@ -17,8 +17,9 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import woowacourse.movie.R -import woowacourse.movie.domain.Minute -import woowacourse.movie.domain.Movie +import woowacourse.movie.domain.movie.Minute +import woowacourse.movie.domain.movie.Movie +import woowacourse.movie.domain.movie.Schedule import woowacourse.movie.view.mapper.toUiModel import woowacourse.movie.view.model.ReservationOptions import woowacourse.movie.view.seatselection.SeatSelectionActivity @@ -31,24 +32,35 @@ import java.time.LocalTime class SeatSelectionActivityTest { private val reservationOptions = ReservationOptions( - "해리 포터와 마법사의 돌", - LocalDateTime.of(LocalDate.of(2024, 3, 1), LocalTime.of(13, 0)), - 2 + "스즈메의 문단속", + LocalDateTime.of(LocalDate.of(2024, 3, 1), LocalTime.of(14, 0)), + 2, + "강남 극장", ) private val movie = Movie( - "해리 포터와 마법사의 돌", + "스즈메의 문단속", LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 31), - Minute(152), - R.drawable.harry_potter1_poster, - "《해리 포터와 마법사의 돌》은 2001년 J. K. 롤링의 동명 소설을 원작으로 하여 만든, 영국과 미국 합작, 판타지 영화이다. 해리포터 시리즈 영화 8부작 중 첫 번째에 해당하는 작품이다. 크리스 콜럼버스가 감독을 맡았다." + Minute(122), + "“이 근처에 폐허 없니? 문을 찾고 있어” 규슈의 한적한 마을에 살고 있는 소녀 ‘스즈메’는 문을 찾아 여행 중인 청년 ‘소타’를 만난다. " + + "그의 뒤를 쫓아 산속 폐허에서 발견한 낡은 문. ‘스즈메’가 무언가에 이끌리듯 문을 열자 마을에 재난의 위기가 닥쳐오고 가문 대대로 문 너머의 재난을 봉인하는 ‘소타’를 도와 간신히 문을 닫는다. " + + "“닫아야만 하잖아요, 여기를!” 재난을 막았다는 안도감도 잠시, 수수께끼의 고양이 ‘다이진’이 나타나 ‘소타’를 의자로 바꿔 버리고 일본 각지의 폐허에 재난을 부르는 문이 열리기 시작하자 ‘스즈메’는 의자가 된 ‘소타’와 함께 재난을 막기 위한 여정에 나선다." + + "“꿈이 아니었어” 규슈, 시코쿠, 고베, 도쿄 재난을 막기 위해 일본 전역을 돌며 필사적으로 문을 닫아가던 중 어릴 적 고향에 닿은 ‘스즈메’는 잊고 있던 진실과 마주하게 되는데…", + Schedule( + mapOf( + "정말아주아주아주아주아주아주아주긴극장이름" to listOf(LocalTime.of(13, 0), LocalTime.of(15, 0), LocalTime.of(17, 0), LocalTime.of(19, 0)), + "선릉 극장" to listOf(LocalTime.of(13, 0), LocalTime.of(15, 0), LocalTime.of(17, 0), LocalTime.of(19, 0)), + "잠실 극장" to listOf(LocalTime.of(9, 0)), + "강남 극장" to listOf(LocalTime.of(12, 0), LocalTime.of(14, 0), LocalTime.of(16, 0)), + ), + ), ) private val intent = SeatSelectionActivity.newIntent( ApplicationProvider.getApplicationContext(), reservationOptions, - movie.toUiModel() + movie.toUiModel(), ) @get:Rule @@ -56,8 +68,8 @@ class SeatSelectionActivityTest { @Test fun 영화_제목을_표시한다() { - onView(withId(R.id.movie_title_textview)) - .check(matches(withText("해리 포터와 마법사의 돌"))) + onView(withId(R.id.text_title)) + .check(matches(withText("스즈메의 문단속"))) } @Test @@ -74,7 +86,7 @@ class SeatSelectionActivityTest { @Test fun 인원수에_해당하는_좌석이_모두_선택되지_않았다면_확인_버튼은_비활성화_상태다() { onView(withText("A1")).perform(click()) - onView(withId(R.id.confirm_reservation_button)) + onView(withId(R.id.btn_next)) .check(matches(isNotEnabled())) } @@ -82,7 +94,7 @@ class SeatSelectionActivityTest { fun 인원수에_해당하는_좌석이_모두_선택되었다면_확인_버튼은_활성화_상태다() { onView(withText("A1")).perform(click()) onView(withText("A2")).perform(click()) - onView(withId(R.id.confirm_reservation_button)) + onView(withId(R.id.btn_next)) .check(matches(isEnabled())) } @@ -90,14 +102,14 @@ class SeatSelectionActivityTest { fun 인원수에_해당하는_좌석이_모두_선택되었다면_최종_금액이_표시된다() { onView(withText("A1")).perform(click()) onView(withText("A2")).perform(click()) - onView(withId(R.id.reservation_fee_textview)).check(matches(withText("20,000원"))) + onView(withId(R.id.text_price)).check(matches(withText("20,000원"))) } @Test - fun 좌석_선택을_해제하여_인원수에_해당하는_좌석이_모두_선택되지_않았다면_최종_금액은_0원으로_표시된다() { + fun 좌석을_선택할_때_가격을_갱신한다() { onView(withText("A1")).perform(click()) + onView(withId(R.id.text_price)).check(matches(withText("10,000원"))) onView(withText("A2")).perform(click()) - onView(withText("A1")).perform(click()) - onView(withId(R.id.reservation_fee_textview)).check(matches(withText("0원"))) + onView(withId(R.id.text_price)).check(matches(withText("20,000원"))) } } diff --git a/app/src/androidTest/java/woowacourse/movie/view/SettingFragmentTest.kt b/app/src/androidTest/java/woowacourse/movie/view/SettingFragmentTest.kt index 79ae36f48..21b5c6013 100644 --- a/app/src/androidTest/java/woowacourse/movie/view/SettingFragmentTest.kt +++ b/app/src/androidTest/java/woowacourse/movie/view/SettingFragmentTest.kt @@ -1,61 +1,35 @@ package woowacourse.movie.view -import android.content.Context -import android.content.SharedPreferences -import androidx.fragment.app.commit -import androidx.preference.PreferenceManager -import androidx.test.core.app.ActivityScenario +import androidx.fragment.app.testing.launchFragmentInContainer import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Before -import org.junit.Rule +import com.google.android.material.switchmaterial.SwitchMaterial import org.junit.Test import org.junit.runner.RunWith import woowacourse.movie.R -import woowacourse.movie.view.moviemain.MovieMainActivity import woowacourse.movie.view.moviemain.setting.SettingFragment @RunWith(AndroidJUnit4::class) class SettingFragmentTest { - @get:Rule - val mActivityTestRule = ActivityScenarioRule(MovieMainActivity::class.java) - - @Before - fun setup() { - ActivityScenario.launch(MovieMainActivity::class.java).onActivity { - it.supportFragmentManager.commit { - replace(R.id.fragment_container_view, SettingFragment()) - } - } - } - - @Test - fun 설정_Fragment에서_저장된_세팅값이_false면_토글도_꺼져있다() { - setSharedPreferences(false) - onView(ViewMatchers.withId(R.id.action_setting)).perform(ViewActions.click()) - onView(ViewMatchers.withId(R.id.setting_toggle)) - .check(ViewAssertions.matches(ViewMatchers.isNotChecked())) - } + private val scenario = launchFragmentInContainer(themeResId = R.style.Theme_Movie) @Test - fun 설정_Fragment에서_저장된_세팅값이_true면_토글도_켜져있다() { - setSharedPreferences(true) - onView(ViewMatchers.withId(R.id.action_setting)).perform(ViewActions.click()) - onView(ViewMatchers.withId(R.id.setting_toggle)) - .check(ViewAssertions.matches(ViewMatchers.isChecked())) - } - - private fun setSharedPreferences(isAlarmOn: Boolean) { - val targetContext: Context = InstrumentationRegistry.getInstrumentation().targetContext - - val preferencesEditor: SharedPreferences.Editor = PreferenceManager.getDefaultSharedPreferences(targetContext).edit() - preferencesEditor.clear() - preferencesEditor.putBoolean(SettingFragment.IS_ALARM_ON, isAlarmOn) - preferencesEditor.commit() + fun 토글의_설정값이_유지된다() { + onView(ViewMatchers.withId(R.id.setting_toggle)).perform(click()) + var isChecked = false + scenario.onFragment { + isChecked = it.requireView().findViewById(R.id.setting_toggle).isChecked + } + scenario.recreate() + if (isChecked) { + onView(ViewMatchers.withId(R.id.setting_toggle)) + .check(ViewAssertions.matches(ViewMatchers.isChecked())).perform(click()) + } else { + onView(ViewMatchers.withId(R.id.setting_toggle)) + .check(ViewAssertions.matches(ViewMatchers.isNotChecked())).perform(click()) + } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4600f42a3..2264434a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ android:theme="@style/Theme.Movie" tools:targetApi="31" > @@ -23,10 +23,10 @@ android:name=".view.seatselection.SeatSelectionActivity" android:exported="false" /> { - return movies.toList() - } -} diff --git a/app/src/main/java/woowacourse/movie/data/movie/MovieMockRepository.kt b/app/src/main/java/woowacourse/movie/data/movie/MovieMockRepository.kt new file mode 100644 index 000000000..1ff47ed81 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/data/movie/MovieMockRepository.kt @@ -0,0 +1,74 @@ +package woowacourse.movie.data.movie + +import woowacourse.movie.domain.movie.Minute +import woowacourse.movie.domain.movie.Movie +import woowacourse.movie.domain.movie.Schedule +import woowacourse.movie.domain.repository.MovieRepository +import java.time.LocalDate +import java.time.LocalTime + +object MovieMockRepository : MovieRepository { + private val movies = listOf( + Movie( + "스즈메의 문단속", + LocalDate.of(2024, 3, 1), + LocalDate.of(2024, 3, 31), + Minute(122), + "“이 근처에 폐허 없니? 문을 찾고 있어” 규슈의 한적한 마을에 살고 있는 소녀 ‘스즈메’는 문을 찾아 여행 중인 청년 ‘소타’를 만난다. " + + "그의 뒤를 쫓아 산속 폐허에서 발견한 낡은 문. ‘스즈메’가 무언가에 이끌리듯 문을 열자 마을에 재난의 위기가 닥쳐오고 가문 대대로 문 너머의 재난을 봉인하는 ‘소타’를 도와 간신히 문을 닫는다. " + + "“닫아야만 하잖아요, 여기를!” 재난을 막았다는 안도감도 잠시, 수수께끼의 고양이 ‘다이진’이 나타나 ‘소타’를 의자로 바꿔 버리고 일본 각지의 폐허에 재난을 부르는 문이 열리기 시작하자 ‘스즈메’는 의자가 된 ‘소타’와 함께 재난을 막기 위한 여정에 나선다." + + "“꿈이 아니었어” 규슈, 시코쿠, 고베, 도쿄 재난을 막기 위해 일본 전역을 돌며 필사적으로 문을 닫아가던 중 어릴 적 고향에 닿은 ‘스즈메’는 잊고 있던 진실과 마주하게 되는데…", + Schedule( + mapOf( + "정말아주아주아주아주아주아주아주긴극장이름" to listOf(LocalTime.of(13, 0), LocalTime.of(15, 0), LocalTime.of(17, 0), LocalTime.of(19, 0)), + "선릉 극장" to listOf(LocalTime.of(13, 0), LocalTime.of(15, 0), LocalTime.of(17, 0), LocalTime.of(19, 0)), + "잠실 극장" to listOf(LocalTime.of(9, 0)), + "강남 극장" to listOf(LocalTime.of(12, 0), LocalTime.of(14, 0), LocalTime.of(16, 0)) + ) + ), + ), + Movie( + "해리 포터와 마법사의 돌", + LocalDate.of(2024, 4, 1), + LocalDate.of(2024, 4, 15), + Minute(152), + "《해리 포터와 마법사의 돌》은 2001년 J. K. 롤링의 동명 소설을 원작으로 하여 만든, 영국과 미국 합작, 판타지 영화이다. 해리포터 시리즈 영화 8부작 중 첫 번째에 해당하는 작품이다. 크리스 콜럼버스가 감독을 맡았다.", + Schedule( + mapOf( + "선릉 극장" to listOf(LocalTime.of(13, 0), LocalTime.of(15, 0), LocalTime.of(17, 0), LocalTime.of(19, 0)), + "잠실 극장" to listOf(LocalTime.of(10, 0), LocalTime.of(12, 0), LocalTime.of(14, 0)), + ), + ), + ), + Movie( + "스타워즈", + LocalDate.of(2024, 4, 16), + LocalDate.of(2024, 4, 30), + Minute(152), + "디즈니+ 오리지널 스타워즈 실사 드라마 만달로리안 시리즈의 세 번째 시즌. 본격적으로 만달로어인들이 결집하는 과정을 그리고 있는 시즌이다.", + Schedule( + mapOf( + "선릉 극장" to listOf(LocalTime.of(9, 0), LocalTime.of(11, 0), LocalTime.of(15, 0)), + "강남 극장" to listOf(LocalTime.of(12, 0), LocalTime.of(14, 0), LocalTime.of(16, 0)) + ), + ), + ), + Movie( + "어벤져스: 엔드게임", + LocalDate.of(2024, 5, 1), + LocalDate.of(2024, 5, 30), + Minute(152), + "어벤져스 실사영화 시리즈의 4번째 작품이자, 마블 시네마틱 유니버스의 스물두번째 작품이며, 페이즈 3의 10번째 작품이자 지난 2008년, 아이언맨을 시작으로 장장 11년 동안 이어져 왔던 인피니티 사가의 최종편.", + Schedule( + mapOf( + "선릉 극장" to listOf(LocalTime.of(9, 0), LocalTime.of(11, 0), LocalTime.of(15, 0)), + "잠실 극장" to listOf(LocalTime.of(11, 0), LocalTime.of(14, 0)), + ), + ), + ), + ) + + override fun findAll(): List { + return movies.toList() + } +} diff --git a/app/src/main/java/woowacourse/movie/data/reservation/ReservationConstract.kt b/app/src/main/java/woowacourse/movie/data/reservation/ReservationConstract.kt new file mode 100644 index 000000000..03d7edf3a --- /dev/null +++ b/app/src/main/java/woowacourse/movie/data/reservation/ReservationConstract.kt @@ -0,0 +1,12 @@ +package woowacourse.movie.data.reservation + +import android.provider.BaseColumns + +object ReservationConstract : BaseColumns { + const val TABLE_NAME = "reservation" + const val TABLE_COLUMN_TITLE = "title" + const val TABLE_COLUMN_SCREENING_TIME = "screening_time" + const val TABLE_COLUMN_SEATS = "seats" + const val TABLE_COLUMN_PRICE = "price" + const val TABLE_COLUMN_THEATER = "theater" +} diff --git a/app/src/main/java/woowacourse/movie/data/reservation/ReservationDbHelper.kt b/app/src/main/java/woowacourse/movie/data/reservation/ReservationDbHelper.kt new file mode 100644 index 000000000..6fef83f40 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/data/reservation/ReservationDbHelper.kt @@ -0,0 +1,65 @@ +package woowacourse.movie.data.reservation + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import woowacourse.movie.domain.price.Price +import woowacourse.movie.domain.reservation.Reservation +import woowacourse.movie.util.DATETIME_FORMATTER +import woowacourse.movie.view.mapper.toDomain +import woowacourse.movie.view.mapper.toUiModel +import woowacourse.movie.view.model.SeatUiModel +import java.time.LocalDateTime + +class ReservationDbHelper(context: Context) : + SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + override fun onCreate(db: SQLiteDatabase?) { + db?.execSQL( + "CREATE TABLE ${ReservationConstract.TABLE_NAME} (" + + " ${ReservationConstract.TABLE_COLUMN_TITLE} varchar(30)," + + " ${ReservationConstract.TABLE_COLUMN_SCREENING_TIME} int," + + " ${ReservationConstract.TABLE_COLUMN_SEATS} text," + + " ${ReservationConstract.TABLE_COLUMN_PRICE} int," + + " ${ReservationConstract.TABLE_COLUMN_THEATER} varchar(100)" + + ");", + ) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + db?.execSQL("DROP TABLE IF EXISTS ${ReservationConstract.TABLE_NAME}") + onCreate(db) + } + + fun insert(reservation: Reservation) { + val values = ContentValues() + values.put(ReservationConstract.TABLE_COLUMN_TITLE, reservation.title) + values.put(ReservationConstract.TABLE_COLUMN_SCREENING_TIME, reservation.screeningDateTime.format(DATETIME_FORMATTER)) + values.put(ReservationConstract.TABLE_COLUMN_SEATS, reservation.seats.joinToString { it.toUiModel().seatId }) + values.put(ReservationConstract.TABLE_COLUMN_PRICE, reservation.price.price) + values.put(ReservationConstract.TABLE_COLUMN_THEATER, reservation.theaterName) + writableDatabase.insert(ReservationConstract.TABLE_NAME, null, values) + } + + fun selectReservations(): List { + val reservations = mutableListOf() + val sql = "select * from ${ReservationConstract.TABLE_NAME}" + val cursor: Cursor = readableDatabase.rawQuery(sql, null) + while (cursor.moveToNext()) { + val title: String = cursor.getString(cursor.getColumnIndexOrThrow(ReservationConstract.TABLE_COLUMN_TITLE)) + val screeningDateTimes: String = cursor.getString(cursor.getColumnIndexOrThrow(ReservationConstract.TABLE_COLUMN_SCREENING_TIME)) + val seats: String = cursor.getString(cursor.getColumnIndexOrThrow(ReservationConstract.TABLE_COLUMN_SEATS)) + val price: Int = cursor.getInt(cursor.getColumnIndexOrThrow(ReservationConstract.TABLE_COLUMN_PRICE)) + val theater: String = cursor.getString(cursor.getColumnIndexOrThrow(ReservationConstract.TABLE_COLUMN_THEATER)) + reservations.add(Reservation(title, LocalDateTime.parse(screeningDateTimes, DATETIME_FORMATTER), seats.split(",").map { SeatUiModel.of(it.trim()).toDomain() }, Price(price), theater)) + } + cursor.close() + return reservations + } + + companion object { + private const val DATABASE_NAME = "system" + private const val DATABASE_VERSION = 1 + } +} diff --git a/app/src/main/java/woowacourse/movie/data/reservation/ReservationDbRepository.kt b/app/src/main/java/woowacourse/movie/data/reservation/ReservationDbRepository.kt new file mode 100644 index 000000000..8158c63c3 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/data/reservation/ReservationDbRepository.kt @@ -0,0 +1,16 @@ +package woowacourse.movie.data.reservation + +import android.content.Context +import woowacourse.movie.domain.repository.ReservationRepository +import woowacourse.movie.domain.reservation.Reservation + +class ReservationDbRepository(context: Context) : ReservationRepository { + private val db = ReservationDbHelper(context) + override fun add(reservation: Reservation) { + db.insert(reservation) + } + + override fun findAll(): List { + return db.selectReservations() + } +} diff --git a/app/src/main/java/woowacourse/movie/data/ReservationMockRepository.kt b/app/src/main/java/woowacourse/movie/data/reservation/ReservationMockRepository.kt similarity index 79% rename from app/src/main/java/woowacourse/movie/data/ReservationMockRepository.kt rename to app/src/main/java/woowacourse/movie/data/reservation/ReservationMockRepository.kt index e044da84b..627cb3d9a 100644 --- a/app/src/main/java/woowacourse/movie/data/ReservationMockRepository.kt +++ b/app/src/main/java/woowacourse/movie/data/reservation/ReservationMockRepository.kt @@ -1,7 +1,7 @@ -package woowacourse.movie.data +package woowacourse.movie.data.reservation -import woowacourse.movie.domain.Reservation import woowacourse.movie.domain.repository.ReservationRepository +import woowacourse.movie.domain.reservation.Reservation object ReservationMockRepository : ReservationRepository { diff --git a/app/src/main/java/woowacourse/movie/data/setting/SettingPreferencesRepository.kt b/app/src/main/java/woowacourse/movie/data/setting/SettingPreferencesRepository.kt new file mode 100644 index 000000000..5d6c7a005 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/data/setting/SettingPreferencesRepository.kt @@ -0,0 +1,18 @@ +package woowacourse.movie.data.setting + +import android.content.Context +import androidx.preference.PreferenceManager +import woowacourse.movie.domain.repository.SettingRepository + +class SettingPreferencesRepository(context: Context) : SettingRepository { + private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + override fun getIsAlarmSetting(): Boolean = sharedPreferences.getBoolean(IS_ALARM_ON, false) + override fun setIsAlarmSetting(isOn: Boolean) { + val editor = sharedPreferences.edit() + editor.putBoolean(IS_ALARM_ON, isOn).apply() + } + + companion object { + const val IS_ALARM_ON = "IS_ALARM_ON" + } +} diff --git a/app/src/main/java/woowacourse/movie/data/theater/TheaterMockRepository.kt b/app/src/main/java/woowacourse/movie/data/theater/TheaterMockRepository.kt new file mode 100644 index 000000000..a3c4286f0 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/data/theater/TheaterMockRepository.kt @@ -0,0 +1,39 @@ +package woowacourse.movie.data.theater + +import woowacourse.movie.domain.policy.MorningPolicy +import woowacourse.movie.domain.policy.MovieDayPolicy +import woowacourse.movie.domain.policy.NightPolicy +import woowacourse.movie.domain.repository.TheaterRepository +import woowacourse.movie.domain.theater.Grade +import woowacourse.movie.domain.theater.Size +import woowacourse.movie.domain.theater.Theater + +object TheaterMockRepository : TheaterRepository { + private val rowGrade = mapOf( + 0 to Grade.B, + 1 to Grade.B, + 2 to Grade.S, + 3 to Grade.S, + 4 to Grade.A, + ) + private val policies = listOf( + MovieDayPolicy(), + MorningPolicy(), + NightPolicy(), + ) + + private val theaters: List = listOf( + Theater("정말아주아주아주아주아주아주아주긴극장이름", Size(5, 4), rowGrade, policies), + Theater("선릉 극장", Size(5, 4), rowGrade, policies), + Theater("잠실 극장", Size(5, 4), rowGrade, policies), + Theater("강남 극장", Size(5, 4), rowGrade, policies), + ) + + override fun findAll(): List { + return theaters + } + + override fun findTheater(name: String): Theater { + return requireNotNull(theaters.find { it.name == name }) { "존재하지 않는 상영관입니다." } + } +} diff --git a/app/src/main/java/woowacourse/movie/util/DateTimeFormatter.kt b/app/src/main/java/woowacourse/movie/util/DateTimeFormatter.kt index 8d0982c00..dc56b5b53 100644 --- a/app/src/main/java/woowacourse/movie/util/DateTimeFormatter.kt +++ b/app/src/main/java/woowacourse/movie/util/DateTimeFormatter.kt @@ -4,3 +4,4 @@ import java.time.format.DateTimeFormatter val DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") val TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm") +val DATETIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") diff --git a/app/src/main/java/woowacourse/movie/util/DecimalFormatter.kt b/app/src/main/java/woowacourse/movie/util/DecimalFormatter.kt new file mode 100644 index 000000000..10a55806d --- /dev/null +++ b/app/src/main/java/woowacourse/movie/util/DecimalFormatter.kt @@ -0,0 +1,5 @@ +package woowacourse.movie.util + +import java.text.DecimalFormat + +val DECIMAL_FORMAT = DecimalFormat("#,###") diff --git a/app/src/main/java/woowacourse/movie/util/MapExtension.kt b/app/src/main/java/woowacourse/movie/util/MapExtension.kt new file mode 100644 index 000000000..bf05fc2ae --- /dev/null +++ b/app/src/main/java/woowacourse/movie/util/MapExtension.kt @@ -0,0 +1,5 @@ +package woowacourse.movie.util + +fun Map>.getOrEmptyList(key: T): List { + return this[key] ?: emptyList() +} diff --git a/app/src/main/java/woowacourse/movie/util/ParcelableExtension.kt b/app/src/main/java/woowacourse/movie/util/ParcelableExtension.kt index 79e5b539b..2932510dd 100644 --- a/app/src/main/java/woowacourse/movie/util/ParcelableExtension.kt +++ b/app/src/main/java/woowacourse/movie/util/ParcelableExtension.kt @@ -13,10 +13,10 @@ inline fun Intent.getParcelableCompat(key: String): T? return getParcelableExtra(key) as? T } -inline fun Bundle.getSerializableCompat(key: String): T? { +inline fun Bundle.getParcelableCompat(key: String): T? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return getSerializable(key, T::class.java) + return getParcelable(key, T::class.java) } @Suppress("DEPRECATION") - return getSerializable(key) as? T + return getParcelable(key) as? T } diff --git a/app/src/main/java/woowacourse/movie/view/AlarmController.kt b/app/src/main/java/woowacourse/movie/view/AlarmController.kt index 9bc6bd018..d94f1f2d7 100644 --- a/app/src/main/java/woowacourse/movie/view/AlarmController.kt +++ b/app/src/main/java/woowacourse/movie/view/AlarmController.kt @@ -1,16 +1,18 @@ package woowacourse.movie.view import android.app.AlarmManager +import android.app.NotificationChannel +import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import woowacourse.movie.view.model.ReservationUiModel -import woowacourse.movie.view.seatselection.AlarmReceiver import java.time.ZoneId -class AlarmController( - private val context: Context -) { +class AlarmController(private val context: Context) { + init { + createChannel() + } fun registerAlarms(reservations: List, minuteInterval: Long) { reservations.forEach { @@ -20,7 +22,7 @@ class AlarmController( fun registerAlarm(reservation: ReservationUiModel, minuteInterval: Long) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - val pendingIntent = getPendingIntent(reservation) + val pendingIntent = getPendingIntent(AlarmReceiver.newIntent(context, reservation)) alarmManager.set( AlarmManager.RTC_WAKEUP, @@ -31,30 +33,27 @@ class AlarmController( ) } - private fun getPendingIntent(reservation: ReservationUiModel): PendingIntent { - return Intent(context, AlarmReceiver::class.java).let { - it.putExtra(AlarmReceiver.RESERVATION, reservation) - PendingIntent.getBroadcast( - context, - ALARM_REQUEST_CODE, - it, - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, - ) - } - } - fun cancelAlarms() { - val pendingIntent = PendingIntent.getBroadcast( - context, - ALARM_REQUEST_CODE, - Intent(context, AlarmReceiver::class.java), - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) + val pendingIntent = getPendingIntent(Intent(context, AlarmReceiver::class.java)) val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager alarmManager.cancel(pendingIntent) } + private fun createChannel() { + val channel = NotificationChannel(AlarmReceiver.CHANNEL_ID, CHANNER_NAME, NotificationManager.IMPORTANCE_DEFAULT) + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (!notificationManager.notificationChannels.contains(channel)) notificationManager.createNotificationChannel(channel) + } + + private fun getPendingIntent(intent: Intent) = PendingIntent.getBroadcast( + context, + ALARM_REQUEST_CODE, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + companion object { private const val ALARM_REQUEST_CODE = 100 + private const val CHANNER_NAME = "Reservation Notification" } } diff --git a/app/src/main/java/woowacourse/movie/view/seatselection/AlarmReceiver.kt b/app/src/main/java/woowacourse/movie/view/AlarmReceiver.kt similarity index 86% rename from app/src/main/java/woowacourse/movie/view/seatselection/AlarmReceiver.kt rename to app/src/main/java/woowacourse/movie/view/AlarmReceiver.kt index 5fa265adb..9fccf95e8 100644 --- a/app/src/main/java/woowacourse/movie/view/seatselection/AlarmReceiver.kt +++ b/app/src/main/java/woowacourse/movie/view/AlarmReceiver.kt @@ -1,4 +1,4 @@ -package woowacourse.movie.view.seatselection +package woowacourse.movie.view import android.Manifest import android.app.Notification @@ -12,8 +12,8 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import woowacourse.movie.R import woowacourse.movie.util.getParcelableCompat -import woowacourse.movie.view.ReservationCompletedActivity import woowacourse.movie.view.model.ReservationUiModel +import woowacourse.movie.view.reservationcompleted.ReservationCompletedActivity class AlarmReceiver : BroadcastReceiver() { @@ -54,5 +54,11 @@ class AlarmReceiver : BroadcastReceiver() { const val NOTIFICATION_ID = 200 const val CHANNEL_ID = "RESERVATION_CHANNEL" const val RESERVATION = "RESERVATION" + + fun newIntent(context: Context, reservation: ReservationUiModel): Intent { + return Intent(context, AlarmReceiver::class.java).apply { + putExtra(RESERVATION, reservation) + } + } } } diff --git a/app/src/main/java/woowacourse/movie/view/ReservationActivity.kt b/app/src/main/java/woowacourse/movie/view/ReservationActivity.kt deleted file mode 100644 index 25b215c02..000000000 --- a/app/src/main/java/woowacourse/movie/view/ReservationActivity.kt +++ /dev/null @@ -1,211 +0,0 @@ -package woowacourse.movie.view - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.MenuItem -import android.view.View -import android.widget.AdapterView -import android.widget.ArrayAdapter -import androidx.appcompat.app.AppCompatActivity -import woowacourse.movie.R -import woowacourse.movie.databinding.ActivityReservationBinding -import woowacourse.movie.domain.Reservation -import woowacourse.movie.domain.ScreeningTime -import woowacourse.movie.util.DATE_FORMATTER -import woowacourse.movie.util.getParcelableCompat -import woowacourse.movie.util.getSerializableCompat -import woowacourse.movie.view.model.MovieListModel.MovieUiModel -import woowacourse.movie.view.model.ReservationOptions -import woowacourse.movie.view.seatselection.SeatSelectionActivity -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime - -class ReservationActivity : AppCompatActivity() { - - private lateinit var binding: ActivityReservationBinding - - private var peopleCountSaved = 1 - private lateinit var selectedScreeningDate: LocalDate - private lateinit var selectedScreeningTime: LocalTime - private val movie: MovieUiModel by lazy { initMovieFromIntent() } - private var timeSpinnerPosition = 0 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityReservationBinding.inflate(layoutInflater) - setContentView(binding.root) - - initViewData() - initSpinner() - initPeopleCountAdjustButtonClickListener() - initReserveButtonClickListener() - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - override fun onResume() { - super.onResume() - binding.peopleCount.text = peopleCountSaved.toString() - } - - private fun initMovieFromIntent(): MovieUiModel { - val movie = intent.getParcelableCompat(MOVIE) - requireNotNull(movie) { "인텐트로 받아온 데이터가 널일 수 없습니다." } - return movie - } - - private fun initViewData() { - binding.apply { - moviePoster.setImageResource(movie.posterResourceId) - movieTitle.text = movie.title - movieScreeningDate.text = getString(R.string.screening_date_format).format( - movie.screeningStartDate.format(DATE_FORMATTER), - movie.screeningEndDate.format(DATE_FORMATTER), - ) - movieRunningTime.text = - getString(R.string.running_time_format).format(movie.runningTime) - movieSummary.text = movie.summary - } - } - - private fun initSpinner() { - selectedScreeningDate = movie.screeningStartDate - selectedScreeningTime = ScreeningTime(selectedScreeningDate).getFirstScreeningTime() - - val screeningDates = movie.getAllScreeningDates() - - val dateSpinnerAdapter = ArrayAdapter( - this, - android.R.layout.simple_spinner_item, - screeningDates, - ) - - binding.dateSpinner.apply { - adapter = dateSpinnerAdapter - onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long, - ) { - selectedScreeningDate = screeningDates[position] - initTimeSpinner(timeSpinnerPosition) - } - - override fun onNothingSelected(parent: AdapterView<*>?) = Unit - } - } - } - - private fun initTimeSpinner(selectedPosition: Int?) { - val screeningTimes = ScreeningTime(selectedScreeningDate).getAllScreeningTimes() - - val timeSpinnerAdapter = ArrayAdapter( - this, - android.R.layout.simple_spinner_item, - screeningTimes, - ) - binding.timeSpinner.apply { - adapter = timeSpinnerAdapter - - if (selectedPosition != null) { - this.setSelection(selectedPosition, false) - } - onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long, - ) { - selectedScreeningTime = screeningTimes[position] - } - - override fun onNothingSelected(parent: AdapterView<*>?) = Unit - } - } - } - - private fun initPeopleCountAdjustButtonClickListener() { - binding.apply { - minusButton.setOnClickListener { - decreasePeopleCount() - } - plusButton.setOnClickListener { - increasePeopleCount() - } - } - } - - private fun ActivityReservationBinding.decreasePeopleCount() { - if (peopleCountSaved > Reservation.MIN_PEOPLE_COUNT) { - peopleCountSaved-- - peopleCount.text = peopleCountSaved.toString() - } - } - - private fun ActivityReservationBinding.increasePeopleCount() { - if (peopleCountSaved < Reservation.MAX_PEOPLE_COUNT) { - peopleCountSaved++ - peopleCount.text = peopleCountSaved.toString() - } - } - - private fun initReserveButtonClickListener() { - binding.reservationButton.setOnClickListener { - val reservationOptions = ReservationOptions( - movie.title, - LocalDateTime.of(selectedScreeningDate, selectedScreeningTime), - peopleCountSaved, - ) - startActivity(SeatSelectionActivity.newIntent(this, reservationOptions, movie)) - } - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - - outState.apply { - putInt(PEOPLE_COUNT, peopleCountSaved) - putSerializable(SELECTED_DATE, selectedScreeningDate) - putSerializable(SELECTED_TIME, selectedScreeningTime) - putInt(SELECTED_TIME_POSITION, binding.timeSpinner.selectedItemPosition) - } - } - - override fun onRestoreInstanceState(savedInstanceState: Bundle) { - super.onRestoreInstanceState(savedInstanceState) - - peopleCountSaved = savedInstanceState.getInt(PEOPLE_COUNT) - timeSpinnerPosition = savedInstanceState.getInt(SELECTED_TIME_POSITION) - - savedInstanceState.getSerializableCompat(SELECTED_DATE)?.run { - selectedScreeningDate = this - } - savedInstanceState.getSerializableCompat(SELECTED_TIME)?.run { - selectedScreeningTime = this - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> finish() - } - return super.onOptionsItemSelected(item) - } - - companion object { - private const val PEOPLE_COUNT = "PEOPLE_COUNT" - private const val SELECTED_DATE = "SELECTED_DATE" - private const val SELECTED_TIME = "SELECTED_TIME" - private const val SELECTED_TIME_POSITION = "SELECTED_TIME_POSITION" - private const val MOVIE = "MOVIE" - fun newIntent(context: Context, movie: MovieUiModel): Intent { - val intent = Intent(context, ReservationActivity::class.java) - intent.putExtra(MOVIE, movie) - return intent - } - } -} diff --git a/app/src/main/java/woowacourse/movie/view/mapper/MovieMapper.kt b/app/src/main/java/woowacourse/movie/view/mapper/MovieMapper.kt index 4ff73c13b..c4dd350f1 100644 --- a/app/src/main/java/woowacourse/movie/view/mapper/MovieMapper.kt +++ b/app/src/main/java/woowacourse/movie/view/mapper/MovieMapper.kt @@ -1,23 +1,22 @@ package woowacourse.movie.view.mapper -import woowacourse.movie.domain.Minute -import woowacourse.movie.domain.Movie -import woowacourse.movie.view.model.MovieListModel.MovieUiModel +import woowacourse.movie.R +import woowacourse.movie.domain.movie.Movie +import woowacourse.movie.view.model.MovieUiModel -fun Movie.toUiModel(): MovieUiModel = MovieUiModel( - title, - screeningStartDate, - screeningEndDate, - runningTime.value, - posterResourceId, - summary +val posters = mapOf( + "스즈메의 문단속" to R.drawable.suzume_poster, + "해리 포터와 마법사의 돌" to R.drawable.harry_potter1_poster, + "스타워즈" to R.drawable.starwars_poster, + "어벤져스: 엔드게임" to R.drawable.avengers_endgame_poster, ) -fun MovieUiModel.toDomainModel(): Movie = Movie( +fun Movie.toUiModel(): MovieUiModel = MovieUiModel( title, - screeningStartDate, - screeningEndDate, - Minute(runningTime), - posterResourceId, - summary + startDate, + endDate, + runningTime.value, + posters[title] ?: 0, + summary, + schedule.schedule, ) diff --git a/app/src/main/java/woowacourse/movie/view/mapper/ReservationMapper.kt b/app/src/main/java/woowacourse/movie/view/mapper/ReservationMapper.kt index af2a2b391..2895c4153 100644 --- a/app/src/main/java/woowacourse/movie/view/mapper/ReservationMapper.kt +++ b/app/src/main/java/woowacourse/movie/view/mapper/ReservationMapper.kt @@ -1,12 +1,13 @@ package woowacourse.movie.view.mapper -import woowacourse.movie.domain.Reservation +import woowacourse.movie.domain.reservation.Reservation import woowacourse.movie.view.model.ReservationUiModel fun Reservation.toUiModel(): ReservationUiModel = ReservationUiModel( - movieTitle, + title, screeningDateTime, seats.size, - seats.map { it.toUiModel().name }, - finalReservationFee.amount + seats.map { it.toUiModel().seatId }, + price.price, + theaterName ) diff --git a/app/src/main/java/woowacourse/movie/view/mapper/SeatMapper.kt b/app/src/main/java/woowacourse/movie/view/mapper/SeatMapper.kt index 0139dd451..f79646c8a 100644 --- a/app/src/main/java/woowacourse/movie/view/mapper/SeatMapper.kt +++ b/app/src/main/java/woowacourse/movie/view/mapper/SeatMapper.kt @@ -1,15 +1,8 @@ package woowacourse.movie.view.mapper -import woowacourse.movie.R -import woowacourse.movie.domain.Seat -import woowacourse.movie.domain.SeatType +import androidx.annotation.ColorInt +import woowacourse.movie.domain.system.Seat import woowacourse.movie.view.model.SeatUiModel -fun Seat.toUiModel(): SeatUiModel = SeatUiModel( - ('A' + row - 1).toString() + "$column", - when (this.type) { - SeatType.BType -> R.color.purple_700 - SeatType.SType -> R.color.green - SeatType.AType -> R.color.blue - } -) +fun Seat.toUiModel(@ColorInt color: Int = 0): SeatUiModel = SeatUiModel(row, col, color) +fun SeatUiModel.toDomain(): Seat = Seat(row, col) diff --git a/app/src/main/java/woowacourse/movie/view/mapper/TheaterMapper.kt b/app/src/main/java/woowacourse/movie/view/mapper/TheaterMapper.kt new file mode 100644 index 000000000..13767ebd3 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/mapper/TheaterMapper.kt @@ -0,0 +1,14 @@ +package woowacourse.movie.view.mapper + +import woowacourse.movie.R +import woowacourse.movie.domain.theater.Grade +import woowacourse.movie.domain.theater.Theater +import woowacourse.movie.view.model.TheaterUiModel + +fun Theater.toUiModel(colorOfGrade: Map): TheaterUiModel { + val colorOfRow: MutableMap = mutableMapOf() + repeat(size.row) { + colorOfRow[it] = colorOfGrade[gradeOfRow[it]] ?: R.color.black + } + return TheaterUiModel(name, size.row, size.col, colorOfRow.toMap()) +} diff --git a/app/src/main/java/woowacourse/movie/view/model/MovieListModel.kt b/app/src/main/java/woowacourse/movie/view/model/MovieListModel.kt deleted file mode 100644 index 23798be9f..000000000 --- a/app/src/main/java/woowacourse/movie/view/model/MovieListModel.kt +++ /dev/null @@ -1,35 +0,0 @@ -package woowacourse.movie.view.model - -import android.os.Parcelable -import androidx.annotation.DrawableRes -import kotlinx.parcelize.Parcelize -import java.time.LocalDate -import java.time.temporal.ChronoUnit - -sealed class MovieListModel { - - data class MovieAdModel( - @DrawableRes val banner: Int, - val url: String - ) : MovieListModel() - - @Parcelize - data class MovieUiModel( - val title: String, - val screeningStartDate: LocalDate, - val screeningEndDate: LocalDate, - val runningTime: Int, - val posterResourceId: Int, - val summary: String - ) : Parcelable, MovieListModel() { - fun getAllScreeningDates(): List { - val screeningDates = mutableListOf() - var screeningDate = screeningStartDate - repeat(ChronoUnit.DAYS.between(screeningStartDate, screeningEndDate).toInt() + 1) { - screeningDates.add(screeningDate) - screeningDate = screeningDate.plusDays(1) - } - return screeningDates - } - } -} diff --git a/app/src/main/java/woowacourse/movie/view/model/MovieUiModel.kt b/app/src/main/java/woowacourse/movie/view/model/MovieUiModel.kt new file mode 100644 index 000000000..46234bcc6 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/model/MovieUiModel.kt @@ -0,0 +1,20 @@ +package woowacourse.movie.view.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import java.time.LocalDate +import java.time.LocalTime + +typealias TheaterName = String +typealias ScreeningTimes = List + +@Parcelize +data class MovieUiModel( + val title: String, + val startDate: LocalDate, + val endDate: LocalDate, + val runningTime: Int, + val posterResourceId: Int, + val summary: String, + val schedule: Map, +) : Parcelable diff --git a/app/src/main/java/woowacourse/movie/view/model/ReservationOptions.kt b/app/src/main/java/woowacourse/movie/view/model/ReservationOptions.kt index 7a75a96a0..1abc21273 100644 --- a/app/src/main/java/woowacourse/movie/view/model/ReservationOptions.kt +++ b/app/src/main/java/woowacourse/movie/view/model/ReservationOptions.kt @@ -8,5 +8,6 @@ import java.time.LocalDateTime data class ReservationOptions( val title: String, val screeningDateTime: LocalDateTime, - val peopleCount: Int + val peopleCount: Int, + val theaterName: String ) : Parcelable diff --git a/app/src/main/java/woowacourse/movie/view/model/ReservationUiModel.kt b/app/src/main/java/woowacourse/movie/view/model/ReservationUiModel.kt index 736494f9a..f691ec7ae 100644 --- a/app/src/main/java/woowacourse/movie/view/model/ReservationUiModel.kt +++ b/app/src/main/java/woowacourse/movie/view/model/ReservationUiModel.kt @@ -8,7 +8,8 @@ import java.time.LocalDateTime data class ReservationUiModel( val title: String, val screeningDateTime: LocalDateTime, - val peopleCount: Int, + val count: Int, val seats: List, - val finalReservationFee: Int + val price: Int, + val theaterName: String ) : Parcelable diff --git a/app/src/main/java/woowacourse/movie/view/model/SeatUiModel.kt b/app/src/main/java/woowacourse/movie/view/model/SeatUiModel.kt index 5f6a22728..d0c09ecef 100644 --- a/app/src/main/java/woowacourse/movie/view/model/SeatUiModel.kt +++ b/app/src/main/java/woowacourse/movie/view/model/SeatUiModel.kt @@ -1,6 +1,18 @@ package woowacourse.movie.view.model -class SeatUiModel( - val name: String, - val color: Int -) +import android.os.Parcelable +import androidx.annotation.ColorRes +import kotlinx.parcelize.Parcelize + +@Parcelize +class SeatUiModel(val row: Int, val col: Int, @ColorRes val color: Int) : Parcelable { + val seatId: String = ('A'.code + row).toChar() + (col + 1).toString() + + companion object { + fun of(seatId: String, @ColorRes color: Int = 0): SeatUiModel { + val row = seatId[0].code - 'A'.code + val col = seatId.substring(1).toInt() - 1 + return SeatUiModel(row, col, color) + } + } +} diff --git a/app/src/main/java/woowacourse/movie/view/model/TheaterUiModel.kt b/app/src/main/java/woowacourse/movie/view/model/TheaterUiModel.kt new file mode 100644 index 000000000..5419a4bf0 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/model/TheaterUiModel.kt @@ -0,0 +1,11 @@ +package woowacourse.movie.view.model + +typealias row = Int +typealias colorId = Int + +data class TheaterUiModel( + val name: String, + val maxRow: Int, + val maxCol: Int, + val colorOfRow: Map, +) diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/MovieMainActivity.kt b/app/src/main/java/woowacourse/movie/view/moviemain/MovieMainActivity.kt index 4f49d69e5..4f74ff322 100644 --- a/app/src/main/java/woowacourse/movie/view/moviemain/MovieMainActivity.kt +++ b/app/src/main/java/woowacourse/movie/view/moviemain/MovieMainActivity.kt @@ -7,8 +7,11 @@ import androidx.fragment.app.commit import com.google.android.material.bottomnavigation.BottomNavigationView import woowacourse.movie.R import woowacourse.movie.view.moviemain.movielist.MovieListFragment +import woowacourse.movie.view.moviemain.movielist.MovieListFragment.Companion.TAG_MOVIE_LIST import woowacourse.movie.view.moviemain.reservationlist.ReservationListFragment +import woowacourse.movie.view.moviemain.reservationlist.ReservationListFragment.Companion.TAG_RESERVATION_LIST import woowacourse.movie.view.moviemain.setting.SettingFragment +import woowacourse.movie.view.moviemain.setting.SettingFragment.Companion.TAG_SETTING class MovieMainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -20,18 +23,18 @@ class MovieMainActivity : AppCompatActivity() { navigation.setOnItemSelectedListener { item -> when (item.itemId) { R.id.action_reservation_list -> { - val fragment = supportFragmentManager.findFragmentByTag(TAG_RESERVATION_LIST) as? ReservationListFragment ?: ReservationListFragment().also { addFragment(it, TAG_RESERVATION_LIST) } - replaceFragment(fragment) + val fragment = ReservationListFragment.of(supportFragmentManager) + replaceFragment(fragment, TAG_RESERVATION_LIST) return@setOnItemSelectedListener true } R.id.action_home -> { - val fragment = supportFragmentManager.findFragmentByTag(TAG_MOVIE_LIST) as? MovieListFragment ?: MovieListFragment().also { addFragment(it, TAG_MOVIE_LIST) } - replaceFragment(fragment) + val fragment = MovieListFragment.of(supportFragmentManager) + replaceFragment(fragment, TAG_MOVIE_LIST) return@setOnItemSelectedListener true } R.id.action_setting -> { - val fragment = supportFragmentManager.findFragmentByTag(TAG_MOVIE_LIST) as? SettingFragment ?: SettingFragment().also { addFragment(it, TAG_SETTING) } - replaceFragment(fragment) + val fragment = SettingFragment.of(supportFragmentManager) + replaceFragment(fragment, TAG_SETTING) return@setOnItemSelectedListener true } else -> return@setOnItemSelectedListener false @@ -40,10 +43,10 @@ class MovieMainActivity : AppCompatActivity() { navigation.selectedItemId = R.id.action_home } - private fun replaceFragment(fragment: Fragment) { + private fun replaceFragment(fragment: Fragment, tag: String) { supportFragmentManager.commit { setReorderingAllowed(true) - replace(R.id.fragment_container_view, fragment) + replace(R.id.fragment_container_view, fragment, tag) } } @@ -52,10 +55,4 @@ class MovieMainActivity : AppCompatActivity() { add(fragment, tag) } } - - companion object { - private const val TAG_RESERVATION_LIST = "RESERVATION_LIST" - private const val TAG_MOVIE_LIST = "MOVIE_LIST" - private const val TAG_SETTING = "SETTING" - } } diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/ItemViewHolder.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/ItemViewHolder.kt new file mode 100644 index 000000000..0f3293be9 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/ItemViewHolder.kt @@ -0,0 +1,45 @@ +package woowacourse.movie.view.moviemain.movielist + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.DrawableRes +import androidx.recyclerview.widget.RecyclerView +import woowacourse.movie.R +import woowacourse.movie.databinding.MovieAdItemBinding +import woowacourse.movie.databinding.MovieItemBinding +import woowacourse.movie.util.DATE_FORMATTER +import woowacourse.movie.view.model.MovieUiModel + +sealed class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { + class MovieItemViewHolder(private val binding: MovieItemBinding) : + ItemViewHolder(binding.root) { + fun set(movie: MovieUiModel, onClick: MovieListAdapter.OnItemClick) { + val context = binding.movieTitle.context + binding.movie = movie + binding.onItemClick = onClick + binding.moviePoster.setImageResource(movie.posterResourceId) + binding.movieScreeningDate.text = + context.getString(R.string.screening_date_format).format( + movie.startDate.format(DATE_FORMATTER), + movie.endDate.format(DATE_FORMATTER), + ) + } + } + + class AdItemViewHolder(private val binding: MovieAdItemBinding) : ItemViewHolder(binding.root) { + fun set(@DrawableRes resId: Int) { + binding.adImageview.setImageResource(resId) + } + } + + companion object { + fun of(parent: ViewGroup, type: ListViewType): ItemViewHolder { + val view = LayoutInflater.from(parent.context).inflate(type.id, parent, false) + return when (type) { + ListViewType.NORMAL_VIEWTYPE -> MovieItemViewHolder(MovieItemBinding.bind(view)) + ListViewType.AD_VIEWTYPE -> AdItemViewHolder(MovieAdItemBinding.bind(view)) + } + } + } +} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/ListViewType.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/ListViewType.kt new file mode 100644 index 000000000..e8994ca98 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/ListViewType.kt @@ -0,0 +1,15 @@ +package woowacourse.movie.view.moviemain.movielist + +import androidx.annotation.LayoutRes +import woowacourse.movie.R + +enum class ListViewType(@LayoutRes val id: Int) { + AD_VIEWTYPE(R.layout.movie_ad_item), NORMAL_VIEWTYPE(R.layout.movie_item); + + companion object { + fun getViewType(adInterval: Int, position: Int): ListViewType { + if ((position + 1) % (adInterval + 1) == 0) return AD_VIEWTYPE + return NORMAL_VIEWTYPE + } + } +} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieAdViewHolder.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieAdViewHolder.kt deleted file mode 100644 index fa0440642..000000000 --- a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieAdViewHolder.kt +++ /dev/null @@ -1,18 +0,0 @@ -package woowacourse.movie.view.moviemain.movielist - -import androidx.recyclerview.widget.RecyclerView -import woowacourse.movie.databinding.MovieAdItemBinding -import woowacourse.movie.view.model.MovieListModel - -class MovieAdViewHolder( - private val binding: MovieAdItemBinding, - private val onViewClick: MovieListAdapter.OnItemClick -) : RecyclerView.ViewHolder(binding.root) { - - fun bind(ad: MovieListModel.MovieAdModel) { - binding.adImageview.setImageResource(ad.banner) - binding.adImageview.setOnClickListener { - onViewClick.onClick(ad) - } - } -} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieItemViewHolder.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieItemViewHolder.kt deleted file mode 100644 index 440f92f2a..000000000 --- a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieItemViewHolder.kt +++ /dev/null @@ -1,30 +0,0 @@ -package woowacourse.movie.view.moviemain.movielist - -import androidx.recyclerview.widget.RecyclerView -import woowacourse.movie.R -import woowacourse.movie.databinding.MovieItemBinding -import woowacourse.movie.util.DATE_FORMATTER -import woowacourse.movie.view.model.MovieListModel.MovieUiModel - -class MovieItemViewHolder( - private val binding: MovieItemBinding, - private val onViewClick: MovieListAdapter.OnItemClick -) : RecyclerView.ViewHolder(binding.root) { - fun bind(movie: MovieUiModel) { - binding.apply { - val context = binding.root.context - moviePoster.setImageResource(movie.posterResourceId) - movieTitle.text = movie.title - movieScreeningDate.text = - context.resources.getString(R.string.screening_date_format).format( - movie.screeningStartDate.format(DATE_FORMATTER), - movie.screeningEndDate.format(DATE_FORMATTER) - ) - movieRunningTime.text = context.resources.getString(R.string.running_time_format) - .format(movie.runningTime) - } - binding.reserveNowButton.setOnClickListener { - onViewClick.onClick(movie) - } - } -} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListAdapter.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListAdapter.kt index 474c9ca41..a07300374 100644 --- a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListAdapter.kt +++ b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListAdapter.kt @@ -1,52 +1,41 @@ package woowacourse.movie.view.moviemain.movielist -import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import woowacourse.movie.databinding.MovieAdItemBinding -import woowacourse.movie.databinding.MovieItemBinding -import woowacourse.movie.view.model.MovieListModel -import woowacourse.movie.view.model.MovieListModel.MovieAdModel -import woowacourse.movie.view.model.MovieListModel.MovieUiModel +import woowacourse.movie.R +import woowacourse.movie.view.model.MovieUiModel class MovieListAdapter( - private val dataList: List, + private val adInterval: Int, + private val movies: List, private val onItemClick: OnItemClick ) : RecyclerView.Adapter() { fun interface OnItemClick { - fun onClick(item: MovieListModel) + fun onClick(item: MovieUiModel) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (MovieListViewType.values()[viewType]) { - MovieListViewType.MOVIE_ITEM -> { - val view = LayoutInflater.from(parent.context).inflate(MovieListViewType.MOVIE_ITEM.id, parent, false) - MovieItemViewHolder(MovieItemBinding.bind(view), onItemClick) - } - MovieListViewType.AD_ITEM -> { - val view = LayoutInflater.from(parent.context).inflate(MovieListViewType.AD_ITEM.id, parent, false) - MovieAdViewHolder(MovieAdItemBinding.bind(view), onItemClick) - } - } + return ItemViewHolder.of(parent, ListViewType.values()[viewType]) } - override fun getItemCount(): Int = dataList.size + override fun getItemCount(): Int = movies.size + movies.size / adInterval override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val item = dataList[position] + val item = movies[position - ((position + 1) / (adInterval + 1))] when (holder) { - is MovieItemViewHolder -> { - holder.bind(item as MovieUiModel) + is ItemViewHolder.MovieItemViewHolder -> { + holder.set(item) { + onItemClick.onClick(item) + } } - is MovieAdViewHolder -> { - holder.bind(item as MovieAdModel) + is ItemViewHolder.AdItemViewHolder -> { + holder.set(R.drawable.woowacourse_banner) } } } - override fun getItemViewType(position: Int): Int = when (dataList[position]) { - is MovieUiModel -> MovieListViewType.MOVIE_ITEM.ordinal - is MovieAdModel -> MovieListViewType.AD_ITEM.ordinal + override fun getItemViewType(position: Int): Int { + return ListViewType.getViewType(adInterval, position).ordinal } } diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListContract.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListContract.kt new file mode 100644 index 000000000..f52e4e24e --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListContract.kt @@ -0,0 +1,12 @@ +package woowacourse.movie.view.moviemain.movielist + +import woowacourse.movie.view.model.MovieUiModel + +interface MovieListContract { + interface View { + fun showMovieList(movies: List) + } + interface Presenter { + fun fetchMovieList() + } +} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListFragment.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListFragment.kt index b69a00fbf..799b76596 100644 --- a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListFragment.kt +++ b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListFragment.kt @@ -1,69 +1,38 @@ package woowacourse.movie.view.moviemain.movielist -import android.content.Intent -import android.net.Uri import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.RecyclerView import woowacourse.movie.R -import woowacourse.movie.data.MovieMockRepository -import woowacourse.movie.domain.Movie -import woowacourse.movie.view.ReservationActivity -import woowacourse.movie.view.mapper.toUiModel -import woowacourse.movie.view.model.MovieListModel +import woowacourse.movie.data.movie.MovieMockRepository +import woowacourse.movie.view.model.MovieUiModel -class MovieListFragment : Fragment(R.layout.fragment_movie_list) { +class MovieListFragment : Fragment(R.layout.fragment_movie_list), MovieListContract.View { + private lateinit var presenter: MovieListContract.Presenter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val movies = MovieMockRepository.findAll() - val dataList = generateMovieListData(movies) - - val movieAdapter = MovieListAdapter(dataList, ::onClick) - val movieListView = view.findViewById(R.id.movie_recyclerview) - movieListView.adapter = movieAdapter + presenter = MovieListPresenter(this, MovieMockRepository) + presenter.fetchMovieList() } - private fun onClick(item: MovieListModel) { - when (item) { - is MovieListModel.MovieUiModel -> { - val intent = ReservationActivity.newIntent(requireContext(), item) - startActivity(intent) - } - is MovieListModel.MovieAdModel -> { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(item.url)) - startActivity(intent) - } - } + override fun showMovieList(movies: List) { + val movieAdapter = MovieListAdapter(3, movies, ::onClick) + val movieListView = view?.findViewById(R.id.movie_recyclerview) + movieListView?.adapter = movieAdapter } - private fun generateMovieListData(movies: List): List { - val ad = MovieListModel.MovieAdModel( - R.drawable.woowacourse_banner, - "https://woowacourse.github.io/", - ) - - return mixMovieAdData(movies, ad, AD_POST_INTERVAL) - } - - private fun mixMovieAdData( - movies: List, - ad: MovieListModel.MovieAdModel, - adPostInterval: Int, - ): List { - val dataList = mutableListOf() - movies.forEachIndexed { index, movie -> - if (index % adPostInterval == adPostInterval - 1) { - dataList.add(movie.toUiModel()) - dataList.add(ad) - return@forEachIndexed - } - dataList.add(movie.toUiModel()) - } - return dataList + private fun onClick(item: MovieUiModel) { + val bottomSheet = TheaterBottomSheetFragment.of(item) + bottomSheet.show(childFragmentManager, TheaterBottomSheetFragment.TAG_THEATER) } companion object { - private const val AD_POST_INTERVAL = 3 + const val TAG_MOVIE_LIST = "MOVIE_LIST" + fun of(supportFragmentManager: FragmentManager): MovieListFragment { + return supportFragmentManager.findFragmentByTag(TAG_MOVIE_LIST) as? MovieListFragment + ?: MovieListFragment() + } } } diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListPresenter.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListPresenter.kt new file mode 100644 index 000000000..75cbc9d9f --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListPresenter.kt @@ -0,0 +1,15 @@ +package woowacourse.movie.view.moviemain.movielist + +import woowacourse.movie.domain.repository.MovieRepository +import woowacourse.movie.view.mapper.toUiModel + +class MovieListPresenter( + private val view: MovieListContract.View, + private val movieRepository: MovieRepository, +) : MovieListContract.Presenter { + override fun fetchMovieList() { + val movies = movieRepository.findAll() + .map { it.toUiModel() } + view.showMovieList(movies) + } +} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListViewType.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListViewType.kt deleted file mode 100644 index 409d390dc..000000000 --- a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/MovieListViewType.kt +++ /dev/null @@ -1,8 +0,0 @@ -package woowacourse.movie.view.moviemain.movielist - -import woowacourse.movie.R - -enum class MovieListViewType(val id: Int) { - MOVIE_ITEM(R.layout.movie_item), - AD_ITEM(R.layout.movie_ad_item) -} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/TheaterBottomSheetFragment.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/TheaterBottomSheetFragment.kt new file mode 100644 index 000000000..a459bc82e --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/TheaterBottomSheetFragment.kt @@ -0,0 +1,36 @@ +package woowacourse.movie.view.moviemain.movielist + +import android.os.Bundle +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import woowacourse.movie.R +import woowacourse.movie.util.getParcelableCompat +import woowacourse.movie.view.model.MovieUiModel +import woowacourse.movie.view.reservation.ReservationActivity + +class TheaterBottomSheetFragment : BottomSheetDialogFragment(R.layout.theater_select_bottom_sheet) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val movie = arguments?.getParcelableCompat(KEY_MOVIE) + val theatersView = view.findViewById(R.id.recycler_theaters) + theatersView.adapter = movie?.schedule?.let { schedule -> + TheaterListAdapter(schedule) { theaterName -> + val intent = ReservationActivity.newIntent(requireContext(), movie, theaterName) + startActivity(intent) + } + } + } + + companion object { + const val TAG_THEATER = "THEATER" + private const val KEY_MOVIE = "MOVIE" + fun of(movie: MovieUiModel): TheaterBottomSheetFragment { + val bundle = Bundle() + bundle.putParcelable(KEY_MOVIE, movie) + val fragment = TheaterBottomSheetFragment() + fragment.arguments = bundle + return fragment + } + } +} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/TheaterItemViewHolder.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/TheaterItemViewHolder.kt new file mode 100644 index 000000000..0c9afc5da --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/TheaterItemViewHolder.kt @@ -0,0 +1,17 @@ +package woowacourse.movie.view.moviemain.movielist + +import androidx.recyclerview.widget.RecyclerView +import woowacourse.movie.databinding.TheaterItemBinding +import java.time.LocalTime + +class TheaterItemViewHolder( + private val binding: TheaterItemBinding, + private val onItemClick: TheaterListAdapter.OnItemClick, +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(name: String, times: List) { + binding.theaterName = name + binding.timeCount = times.size + binding.onItemClick = onItemClick + } +} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/movielist/TheaterListAdapter.kt b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/TheaterListAdapter.kt new file mode 100644 index 000000000..42d1da6f0 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/moviemain/movielist/TheaterListAdapter.kt @@ -0,0 +1,32 @@ +package woowacourse.movie.view.moviemain.movielist + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import woowacourse.movie.R +import woowacourse.movie.databinding.TheaterItemBinding +import java.time.LocalTime + +class TheaterListAdapter( + schedule: Map>, + private val onItemClick: OnItemClick, +) : RecyclerView.Adapter() { + private val convertSchedule = schedule.toList() + + fun interface OnItemClick { + fun onClick(name: String) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TheaterItemViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.theater_item, parent, false) + return TheaterItemViewHolder(TheaterItemBinding.bind(view), onItemClick) + } + + override fun getItemCount(): Int { + return convertSchedule.size + } + + override fun onBindViewHolder(holder: TheaterItemViewHolder, position: Int) { + holder.bind(convertSchedule[position].first, convertSchedule[position].second) + } +} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationItemViewHolder.kt b/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationItemViewHolder.kt index 7cfc944e3..ce6047cab 100644 --- a/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationItemViewHolder.kt +++ b/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationItemViewHolder.kt @@ -14,16 +14,14 @@ class ReservationItemViewHolder( RecyclerView.ViewHolder(binding.root) { fun bind(reservation: ReservationUiModel) { val context = binding.root.context + binding.reservation = reservation + binding.onItemClick = onItemClick with(binding) { reservationDatetime.text = context.getString( R.string.datetime_with_line, reservation.screeningDateTime.format(DATE_FORMATTER), reservation.screeningDateTime.format(TIME_FORMATTER), ) - movieTitle.text = reservation.title - } - binding.reservationLayout.setOnClickListener { - onItemClick.onClick(reservation) } } } diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationListContract.kt b/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationListContract.kt new file mode 100644 index 000000000..193357130 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationListContract.kt @@ -0,0 +1,12 @@ +package woowacourse.movie.view.moviemain.reservationlist + +import woowacourse.movie.view.model.ReservationUiModel + +interface ReservationListContract { + interface View { + fun showReservations(reservations: List) + } + interface Presenter { + fun fetchReservations() + } +} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationListFragment.kt b/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationListFragment.kt index f7e95b6b6..f61809bcb 100644 --- a/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationListFragment.kt +++ b/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationListFragment.kt @@ -3,20 +3,37 @@ package woowacourse.movie.view.moviemain.reservationlist import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.RecyclerView import woowacourse.movie.R -import woowacourse.movie.data.ReservationMockRepository -import woowacourse.movie.view.ReservationCompletedActivity -import woowacourse.movie.view.mapper.toUiModel +import woowacourse.movie.data.reservation.ReservationDbRepository +import woowacourse.movie.view.model.ReservationUiModel +import woowacourse.movie.view.reservationcompleted.ReservationCompletedActivity + +class ReservationListFragment : + Fragment(R.layout.fragment_reservation_list), + ReservationListContract.View { + private lateinit var presenter: ReservationListContract.Presenter -class ReservationListFragment : Fragment(R.layout.fragment_reservation_list) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val recyclerView = view.findViewById(R.id.recyclerview) - val reservations = ReservationMockRepository.findAll().map { it.toUiModel() } - recyclerView.adapter = ReservationListAdapter(reservations) { reservation -> + presenter = ReservationListPresenter(this, ReservationDbRepository(requireContext())) + presenter.fetchReservations() + } + + override fun showReservations(reservations: List) { + val recyclerView = view?.findViewById(R.id.recyclerview) + recyclerView?.adapter = ReservationListAdapter(reservations) { reservation -> val intent = ReservationCompletedActivity.newIntent(requireContext(), reservation) startActivity(intent) } } + + companion object { + const val TAG_RESERVATION_LIST = "RESERVATION_LIST" + fun of(supportFragmentManager: FragmentManager): ReservationListFragment { + return supportFragmentManager.findFragmentByTag(TAG_RESERVATION_LIST) as? ReservationListFragment + ?: ReservationListFragment() + } + } } diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationListPresenter.kt b/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationListPresenter.kt new file mode 100644 index 000000000..28883f18a --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/moviemain/reservationlist/ReservationListPresenter.kt @@ -0,0 +1,11 @@ +package woowacourse.movie.view.moviemain.reservationlist + +import woowacourse.movie.domain.repository.ReservationRepository +import woowacourse.movie.view.mapper.toUiModel + +class ReservationListPresenter(private val view: ReservationListContract.View, private val reservationRespository: ReservationRepository) : ReservationListContract.Presenter { + override fun fetchReservations() { + val reservations = reservationRespository.findAll().map { it.toUiModel() } + view.showReservations(reservations) + } +} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/setting/SettingContract.kt b/app/src/main/java/woowacourse/movie/view/moviemain/setting/SettingContract.kt new file mode 100644 index 000000000..b6bbc4fae --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/moviemain/setting/SettingContract.kt @@ -0,0 +1,17 @@ +package woowacourse.movie.view.moviemain.setting + +import woowacourse.movie.view.model.ReservationUiModel + +interface SettingContract { + interface View { + fun setToggle(isOn: Boolean) + fun cancelAlarms() + fun setAlarms(reservations: List) + fun requestNotificationPermission(): Boolean + } + + interface Presenter { + fun initState() + fun changeAlarmState(isOn: Boolean) + } +} diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/setting/SettingFragment.kt b/app/src/main/java/woowacourse/movie/view/moviemain/setting/SettingFragment.kt index c77e3c615..15bfa5b24 100644 --- a/app/src/main/java/woowacourse/movie/view/moviemain/setting/SettingFragment.kt +++ b/app/src/main/java/woowacourse/movie/view/moviemain/setting/SettingFragment.kt @@ -1,78 +1,101 @@ package woowacourse.movie.view.moviemain.setting import android.Manifest -import android.content.SharedPreferences import android.content.pm.PackageManager import android.os.Build import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat +import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment -import androidx.preference.PreferenceManager -import com.google.android.material.switchmaterial.SwitchMaterial +import androidx.fragment.app.FragmentManager import woowacourse.movie.R -import woowacourse.movie.data.ReservationMockRepository +import woowacourse.movie.data.reservation.ReservationDbRepository +import woowacourse.movie.data.setting.SettingPreferencesRepository +import woowacourse.movie.databinding.FragmentSettingBinding import woowacourse.movie.view.AlarmController -import woowacourse.movie.view.mapper.toUiModel +import woowacourse.movie.view.model.ReservationUiModel -class SettingFragment : Fragment(R.layout.fragment_setting) { +class SettingFragment : Fragment(), SettingContract.View { private lateinit var alarmController: AlarmController - private lateinit var sharedPreferences: SharedPreferences - private lateinit var toggle: SwitchMaterial - - private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> - if (!isGranted) { - Toast.makeText(requireContext(), "권한을 설정해야 알림을 받을 수 있습니다. 설정에서 알림을 켜주세요.", Toast.LENGTH_LONG).show() - toggle.isChecked = false + private lateinit var binding: FragmentSettingBinding + private lateinit var presenter: SettingContract.Presenter + private val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> + if (!isGranted) { + Toast.makeText( + requireContext(), + NOTICE_REQUEST_PERMISSION_MESSAGE, + Toast.LENGTH_LONG, + ).show() + setToggle(false) + } } - } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) - alarmController = AlarmController(requireContext()) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + presenter = SettingPresenter( + this, + SettingPreferencesRepository(requireContext()), + ReservationDbRepository(requireContext()), + ) + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_setting, container, false) + binding.presenter = presenter + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + alarmController = AlarmController(requireContext()) + presenter.initState() + } - toggle = view.findViewById(R.id.setting_toggle) - - val savedToggle = sharedPreferences.getBoolean(IS_ALARM_ON, false) - toggle.isChecked = savedToggle + override fun setToggle(isOn: Boolean) { + binding.settingToggle.isChecked = isOn + } - toggle.setOnCheckedChangeListener { _, isChecked -> - setToggleChangeListener(isChecked) - } + override fun setAlarms(reservations: List) { + alarmController.registerAlarms(reservations, ALARM_MINUTE_INTERVAL) } - private fun setToggleChangeListener(isChecked: Boolean) { - val editor: SharedPreferences.Editor = sharedPreferences.edit() - if (isChecked && requestNotificationPermission()) { - val reservations = ReservationMockRepository.findAll().map { it.toUiModel() } - alarmController.registerAlarms(reservations, ALARM_MINUTE_INTERVAL) - editor.putBoolean(IS_ALARM_ON, true).apply() - return - } + override fun cancelAlarms() { alarmController.cancelAlarms() - editor.putBoolean(IS_ALARM_ON, false).apply() } - private fun requestNotificationPermission(): Boolean { - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + override fun requestNotificationPermission(): Boolean { + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.POST_NOTIFICATIONS, + ) != PackageManager.PERMISSION_GRANTED + ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) - return ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + return ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED } } return true // android 12 version 이하는 notification 권한 필요 없기 때문 } companion object { - const val IS_ALARM_ON = "IS_ALARM_ON" + private const val NOTICE_REQUEST_PERMISSION_MESSAGE = + "권한을 설정해야 알림을 받을 수 있습니다. 설정에서 알림을 켜주세요." const val ALARM_MINUTE_INTERVAL = 30L + const val TAG_SETTING = "SETTING" + + fun of(supportFragmentManager: FragmentManager): SettingFragment { + return supportFragmentManager.findFragmentByTag(TAG_SETTING) as? SettingFragment + ?: SettingFragment() + } } } diff --git a/app/src/main/java/woowacourse/movie/view/moviemain/setting/SettingPresenter.kt b/app/src/main/java/woowacourse/movie/view/moviemain/setting/SettingPresenter.kt new file mode 100644 index 000000000..1c0348dca --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/moviemain/setting/SettingPresenter.kt @@ -0,0 +1,29 @@ +package woowacourse.movie.view.moviemain.setting + +import woowacourse.movie.domain.repository.ReservationRepository +import woowacourse.movie.domain.repository.SettingRepository +import woowacourse.movie.view.mapper.toUiModel + +class SettingPresenter( + private val view: SettingContract.View, + private val settingManager: SettingRepository, + private val reservationRepository: ReservationRepository +) : SettingContract.Presenter { + + override fun initState() { + view.setToggle(settingManager.getIsAlarmSetting()) + } + + override fun changeAlarmState(isOn: Boolean) { + if (isOn && view.requestNotificationPermission()) { + val reservations = reservationRepository.findAll().map { it.toUiModel() } + view.setAlarms(reservations) + settingManager.setIsAlarmSetting(true) + view.setToggle(true) + return + } + view.cancelAlarms() + settingManager.setIsAlarmSetting(false) + view.setToggle(false) + } +} diff --git a/app/src/main/java/woowacourse/movie/view/reservation/ReservationActivity.kt b/app/src/main/java/woowacourse/movie/view/reservation/ReservationActivity.kt new file mode 100644 index 000000000..f6c2a3201 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/reservation/ReservationActivity.kt @@ -0,0 +1,151 @@ +package woowacourse.movie.view.reservation + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import woowacourse.movie.R +import woowacourse.movie.databinding.ActivityReservationBinding +import woowacourse.movie.util.DATE_FORMATTER +import woowacourse.movie.util.getParcelableCompat +import woowacourse.movie.view.model.MovieUiModel +import woowacourse.movie.view.model.ReservationOptions +import woowacourse.movie.view.seatselection.SeatSelectionActivity +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime + +class ReservationActivity : AppCompatActivity(), ReservationContract.View { + + private lateinit var binding: ActivityReservationBinding + private lateinit var presenter: ReservationContract.Presenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityReservationBinding.inflate(layoutInflater) + setContentView(binding.root) + val movie = intent.getParcelableCompat(MOVIE) + val theaterName = intent.getStringExtra(THEATER) + if (movie == null || theaterName == null) { + Toast.makeText(this, "데이터가 없습니다. 다시 시도해주세요.", Toast.LENGTH_SHORT).show() + finish() + return + } + presenter = ReservationPresenter(this, movie, theaterName) + binding.presenter = presenter + presenter.fetchMovieData() + presenter.fetchScreeningDates() + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + override fun setCount(count: Int) { + binding.peopleCount.text = count.toString() + } + + override fun showScreeningDate(screeningDates: List) { + val dateSpinnerAdapter = ArrayAdapter( + this, + android.R.layout.simple_spinner_item, + screeningDates, + ) + + binding.dateSpinner.apply { + adapter = dateSpinnerAdapter + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long, + ) { + presenter.fetchScreeningTimes(parent?.getItemAtPosition(position) as LocalDate) + } + + override fun onNothingSelected(parent: AdapterView<*>?) = Unit + } + } + } + + override fun showScreeningTimes(screeningTimes: List) { + val timeSpinnerAdapter = ArrayAdapter( + this, + android.R.layout.simple_spinner_item, + screeningTimes, + ) + binding.timeSpinner.apply { + adapter = timeSpinnerAdapter + } + } + + override fun setMovieData(movie: MovieUiModel, theaterName: String) { + binding.apply { + this.movie = movie + moviePoster.setImageResource(movie.posterResourceId) + movieScreeningDate.text = getString(R.string.screening_date_format).format( + movie.startDate.format(DATE_FORMATTER), + movie.endDate.format(DATE_FORMATTER), + ) + } + setReserveButtonClickListener(movie, theaterName) + } + + private fun setReserveButtonClickListener(movie: MovieUiModel, theaterName: String) { + binding.reservationButton.setOnClickListener { + val reservationOptions = ReservationOptions( + movie.title, + LocalDateTime.of( + binding.dateSpinner.selectedItem as LocalDate, + binding.timeSpinner.selectedItem as LocalTime, + ), + binding.peopleCount.text.toString().toInt(), + theaterName, + ) + startActivity(SeatSelectionActivity.newIntent(this, reservationOptions, movie)) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + outState.apply { + putInt(PEOPLE_COUNT, binding.peopleCount.text.toString().toInt()) + putInt(SELECTED_DATE_POSITION, binding.dateSpinner.selectedItemPosition) + putInt(SELECTED_TIME_POSITION, binding.timeSpinner.selectedItemPosition) + } + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + setCount(savedInstanceState.getInt(PEOPLE_COUNT)) + binding.dateSpinner.setSelection(savedInstanceState.getInt(SELECTED_DATE_POSITION)) + binding.timeSpinner.setSelection(savedInstanceState.getInt(SELECTED_TIME_POSITION)) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> finish() + } + return super.onOptionsItemSelected(item) + } + + companion object { + private const val PEOPLE_COUNT = "PEOPLE_COUNT" + + private const val SELECTED_DATE_POSITION = "SELECTED_DATE_POSITION" + private const val SELECTED_TIME_POSITION = "SELECTED_TIME_POSITION" + private const val MOVIE = "MOVIE" + private const val THEATER = "THEATER" + + fun newIntent(context: Context, movie: MovieUiModel, theaterName: String): Intent { + val intent = Intent(context, ReservationActivity::class.java) + intent.putExtra(MOVIE, movie) + intent.putExtra(THEATER, theaterName) + return intent + } + } +} diff --git a/app/src/main/java/woowacourse/movie/view/reservation/ReservationContract.kt b/app/src/main/java/woowacourse/movie/view/reservation/ReservationContract.kt new file mode 100644 index 000000000..4fa7b1ea2 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/reservation/ReservationContract.kt @@ -0,0 +1,23 @@ +package woowacourse.movie.view.reservation + +import woowacourse.movie.view.model.MovieUiModel +import java.time.LocalDate +import java.time.LocalTime + +interface ReservationContract { + interface View { + fun setCount(count: Int) + fun setMovieData(movie: MovieUiModel, theaterName: String) + fun showScreeningDate(screeningDates: List) + fun showScreeningTimes(screeningTimes: List) + } + + interface Presenter { + + fun fetchMovieData() + fun fetchScreeningDates() + fun fetchScreeningTimes(date: LocalDate) + fun minusCount() + fun plusCount() + } +} diff --git a/app/src/main/java/woowacourse/movie/view/reservation/ReservationPresenter.kt b/app/src/main/java/woowacourse/movie/view/reservation/ReservationPresenter.kt new file mode 100644 index 000000000..6c7ab2a17 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/reservation/ReservationPresenter.kt @@ -0,0 +1,50 @@ +package woowacourse.movie.view.reservation + +import woowacourse.movie.domain.movie.ScreeningDateTimes +import woowacourse.movie.util.getOrEmptyList +import woowacourse.movie.view.model.MovieUiModel +import java.time.LocalDate + +class ReservationPresenter( + private val view: ReservationContract.View, + private val movie: MovieUiModel, + private val theater: String, +) : + ReservationContract.Presenter { + private var peopleCountSaved = 1 + private val screeningDateTimes: ScreeningDateTimes + + init { + val startDate = movie.startDate + val endDate = movie.endDate + val times = movie.schedule.getOrEmptyList(theater) + screeningDateTimes = ScreeningDateTimes.of(startDate, endDate, times) + } + + override fun fetchMovieData() { + view.setMovieData(movie, theater) + } + + override fun fetchScreeningDates() { + view.showScreeningDate(screeningDateTimes.dateTimes.keys.toList()) + } + + override fun fetchScreeningTimes(date: LocalDate) { + val times = screeningDateTimes.dateTimes[date] + times?.let { view.showScreeningTimes(it) } + } + + override fun minusCount() { + if (peopleCountSaved > 1) { + peopleCountSaved-- + view.setCount(peopleCountSaved) + } + } + + override fun plusCount() { + if (peopleCountSaved < 20) { + peopleCountSaved++ + view.setCount(peopleCountSaved) + } + } +} diff --git a/app/src/main/java/woowacourse/movie/view/ReservationCompletedActivity.kt b/app/src/main/java/woowacourse/movie/view/reservationcompleted/ReservationCompletedActivity.kt similarity index 55% rename from app/src/main/java/woowacourse/movie/view/ReservationCompletedActivity.kt rename to app/src/main/java/woowacourse/movie/view/reservationcompleted/ReservationCompletedActivity.kt index 993ab2bd4..33577d286 100644 --- a/app/src/main/java/woowacourse/movie/view/ReservationCompletedActivity.kt +++ b/app/src/main/java/woowacourse/movie/view/reservationcompleted/ReservationCompletedActivity.kt @@ -1,12 +1,9 @@ -package woowacourse.movie.view +package woowacourse.movie.view.reservationcompleted import android.Manifest -import android.app.NotificationChannel -import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.content.SharedPreferences import android.content.pm.PackageManager import android.os.Build import android.os.Bundle @@ -15,36 +12,42 @@ import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import androidx.preference.PreferenceManager import woowacourse.movie.R +import woowacourse.movie.data.setting.SettingPreferencesRepository import woowacourse.movie.databinding.ActivityReservationCompletedBinding import woowacourse.movie.util.DATE_FORMATTER +import woowacourse.movie.util.DECIMAL_FORMAT import woowacourse.movie.util.TIME_FORMATTER import woowacourse.movie.util.getParcelableCompat +import woowacourse.movie.view.AlarmController import woowacourse.movie.view.model.ReservationUiModel import woowacourse.movie.view.moviemain.MovieMainActivity import woowacourse.movie.view.moviemain.setting.SettingFragment -import woowacourse.movie.view.seatselection.AlarmReceiver -import java.text.DecimalFormat -class ReservationCompletedActivity : AppCompatActivity() { +class ReservationCompletedActivity : AppCompatActivity(), ReservationCompletedContract.View { private lateinit var binding: ActivityReservationCompletedBinding - private lateinit var sharedPreferences: SharedPreferences + private lateinit var alarmController: AlarmController + private lateinit var presenter: ReservationCompletedContract.Presenter + private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> + presenter.setAlarm(isGranted) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityReservationCompletedBinding.inflate(layoutInflater) setContentView(binding.root) - val reservation = intent.getParcelableCompat(RESERVATION) - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) - val isAlarmOn = sharedPreferences.getBoolean(SettingFragment.IS_ALARM_ON, false) + alarmController = AlarmController(this) - requestNotificationPermission() + presenter = ReservationCompletedPresenter(this, SettingPreferencesRepository(this)) + val reservation = intent.getParcelableCompat(RESERVATION) + binding.reservation = reservation reservation?.let { initViewData(it) - if (isAlarmOn) setAlarm(reservation, SettingFragment.ALARM_MINUTE_INTERVAL) + requestNotificationPermission() + presenter.decideAlarm(it) } supportActionBar?.setDisplayHomeAsUpEnabled(true) @@ -52,60 +55,42 @@ class ReservationCompletedActivity : AppCompatActivity() { this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - val intent = Intent(this@ReservationCompletedActivity, MovieMainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - startActivity(intent) + onBack() } }, ) } - private fun setAlarm(reservation: ReservationUiModel, alarmMinuteInterval: Long) { - val alarmController = AlarmController(this) - createChannel() - alarmController.registerAlarm(reservation, alarmMinuteInterval) + override fun registerAlarm(reservation: ReservationUiModel) { + alarmController.registerAlarm(reservation, SettingFragment.ALARM_MINUTE_INTERVAL) } private fun initViewData(reservation: ReservationUiModel) { - binding.apply { - movieTitle.text = reservation.title + with(binding) { movieScreeningDate.text = getString( R.string.datetime_with_space, reservation.screeningDateTime.format(DATE_FORMATTER), reservation.screeningDateTime.format(TIME_FORMATTER), ) - peopleCount.text = getString(R.string.reservation_people_count_format) - .format( - getString(R.string.general_person), - reservation.peopleCount, - reservation.seats.joinToString(), - ) + peopleCount.text = getString(R.string.reservation_count_seats_theater, reservation.count, reservation.seats.joinToString(), reservation.theaterName) totalPrice.text = - getString(R.string.total_price_format).format(DECIMAL_FORMAT.format(reservation.finalReservationFee)) + getString(R.string.total_price_format, DECIMAL_FORMAT.format(reservation.price)) } } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> { - val intent = Intent(this, MovieMainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - startActivity(intent) + onBack() } } return super.onOptionsItemSelected(item) } - private fun createChannel() { - val name = "Reservation Notification" - val channel = NotificationChannel( - AlarmReceiver.CHANNEL_ID, - name, - NotificationManager.IMPORTANCE_DEFAULT, - ) - val notificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) + private fun onBack() { + val intent = Intent(this, MovieMainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + startActivity(intent) } private fun requestNotificationPermission() { @@ -116,20 +101,8 @@ class ReservationCompletedActivity : AppCompatActivity() { } } - private val requestPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission(), - ) { isGranted: Boolean -> - val editor: SharedPreferences.Editor = sharedPreferences.edit() - if (isGranted) { - editor.putBoolean(SettingFragment.IS_ALARM_ON, true).apply() - return@registerForActivityResult - } - editor.putBoolean(SettingFragment.IS_ALARM_ON, false).apply() - } - companion object { private const val RESERVATION = "RESERVATION" - private val DECIMAL_FORMAT = DecimalFormat("#,###") const val REQUEST_CODE = 101 fun newIntent(context: Context, reservation: ReservationUiModel): Intent { diff --git a/app/src/main/java/woowacourse/movie/view/reservationcompleted/ReservationCompletedContract.kt b/app/src/main/java/woowacourse/movie/view/reservationcompleted/ReservationCompletedContract.kt new file mode 100644 index 000000000..8b89458a5 --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/reservationcompleted/ReservationCompletedContract.kt @@ -0,0 +1,13 @@ +package woowacourse.movie.view.reservationcompleted + +import woowacourse.movie.view.model.ReservationUiModel + +interface ReservationCompletedContract { + interface View { + fun registerAlarm(reservation: ReservationUiModel) + } + interface Presenter { + fun decideAlarm(reservation: ReservationUiModel) + fun setAlarm(isOn: Boolean) + } +} diff --git a/app/src/main/java/woowacourse/movie/view/reservationcompleted/ReservationCompletedPresenter.kt b/app/src/main/java/woowacourse/movie/view/reservationcompleted/ReservationCompletedPresenter.kt new file mode 100644 index 000000000..6ca50952a --- /dev/null +++ b/app/src/main/java/woowacourse/movie/view/reservationcompleted/ReservationCompletedPresenter.kt @@ -0,0 +1,13 @@ +package woowacourse.movie.view.reservationcompleted + +import woowacourse.movie.domain.repository.SettingRepository +import woowacourse.movie.view.model.ReservationUiModel + +class ReservationCompletedPresenter(private val view: ReservationCompletedContract.View, private val settingManager: SettingRepository) : ReservationCompletedContract.Presenter { + override fun decideAlarm(reservation: ReservationUiModel) { + if (settingManager.getIsAlarmSetting()) view.registerAlarm(reservation) + } + override fun setAlarm(isOn: Boolean) { + settingManager.setIsAlarmSetting(isOn) + } +} diff --git a/app/src/main/java/woowacourse/movie/view/seatselection/SeatSelectionActivity.kt b/app/src/main/java/woowacourse/movie/view/seatselection/SeatSelectionActivity.kt index 717c07399..0b19ef6ca 100644 --- a/app/src/main/java/woowacourse/movie/view/seatselection/SeatSelectionActivity.kt +++ b/app/src/main/java/woowacourse/movie/view/seatselection/SeatSelectionActivity.kt @@ -2,189 +2,158 @@ package woowacourse.movie.view.seatselection import android.content.Context import android.content.Intent +import android.graphics.Typeface import android.os.Bundle +import android.view.Gravity import android.view.MenuItem -import android.widget.Button import android.widget.TableLayout import android.widget.TableRow +import android.widget.TextView +import android.widget.Toast import android.widget.Toolbar.LayoutParams import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources -import androidx.appcompat.widget.AppCompatButton +import androidx.core.content.ContextCompat import androidx.core.view.children import woowacourse.movie.R -import woowacourse.movie.data.ReservationMockRepository +import woowacourse.movie.data.reservation.ReservationDbRepository +import woowacourse.movie.data.theater.TheaterMockRepository import woowacourse.movie.databinding.ActivitySeatSelectionBinding -import woowacourse.movie.domain.ReservationAgency -import woowacourse.movie.domain.Seat -import woowacourse.movie.domain.repository.ReservationRepository +import woowacourse.movie.domain.theater.Grade +import woowacourse.movie.util.DECIMAL_FORMAT import woowacourse.movie.util.getParcelableCompat -import woowacourse.movie.view.ReservationCompletedActivity -import woowacourse.movie.view.mapper.toDomainModel -import woowacourse.movie.view.mapper.toUiModel -import woowacourse.movie.view.model.MovieListModel.MovieUiModel +import woowacourse.movie.view.model.MovieUiModel import woowacourse.movie.view.model.ReservationOptions +import woowacourse.movie.view.model.ReservationUiModel import woowacourse.movie.view.model.SeatUiModel -import java.text.DecimalFormat +import woowacourse.movie.view.model.TheaterUiModel +import woowacourse.movie.view.model.row +import woowacourse.movie.view.reservationcompleted.ReservationCompletedActivity -class SeatSelectionActivity : AppCompatActivity() { +class SeatSelectionActivity : AppCompatActivity(), SeatSelectionContract.View { private lateinit var binding: ActivitySeatSelectionBinding - private val reservationRepository: ReservationRepository = ReservationMockRepository - private val reservationOptions by lazy { - intent.getParcelableCompat(RESERVATION_OPTIONS) + + private lateinit var presenter: SeatSelectionContract.Presenter + + private val seats: List by lazy { + binding.layoutSeats.children + .filterIsInstance() + .flatMap { it.children } + .filterIsInstance().toList() } - private lateinit var reservationAgency: ReservationAgency - private var selectedSeatCount = 0 - private var selectedSeats: List = emptyList() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivitySeatSelectionBinding.inflate(layoutInflater) setContentView(binding.root) - initSeatButtons() - initReserveLayout() - initReservationAgency() - initConfirmReservationButton() + val options = intent.getParcelableCompat(RESERVATION_OPTIONS) + if (options == null) { + Toast.makeText(this, DATA_LOADING_ERROR_MESSAGE, Toast.LENGTH_LONG).show() + finish() + return + } + binding.options = options + presenter = SeatSelectionPresenter( + this, + options, + ReservationDbRepository(this), + TheaterMockRepository, + ) + + presenter.fetchSeatsData( + mapOf( + Grade.B to R.color.seat_rank_b, + Grade.S to R.color.seat_rank_s, + Grade.A to R.color.seat_rank_a, + ), + ) + setNextButton() supportActionBar?.setDisplayHomeAsUpEnabled(true) } - private fun initSeatButtons() { - for (row in Seat.MIN_ROW..Seat.MAX_ROW) { + override fun createSeats(theaterUiModel: TheaterUiModel) { + for (row in 0 until theaterUiModel.maxRow) { val tableRow = TableRow(this).apply { layoutParams = TableLayout.LayoutParams(0, 0, 1f) } - for (col in Seat.MIN_COLUMN..Seat.MAX_COLUMN) { - val seat = Seat(col, row) - tableRow.addView(createSeat(this, seat.toUiModel())) + for (col in 0 until theaterUiModel.maxCol) { + tableRow.addView(createSeat(SeatUiModel(row, col, theaterUiModel.colorOfRow[row] ?: 0))) } - binding.seatTablelayout.addView(tableRow) + binding.layoutSeats.addView(tableRow) } } - private fun createSeat(context: Context, seatUi: SeatUiModel): AppCompatButton = - AppCompatButton(context).apply { - text = seatUi.name - setTextColor(getColor(seatUi.color)) - setOnClickListener { onSeatClick(this) } + private fun createSeat(seat: SeatUiModel): TextView { + val textView = TextView(this).apply { + text = seat.seatId + setTextColor(ContextCompat.getColor(context, seat.color)) + setTypeface(null, Typeface.BOLD) + textSize = 22F + textAlignment = TextView.TEXT_ALIGNMENT_CENTER + gravity = Gravity.CENTER + setOnClickListener { presenter.updateSeat(seat.row, seat.col) } background = - AppCompatResources.getDrawable(this@SeatSelectionActivity, R.drawable.selector_seat) + AppCompatResources.getDrawable(this@SeatSelectionActivity, R.drawable.seat_selector) layoutParams = TableRow.LayoutParams(0, LayoutParams.MATCH_PARENT, 1f) } - - private fun onSeatClick(seat: Button) { - if (seat.isSelected) { - deselectSeat(seat) - return - } - selectSeat(seat) + return textView } - private fun deselectSeat(seat: Button) { - selectedSeatCount-- - seat.isSelected = false - binding.confirmReservationButton.isEnabled = false - binding.reservationFeeTextview.text = getString(R.string.reservation_fee_format).format( - DECIMAL_FORMAT.format(0), - ) + private fun setNextButton() { + binding.btnNext.setOnClickListener { + showSubmitDialog() + } } - private fun selectSeat(seat: Button) { - reservationOptions?.let { - if (selectedSeatCount < it.peopleCount) { - seat.isSelected = true - selectedSeatCount++ - if (selectedSeatCount == it.peopleCount) { - onSelectionComplete() - return - } + private fun showSubmitDialog() { + AlertDialog.Builder(this).run { + setTitle(context.getString(R.string.reserve_dialog_title)) + setMessage(context.getString(R.string.reserve_dialog_detail)) + setPositiveButton(context.getString(R.string.reserve_dialog_submit)) { _, _ -> + presenter.reserve() } - } + setNegativeButton(context.getString(R.string.reserve_dialog_cancel)) { dialog, _ -> + dialog.dismiss() + } + setCancelable(false) + }.show() } - private fun onSelectionComplete() { - val seats = binding.seatTablelayout.children - .filterIsInstance() - .flatMap { it.children } - .filterIsInstance