Skip to content

Commit

Permalink
[둘리] 1, 2단계 영화 티켓 예매 제출합니다. (#7)
Browse files Browse the repository at this point in the history
* docs: 클래스와 액티비티 설계도 작성

* feat: Movie 데이터 클래스 작성

* docs: 설계도 수정

* feat: Price 클래스 구현

* feat: TicketingInfo 클래스 구현

* refactor: TicketingInfo name->title로 수정

* feat: ListView, ListAdapter 구현

* feat: 문자열 상수 @string으로 분할

* feat: MovieListActivity 구현

* feat: activity_ticketing xml 구현

* refactor: xml 더미데이터 삭제

* refactor: Price 클래스 value class 로 변경

* feat: TicketingActivity 구현

* feat: activity_movie_ticket xml 구현

* feat: MovieTicketActivity 구현

* refactor: activity_movie_ticket.xml 글자 볼드 처리 수정

* refactor: ViewHolder 적용

* refactor: 불필요한 MainActivity 삭제

* refactor: 상영일, 러닝타임 문자 출력 수정

* refactor: activity_ticketing.xml 본문 스크롤 되도록 수정

* docs: 클래스 설계도 수정

* feat: PlayingTimes 구현

* refactor: PlayintDate, PlayingTime 따로 받도록 수정

* refactor: 직렬화되도록 수정

* feat: 날짜 시간 선택 spinner 구현

* feat: 할인 정책 구현

* feat: 화면 회전 시 데이터 유지 구현

* refactor: 영화 더미 데이터 가져오는 함수 분리

* refactor: 로직 함수로 분리

* feat: Formatter 구현

* feat: Intent getSerializable 확장함수 구현

* refactor: MovieListAdapter, MovieTicketActivity 리팩터링

* test: FormatterTest 구현

* refactor: TicketingActivity.kt 코드 리팩토링

* refactor: 패키지 구조 분리 및 액티비티 명 수정

* refactor: SpinnerListener, Keys 분리

* refactor: DummyData 분리

* refactor: getString 필요없는 format 함수 사용 삭제

* refactor: require 수정

* refactor: ViewHolder 필드들 nullable인 것 수정

* refactor: PlayingTimes 리팩터링

* fix: Exception 발생 버그 수정

* refactor: Policies 따로 분할, 범용적으로 사용할 수 있도록 수정

* test: DiscountPolicy 클래스 변경으로 인한 테스트 수정 및 추가

* refactor: Formatter 삭제

* refactor: 모든 layout Linear에서 Constraint로 변경

* refactor: ktlintFormat 적용

* refactor: FormatterTest 삭제

* refactor: null일시 토스트 띄운 후 뒤로가도록 수정

* refactor: 확장함수명 변경

* refactor: 패키지 변경 및 뷰 값 세팅 클래스 분리

* refactor: 패키지 변경

* refactor: MovieDetailActivity, TicketResultActivity 함수 분리

* refactor: DummyData 추가, 변수명 변경

* refactor: MovieDTO 추가

* refactor: Key들 위치 변경, Keys object 삭제

* refactor: 확장함수명 변경

* refactor: else문 삭제

* refactor: scope function run 대신 with 사용

* refactor: package 이동, MovieDTO 및 Mapper 수정

* refactor: view를 객체 내부에서 찾도록 변경

* refactor: key가 없으면 빈 리스트를 반환하는 Map 확장함수 구현

* refactor: 모듈 분리

* refactor: ViewHolders Map 생성, set 함수 이동

* refactor: MovieListItemListener 생성 후 Listener 분할

* refactor: getKeyFromIndex Map 확장함수 생성

* refactor: MovieDTO -> MovieModel 네이밍 변경

* refactor: View Setting 함수 하나로 합치기

* refactor: SpinnerAdapter 함수 분리

* refactor: TicketingInfo -> Ticket으로 변경, payment 삭제

* refactor: TicketModel 생성, 도메인 객체 의존성 제거

---------

Co-authored-by: whk06061 <[email protected]>
  • Loading branch information
hyemdooly and whk06061 committed Apr 19, 2023
1 parent 1b1ace6 commit 9924400
Show file tree
Hide file tree
Showing 45 changed files with 1,288 additions and 32 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# android-movie-ticket
# android-movie-ticket

## 액티비티
- MovieListActivity
- TicketingActivity
- MovieTicketActivity

## 어댑터
- MoviesAdapter

## 데이터
- Movie
- 이미지, 제목, 상영일(PlayingTimes), 러닝타임, 소개
- TicketingInfo
- 영화 이름, 상영일, 몇명, 가격, 무슨 결제
- Price
- 음수 체크
- 티켓 한 장의 가격은 13000원
- 영화 가격에 할인을 적용한다.
- Discount 구현 클래스를 리스트로 받는다.
- PlayingTimes
- 날짜-상영시간 map 타입으로 가지고 있다.
- Discount (인터페이스)
- calculate 메소드
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ dependencies {
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation(project(":domain"))
}
10 changes: 8 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
android:theme="@style/Theme.Movie"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".activity.ticketresult.TicketResultActivity"
android:exported="false" />
<activity
android:name=".activity.moviedetail.MovieDetailActivity"
android:exported="false" />
<activity
android:name=".activity.movielist.MovieListActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand All @@ -22,4 +28,4 @@
</activity>
</application>

</manifest>
</manifest>
11 changes: 0 additions & 11 deletions app/src/main/java/woowacourse/movie/MainActivity.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package woowacourse.movie.activity.moviedetail

import android.R
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import woowacourse.movie.util.getKeyFromIndex
import woowacourse.movie.util.getOrEmptyList
import java.time.LocalDate
import java.time.LocalTime

class DateSpinnerListener(private val playingTimes: Map<LocalDate, List<LocalTime>>, private val spinnerTime: Spinner) : AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>?, view: View?, index: Int, p3: Long) {
val times = playingTimes.getOrEmptyList(playingTimes.getKeyFromIndex(index))
spinnerTime.adapter = ArrayAdapter(spinnerTime.context, R.layout.simple_spinner_item, times)
}

