Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[둘리] 1, 2단계 영화 티켓 예매 제출합니다. #7

Merged
merged 71 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
81ab027
docs: 클래스와 액티비티 설계도 작성
whk06061 Apr 11, 2023
545de51
feat: Movie 데이터 클래스 작성
whk06061 Apr 11, 2023
766d546
docs: 설계도 수정
whk06061 Apr 11, 2023
4c79b1a
feat: Price 클래스 구현
whk06061 Apr 11, 2023
4396e2c
feat: TicketingInfo 클래스 구현
whk06061 Apr 11, 2023
282882b
refactor: TicketingInfo name->title로 수정
whk06061 Apr 11, 2023
4690eae
feat: ListView, ListAdapter 구현
whk06061 Apr 11, 2023
2bc5a77
feat: 문자열 상수 @string으로 분할
whk06061 Apr 11, 2023
15cf193
feat: MovieListActivity 구현
whk06061 Apr 11, 2023
c75d5a7
feat: activity_ticketing xml 구현
whk06061 Apr 11, 2023
e38faa0
refactor: xml 더미데이터 삭제
whk06061 Apr 11, 2023
2616c9a
refactor: Price 클래스 value class 로 변경
whk06061 Apr 11, 2023
7c5e91f
feat: TicketingActivity 구현
whk06061 Apr 11, 2023
761850e
feat: activity_movie_ticket xml 구현
whk06061 Apr 11, 2023
013a294
feat: MovieTicketActivity 구현
whk06061 Apr 11, 2023
adf25b7
refactor: activity_movie_ticket.xml 글자 볼드 처리 수정
whk06061 Apr 11, 2023
f50a2ce
refactor: ViewHolder 적용
whk06061 Apr 11, 2023
48a66c5
refactor: 불필요한 MainActivity 삭제
whk06061 Apr 11, 2023
47f12c8
refactor: 상영일, 러닝타임 문자 출력 수정
whk06061 Apr 11, 2023
443e903
refactor: activity_ticketing.xml 본문 스크롤 되도록 수정
whk06061 Apr 12, 2023
e7db664
docs: 클래스 설계도 수정
whk06061 Apr 12, 2023
aa74fa9
feat: PlayingTimes 구현
whk06061 Apr 12, 2023
c05ff0e
refactor: PlayintDate, PlayingTime 따로 받도록 수정
whk06061 Apr 12, 2023
fc093c7
refactor: 직렬화되도록 수정
whk06061 Apr 12, 2023
3f5cc8f
feat: 날짜 시간 선택 spinner 구현
whk06061 Apr 12, 2023
b512714
feat: 할인 정책 구현
whk06061 Apr 12, 2023
6d2199c
feat: 화면 회전 시 데이터 유지 구현
whk06061 Apr 12, 2023
520d74b
refactor: 영화 더미 데이터 가져오는 함수 분리
whk06061 Apr 13, 2023
3ddf00b
refactor: 로직 함수로 분리
whk06061 Apr 13, 2023
fe47ef3
feat: Formatter 구현
whk06061 Apr 13, 2023
5525f7b
feat: Intent getSerializable 확장함수 구현
whk06061 Apr 13, 2023
7dce42c
refactor: MovieListAdapter, MovieTicketActivity 리팩터링
whk06061 Apr 13, 2023
70e7f6b
test: FormatterTest 구현
whk06061 Apr 13, 2023
6b3843c
refactor: TicketingActivity.kt 코드 리팩토링
whk06061 Apr 13, 2023
c881fba
refactor: 패키지 구조 분리 및 액티비티 명 수정
whk06061 Apr 13, 2023
4d17d10
refactor: SpinnerListener, Keys 분리
hyemdooly Apr 14, 2023
10f416c
refactor: DummyData 분리
hyemdooly Apr 14, 2023
303b877
refactor: getString 필요없는 format 함수 사용 삭제
hyemdooly Apr 14, 2023
ed85463
refactor: require 수정
hyemdooly Apr 14, 2023
452126c
refactor: ViewHolder 필드들 nullable인 것 수정
hyemdooly Apr 14, 2023
77afa95
refactor: PlayingTimes 리팩터링
hyemdooly Apr 14, 2023
54b5c30
fix: Exception 발생 버그 수정
hyemdooly Apr 14, 2023
8609c35
refactor: Policies 따로 분할, 범용적으로 사용할 수 있도록 수정
hyemdooly Apr 14, 2023
2e67294
test: DiscountPolicy 클래스 변경으로 인한 테스트 수정 및 추가
hyemdooly Apr 14, 2023
187af84
refactor: Formatter 삭제
hyemdooly Apr 14, 2023
6fc6324
refactor: 모든 layout Linear에서 Constraint로 변경
hyemdooly Apr 14, 2023
a5e2e98
refactor: ktlintFormat 적용
hyemdooly Apr 14, 2023
37cf719
refactor: FormatterTest 삭제
hyemdooly Apr 14, 2023
b8c06a2
refactor: null일시 토스트 띄운 후 뒤로가도록 수정
hyemdooly Apr 15, 2023
e2c4c55
refactor: 확장함수명 변경
hyemdooly Apr 15, 2023
0fe291d
refactor: 패키지 변경 및 뷰 값 세팅 클래스 분리
hyemdooly Apr 15, 2023
dc6ad79
refactor: 패키지 변경
hyemdooly Apr 15, 2023
e062f6f
refactor: MovieDetailActivity, TicketResultActivity 함수 분리
hyemdooly Apr 15, 2023
9864e56
refactor: DummyData 추가, 변수명 변경
hyemdooly Apr 15, 2023
5cca9a1
refactor: MovieDTO 추가
hyemdooly Apr 15, 2023
a45c442
refactor: Key들 위치 변경, Keys object 삭제
hyemdooly Apr 16, 2023
9f8e452
refactor: 확장함수명 변경
hyemdooly Apr 16, 2023
d04bd6c
refactor: else문 삭제
hyemdooly Apr 16, 2023
5336de6
refactor: scope function run 대신 with 사용
hyemdooly Apr 17, 2023
77cd778
refactor: package 이동, MovieDTO 및 Mapper 수정
hyemdooly Apr 17, 2023
1c36e77
refactor: view를 객체 내부에서 찾도록 변경
hyemdooly Apr 17, 2023
186d6f6
refactor: key가 없으면 빈 리스트를 반환하는 Map 확장함수 구현
hyemdooly Apr 17, 2023
b1bffce
refactor: 모듈 분리
hyemdooly Apr 18, 2023
b13a913
refactor: ViewHolders Map 생성, set 함수 이동
hyemdooly Apr 18, 2023
f8c3245
refactor: MovieListItemListener 생성 후 Listener 분할
hyemdooly Apr 18, 2023
ae36175
refactor: getKeyFromIndex Map 확장함수 생성
hyemdooly Apr 18, 2023
f2bfce9
refactor: MovieDTO -> MovieModel 네이밍 변경
hyemdooly Apr 18, 2023
1dc76f1
refactor: View Setting 함수 하나로 합치기
hyemdooly Apr 18, 2023
ac7b7e6
refactor: SpinnerAdapter 함수 분리
hyemdooly Apr 18, 2023
39215c6
refactor: TicketingInfo -> Ticket으로 변경, payment 삭제
hyemdooly Apr 18, 2023
b89051c
refactor: TicketModel 생성, 도메인 객체 의존성 제거
hyemdooly Apr 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Comment on lines +12 to +13

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 스피너 리스너는 다른 스피너에 크게 의존적이에요.
동일한 스피너 리스너를 재활용할 수 있게 할 수 있을까요?