override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package woowacourse.movie.activity.moviedetail

import android.os.Bundle
import android.view.MenuItem
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import woowacourse.movie.R
import woowacourse.movie.model.MovieModel
import woowacourse.movie.util.getSerializableExtraCompat

class MovieDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_movie_detail)
val movie: MovieModel? = intent.getSerializableExtraCompat(MOVIE_KEY)
if (movie == null) {
Toast.makeText(this, DATA_LOADING_ERROR_MESSAGE, Toast.LENGTH_LONG).show()
finish()
return
}
MovieDetailView(findViewById(R.id.layout_detail_info)).set(movie)
ReservationInfoView(findViewById(R.id.layout_reservation_info)).set(savedInstanceState, movie)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
finish()
true
}
else -> {
super.onOptionsItemSelected(item)
}
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val countText = findViewById<TextView>(R.id.text_count)
val spinnerDate = findViewById<Spinner>(R.id.spinner_date)
val spinnerTime = findViewById<Spinner>(R.id.spinner_time)
outState.putInt(COUNT_KEY, countText.text.toString().toInt())
outState.putInt(SPINNER_DATE_KEY, spinnerDate.selectedItemPosition)
outState.putInt(SPINNER_TIME_KEY, spinnerTime.selectedItemPosition)
}

companion object {
private const val DATA_LOADING_ERROR_MESSAGE = "데이터가 로딩되지 않았습니다. 다시 시도해주세요."
const val MOVIE_KEY = "MOVIE"
const val COUNT_KEY = "COUNT"
const val SPINNER_DATE_KEY = "SPINNER_DATE"
const val SPINNER_TIME_KEY = "SPINNER_TIME"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package woowacourse.movie.activity.moviedetail

import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import woowacourse.movie.R
import woowacourse.movie.model.MovieModel
import java.time.LocalDate
import java.time.format.DateTimeFormatter

class MovieDetailView(private val viewGroup: ViewGroup) {
fun set(movie: MovieModel) {
setImageView(movie.image)
setTitle(movie.title)
setPlayingDate(movie.startDate, movie.endDate)
setRunningTime(movie.runningTime)
setDescription(movie.description)
}

private fun setDescription(description: String) {
viewGroup.findViewById<TextView>(R.id.text_description).text = description
}

private fun setRunningTime(runningTime: Int) {
viewGroup.findViewById<TextView>(R.id.text_running_time).text = viewGroup.context.getString(R.string.running_time, runningTime)
}

private fun setPlayingDate(startDate: LocalDate, endDate: LocalDate) {
viewGroup.findViewById<TextView>(R.id.text_playing_date).text = viewGroup.context.getString(
R.string.playing_date_range,
DateTimeFormatter.ofPattern(viewGroup.context.getString(R.string.date_format)).format(startDate),
DateTimeFormatter.ofPattern(viewGroup.context.getString(R.string.date_format)).format(endDate)
)
}

private fun setTitle(title: String) {
viewGroup.findViewById<TextView>(R.id.text_title).text = title
}

private fun setImageView(image: Int) {
viewGroup.findViewById<ImageView>(R.id.img_movie).setImageResource(image)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package woowacourse.movie.activity.moviedetail

import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.Spinner
import android.widget.TextView
import woowacourse.movie.R
import woowacourse.movie.activity.ticketresult.TicketResultActivity
import woowacourse.movie.domain.policy.DiscountPolicies
import woowacourse.movie.domain.ticket.Price
import woowacourse.movie.domain.ticket.Ticket
import woowacourse.movie.model.MovieModel
import woowacourse.movie.model.toPresentation
import woowacourse.movie.util.getKeyFromIndex
import woowacourse.movie.util.getOrEmptyList
import java.time.LocalDate
import java.time.LocalTime

class ReservationInfoView(private val viewGroup: ViewGroup) {

fun set(savedInstanceState: Bundle?, movie: MovieModel) {
val savedCount = savedInstanceState?.getInt(MovieDetailActivity.COUNT_KEY) ?: DEFAULT_COUNT
val savedDate =
savedInstanceState?.getInt(MovieDetailActivity.SPINNER_DATE_KEY) ?: DEFAULT_POSITION
val savedTime =
savedInstanceState?.getInt(MovieDetailActivity.SPINNER_TIME_KEY) ?: DEFAULT_POSITION

setCount(savedCount)
setMinusButton()
setPlusButton()
setReserveButton(movie.title)
setDateSpinner(savedDate, movie.playingDateTimes)
setTimeSpinner(
savedTime,
movie.playingDateTimes.getOrEmptyList(movie.playingDateTimes.getKeyFromIndex(savedDate))
)
}

private fun setReserveButton(title: String) {
viewGroup.findViewById<Button>(R.id.btn_reserve).setOnClickListener {
val intent = Intent(it.context, TicketResultActivity::class.java)
val ticket = Ticket.of(
DiscountPolicies.policies,
title,
viewGroup.findViewById<Spinner>(R.id.spinner_date).selectedItem as LocalDate,
viewGroup.findViewById<Spinner>(R.id.spinner_time).selectedItem as LocalTime,
viewGroup.findViewById<TextView>(R.id.text_count).text.toString().toInt(),
Price()
)
intent.putExtra(TicketResultActivity.INFO_KEY, ticket.toPresentation())
it.context.startActivity(intent)
}
}

private fun setTimeSpinner(savedTimePosition: Int, times: List<LocalTime>) {
val timeSpinner = viewGroup.findViewById<Spinner>(R.id.spinner_time)
timeSpinner.adapter = SpinnerAdapter(times)
timeSpinner.setSelection(savedTimePosition)
}

private fun setDateSpinner(
savedDatePosition: Int,
playingTimes: Map<LocalDate, List<LocalTime>>
) {
val dateSpinner = viewGroup.findViewById<Spinner>(R.id.spinner_date)
val timeSpinner = viewGroup.findViewById<Spinner>(R.id.spinner_time)
dateSpinner.adapter = SpinnerAdapter(playingTimes.keys.toList())
dateSpinner.setSelection(savedDatePosition, false)
dateSpinner.onItemSelectedListener = DateSpinnerListener(playingTimes, timeSpinner)
}

private fun setMinusButton() {
val minusButton = viewGroup.findViewById<Button>(R.id.btn_minus)
val countView = viewGroup.findViewById<TextView>(R.id.text_count)
minusButton.setOnClickListener {
val count = countView.text.toString().toInt()
if (count > 1) countView.text = (count - 1).toString()
}
}

private fun setPlusButton() {
val plusButton = viewGroup.findViewById<Button>(R.id.btn_plus)
val countView = viewGroup.findViewById<TextView>(R.id.text_count)
plusButton.setOnClickListener {
val count = countView.text.toString().toInt()
countView.text = (count + 1).toString()
}
}

private fun setCount(savedCount: Int) {
val countView = viewGroup.findViewById<TextView>(R.id.text_count)
countView.text = savedCount.toString()
}

private fun <T> SpinnerAdapter(items: List<T>) =
ArrayAdapter(viewGroup.context, android.R.layout.simple_spinner_item, items).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}

companion object {
private const val DEFAULT_COUNT = 1
private const val DEFAULT_POSITION = 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package woowacourse.movie.activity.movielist

import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.ListView
import androidx.appcompat.app.AppCompatActivity
import woowacourse.movie.R
import woowacourse.movie.activity.moviedetail.MovieDetailActivity
import woowacourse.movie.model.MovieModel
import woowacourse.movie.model.toPresentation
import woowacourse.movie.util.DummyData

class MovieListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_movie_list)

val listView = findViewById<ListView>(R.id.list_view)
val adapter = MovieListAdapter(
DummyData.movies.map { it.toPresentation(R.drawable.img) },
object : MovieListItemListener {
override fun onClick(movie: MovieModel, view: View) {
val intent = Intent(view.context, MovieDetailActivity::class.java)
intent.putExtra(MovieDetailActivity.MOVIE_KEY, movie)
view.context.startActivity(intent)
}
}
)
listView.adapter = adapter
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package woowacourse.movie.activity.movielist

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import woowacourse.movie.R
import woowacourse.movie.model.MovieModel
import java.time.format.DateTimeFormatter

class MovieListAdapter(private val movies: List<MovieModel>, private val listener: MovieListItemListener) : BaseAdapter() {
private val viewHolders: MutableMap<View, ViewHolder> = mutableMapOf()
override fun getCount(): Int {
return movies.size
}

override fun getItem(position: Int): Any {
return movies[position]
}

override fun getItemId(position: Int): Long {
return position.toLong()
}

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = convertView ?: LayoutInflater.from(parent?.context).inflate(R.layout.movie_item, null)
if (viewHolders[view] == null) viewHolders[view] = getViewHolder(view)
val movie = getItem(position) as MovieModel
viewHolders[view]?.set(movie, parent?.context) {
listener.onClick(movie, it)
}
return view
}

private fun getViewHolder(view: View): ViewHolder = ViewHolder(
view.findViewById(R.id.img_movie),
view.findViewById(R.id.text_title),
view.findViewById(R.id.text_playing_date),
view.findViewById(R.id.text_running_time),
view.findViewById(R.id.btn_reserve)
)
private class ViewHolder(
val image: ImageView,
val title: TextView,
val playingDate: TextView,
val runningTime: TextView,
val reserveButton: Button
) {
fun set(movie: MovieModel, context: Context?, clickListener: OnClickListener) {
image.setImageResource(movie.image)
title.text = movie.title
playingDate.text = context?.getString(
R.string.playing_date_range,
DateTimeFormatter.ofPattern(context.getString(R.string.date_format)).format(movie.startDate),
DateTimeFormatter.ofPattern(context.getString(R.string.date_format)).format(movie.endDate)
)
runningTime.text = context?.getString(R.string.running_time, movie.runningTime)
reserveButton.setOnClickListener(clickListener)
}
}
}
Loading

0 comments on commit 9924400

Please sign in to comment.