혹은 이 리스너만 따로 클래스로 분리되는 게 적절할지도 고민해보면 좋겠어요 :)

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)
Comment on lines +17 to +24

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흐름도 분리도 아주 깔끔해졌네요 👍
덕분에 액티비티 라인 수가 아주 줄어들었네요 :)

아래는 조금 어렵지만, 도전해보셔도 좋고 공부만 해보셔도 좋아요 :)
MovieDetailView를 아래 처럼 xml에 바로 세팅해볼 수 있을까요?

<androidx.constraintlayout.widget.ConstraintLayout>

    <...MovieDetailView
        android:....
        android:.... />

    <...ReservationInfoView
        android:....
        android:.... />

</androidx.constraintlayout.widget.ConstraintLayout>

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)
}
Comment on lines +12 to +18

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set을 할 때마다 findViewId를 실행해주어야겠네요.
한 번만 해보는 방법도 있겠네요 :)


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)
Comment on lines +53 to +54

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 View 객체를 재활용해야할 때 어떤 문제가 발생할 수 있을까요?
이 View만이 가져야 할 역할 이외의 것은 외부에서 받아보는 건 어떨까요?

}
}

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)
}
}
Comment on lines +22 to +28

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 어떤 이유로 setOnClickLisntener { } 처럼 바로 람다로 표현하지 못할 수 밖에 없었을까요? 🤔

)
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()
Comment on lines +16 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

더 이상 tag에 저장하지 않게 되었네요 👍

아래는 구현하지 않고 고민해보세요 :)

  • 다른 어뎁터들이 같은 ViewHolder를 공유해볼 수 있을까요?
  • View와 ViewHolder를 1대1 매핑해서 관리하지 않고, ViewHolder만을 관리해볼 수 있을까요?

위 내용들은 RecyclerView에서 어떻게 해결했을 지도 잘 참고해보세요 😊

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