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

[둘리] 3, 4 단계 영화 티켓 예매 제출합니다. #47

Merged
merged 47 commits into from
Apr 24, 2023

Conversation

hyemdooly
Copy link

안녕하세요, 말리빈! 둘리입니다.☺️
좌석 선택 기능을 추가하면서 도메인 코드가 많이 추가되고 수정되었습니다!
row, col을 기준으로 5*4 좌석 뿐만 아니라 다양한 크기의 좌석들도 호환되도록 고려하여 도메인을 작성했는데, 뷰에서는 아직 해결하지 못했습니다.. 주말 내내 고민했는데 어떻게 해결해야할지 아직 잘 모르겠습니다🥲

다음 미션 시작날이 얼마 남지 않아 마감일보다 조금 더 빨리 제출합니다!
날카로운 피드백 부탁드립니다!

Copy link

@malibinYun malibinYun left a comment

Choose a reason for hiding this comment

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

3, 4단계 미션 고생 많으셨어요 :)
리사이클러뷰의 어뎁터 코드가 굉장히 재미있었습니다 ㅎ_ㅎ
고민해보면 좋을 점들에 코멘트 남겨두었으니 확인해주세요 :)
피드백 반영 후 다시 리뷰 요청 부탁드려요~

Comment on lines 28 to 32
private val intent =
Intent(ApplicationProvider.getApplicationContext(), MovieDetailActivity::class.java).apply {
putExtra(MovieDetailActivity.MOVIE_KEY, movieModel)
}

Choose a reason for hiding this comment

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

같은 화면이지만 특정 테스트에선 다른 intent 정보가 필요할 수도 있겠어요.
그럴 때는 어떻게 변경하면 좋을지 고민해볼까요?

Copy link
Author

@hyemdooly hyemdooly Apr 24, 2023

Choose a reason for hiding this comment

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

val intent = Intent(ApplicationProvider.getApplicationContext(), MovieDetailActivity::class.java)
intent.putExtra(MovieDetailActivity.MOVIE_KEY, movieModel)
activityScenario = ActivityScenario.launch(intent)

각 테스트에서 다음과 같이 intent 내용을 지정하여 launch로 실행해줄 수 있습니다!
함수로 분리하여 movieModel을 받아 실행하도록 수정했습니다. :)

Choose a reason for hiding this comment

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

@BeforeEach 를 활용해보세요 :)

Copy link
Author

@hyemdooly hyemdooly Apr 24, 2023

Choose a reason for hiding this comment

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

JUnit4에서 @Before는 파라미터를 받을 수 없고, 안드로이드에서는 JUnit4를 사용해야하는데 다른 방법이 있을까요?!

Copy link

Choose a reason for hiding this comment

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

이 부분은 제가 ServiceTestRule과 착각해서 각 activity를 실행시킬 때마다 다른 intent를 넘겨줄 수 있을 것으로 잘못 생각했어요.

ActivityScenarioRule에는 해당 메서드가 존재하지 않네요. 내부를 잘 들여다보니, 생성자로부터 intent를 한 번 넘겨주면 해당 intent로 액티비티를 실행시켜버려서 바꿀 방법이 없네요. 혼동을 드려서 죄송해요 😥

그래도 방법이 없을까, 고민을 많이 해봤는데요, 꽤나 어려운 방법일 것 같아서 방법만 공유해봅니다 😅

Rule class는 사실 before, after에 미리 해야 할 일을 모아 둔 별 거 없는 class 랍니다. ActivityScenarioRuleExternalResource를 상속 받고 있어요. 그래서 테스트를 실행할 때 @Before 붙어있는 메서드를 자동으로 실행해 주듯이, 이 내부에서도 실행시켜주어요 :)

ActivityScenarioRule의 내부는 생각보다 매우 간단한데요, 4개의 생성자를 통해 여러 동작을 지원하고, 해당 동작을 매 테스트 때마다 before를 통해 실행시켜줍니다.

다행스럽게도 내부에서 사용하는 ActivityScenario class는 public 이에요. 그렇기에 이 Rule class를 참고하고, intent 객체를 lazy하게 가져오게 만드는 기능을 추가해서 직접 Rule class를 만들어내면 충분히 intent를 교체할 수 있게 되어요.

ActivityScenarioRule이 final class 여서 상속을 받아 개조하는 방법은 통하지 않겠더라구요.

이 부분은 꼭 시도해보시지 않아도 좋아요.

간단히 접근할 수 없는 부분이었는데, 쉽게 접근할 수 있는 분야처럼 말씀드려 혼란을 드린 거 같아 죄송한 마음이에요.

많은 고민을 하셨지만 답을 찾지 못해 답답하실 것 같아서, 해결 방법 중 하나를 대신 고민해서 공유드려요 :)

Copy link
Author

Choose a reason for hiding this comment

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

이번 미션 기간에 여유가 생기면 같이 공부해보겠습니다ㅎㅎ
리뷰가 끝났는데도 이렇게 답변 주셔서 정말정말 감사합니다~~!! ☺️

Comment on lines 36 to 49
@Test
fun 제목을_띄운다() {
onView(withId(R.id.text_title)).check(matches(withText("해리포터와 마법사의 돌 1")))
}

@Test
fun 상영일을_띄운다() {
onView(withId(R.id.text_playing_date)).check(matches(withText("상영일: 2023.3.1 ~ 2023.3.31")))
}

@Test
fun 설명을_띄운다() {
onView(withId(R.id.text_description)).check(matches(withText("해리포터 첫 번째 시리즈")))
}

Choose a reason for hiding this comment

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

이 테스트는 한 종류의 케이스로 묶어도 좋겠다는 생각이 드네요 :)
UI 테스트는 메서드 하나 당 한 번 씩 앱을 시뮬레이트 하니까요!

Comment on lines 3 to 5
interface InjectedModelListener<T> {
fun onClick(model: T)
}

Choose a reason for hiding this comment

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

동일한 모델에 대한 리스너가 많이 생겨서 만든 의도로 보여요.

InjectedModelListener이름은 다른 사람들도 원하는 동작에 대해 한 눈에 알아볼 수 있을까요?
또, 한 클래스 생성자 등에 Type T 만 다른 이 클릭 리스너를 여러 개를 넣는다면 어떤 문제가 발생할까요?

Copy link
Author

@hyemdooly hyemdooly Apr 24, 2023

Choose a reason for hiding this comment

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

모델을 다른 액티비티에 전달하는 리스너 형태가 많아져서 생성한 것이 맞습니다.ㅎㅎ

말씀대로 인터페이스 이름이 한 눈에 알아볼 수 있는 이름이 아닌 것 같습니다.ㅜㅜ 이름 짓기 어렵네요..
그리고 지금처럼 이 T만 다른 Listener 구현이 여러 개 필요할 때, Activity에서 모두 implement할 수도 없습니다!

모델을 인자로 받는 onClick 본문 함수를 정의하고 싶어 해당 인터페이스를 만들었는데, 고차함수로 받는 것이 더 좋은 방법일까요?🤔
모든 모델에 대해 이 클래스를 따로 구현해주는 것은 context를 넘겨주어야할 것 같아 조금 꺼려집니다..!

Choose a reason for hiding this comment

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

어떨 때에 리스너에서 context가 필요한가요 ?_?

Copy link
Author

Choose a reason for hiding this comment

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

클릭했을 때 intent를 생성하고 싶으면 packageContext가 필요해서 넘겨주어야한다고 생각했습니다!

Choose a reason for hiding this comment

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

intent 를 생성해주는 것은 메서드를 넘겨주는 곳에서 만들게 하는 건 어떨까요?

람다와 크게 다르지 않습니다 :)
fun interface 에 대해 학습해보시면 더 이해가 높아질 거예요 🙂

private val dateSpinner = viewGroup.findViewById<Spinner>(R.id.spinner_date)
private val minusButton = viewGroup.findViewById<Button>(R.id.btn_minus)
private val countView = viewGroup.findViewById<TextView>(R.id.text_count)
private val plusButton = viewGroup.findViewById<Button>(R.id.btn_plus)

fun set(savedInstanceState: Bundle?, movie: MovieModel) {

Choose a reason for hiding this comment

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

view가 bundle에 대해 알아야할까요?
bundle 없이 데이터를 세팅해야할때는 어떤 변경점이 생겨날까요?

Copy link
Author

Choose a reason for hiding this comment

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

Bundle의 내용이 변경될 경우 ReservationView에서 많은 부분을 함께 변경해주어야합니다!
데이터를 세팅할 때 필요한 값만 넘겨주는 것이 Bundle의 의존을 막을 수 있을 것 같아 데이터 값을 받도록 수정했습니다.

Comment on lines 49 to 57
reserveButton.setOnClickListener {
val intent = Intent(it.context, SeatSelectActivity::class.java)
val date = viewGroup.findViewById<Spinner>(R.id.spinner_date).selectedItem as LocalDate
val time = viewGroup.findViewById<Spinner>(R.id.spinner_time).selectedItem as LocalTime
val count = viewGroup.findViewById<TextView>(R.id.text_count).text.toString().toInt()
intent.putExtra(SeatSelectActivity.TITLE_KEY, title)
intent.putExtra(SeatSelectActivity.DATETIME_KEY, LocalDateTime.of(date, time))
intent.putExtra(SeatSelectActivity.COUNT_KEY, count)
it.context.startActivity(intent)

Choose a reason for hiding this comment

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

다른 화면에서 이 View와 생긴 것은 같고 동작 이벤트가 다를 때, 이 View 객체는 재활용할 수 있는 형태일까요?

Copy link
Author

@hyemdooly hyemdooly Apr 24, 2023

Choose a reason for hiding this comment

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

다른 코드와 동일하게 intent를 넘겨주는 고차함수로 빼주었습니다!

Comment on lines 43 to 47
override fun getItemViewType(position: Int): Int {
if (position != 0 && position % 3 == 0) {
return AD_VIEWTYPE
}
return view
return NORMAL_VIEWTYPE

Choose a reason for hiding this comment

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

광고 배치의 규칙이 변경된다면 유연하게 변경될 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

enum class ListViewType에서 광고 아이템 주기를 입력 받아 Type을 리턴하는 getViewType을 작성했습니다!

Comment on lines 24 to 28
val title = intent.getStringExtra(TITLE_KEY)
val dateTime = intent.getSerializableExtraCompat<LocalDateTime>(DATETIME_KEY)
val count = intent.getIntExtra(COUNT_KEY, -1)

if (isDataNull(title, dateTime, count)) {

Choose a reason for hiding this comment

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

전달 받는 객체를 한 개로 묶어서 전달 받으면 isDataNull 같은 함수를 만들지 않아도 되겠네요 :)

Copy link
Author

Choose a reason for hiding this comment

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

객체를 생성해서 함수를 삭제했습니다!

Comment on lines 35 to 44
val priceSystem = PriceSystem(PriceCalculator(Theater.policies), dateTime!!)
val seatView =
SeatSelectView(
findViewById(R.id.layout_select_seat),
seatSelectSystem,
priceSystem,
this,
)

seatView.set(title!!, dateTime)

Choose a reason for hiding this comment

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

!! 연산자의 사용을 지양해주세요.
코틀린의 non-null 특장점을 발휘 하지 못하는 케이스입니다 :)

Copy link
Author

Choose a reason for hiding this comment

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

객체를 생성하여 null값을 체크하면서 !! 연산자도 지웠습니다!

Comment on lines +5 to +12
sealed class SelectResult {
sealed class Success(val seatPrice: Price) : SelectResult() {
class Selection(seatPrice: Price, val isSelectAll: Boolean) : Success(seatPrice)
class Deselection(seatPrice: Price) : Success(seatPrice)
}

object WrongInput : SelectResult()
object MaxSelection : SelectResult()

Choose a reason for hiding this comment

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

이벤트를 sealed class을 활용해 만들어주셨네요 👍

Comment on lines 37 to 44
@Test
fun `선택 후 선택한 자리의 가격을 함께 반환한다`() {
val actual = system.select(0, 0)
if (actual is SelectResult.Success.Selection) {
assertEquals(actual.seatPrice, Grade.B.price)
}
}

Choose a reason for hiding this comment

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

테스트 코드에 if문이 들어있는 것은 좋은 패턴이 아닙니다.
테스트 코드가 if를 통과하지 않고, else를 통과하면 테스트는 성공일까요 실패일까요?
콘솔에는 어떻게 나타날까요?

Copy link
Author

Choose a reason for hiding this comment

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

if문을 통과하지 않은 경우 성공으로 나오는 것을 확인했습니다😱
클래스 타입을 확인하고 가격을 확인하도록 수정했습니다!

@malibinYun
Copy link

row, col을 기준으로 5*4 좌석 뿐만 아니라 다양한 크기의 좌석들도 호환되도록 고려하여 도메인을 작성했는데, 뷰에서는 아직 해결하지 못했습니다.. 주말 내내 고민했는데 어떻게 해결해야할지 아직 잘 모르겠습니다🥲

동적인 View를 만드는 것은 사실 쉬운 일이 아니에요.
너무 어렵고 시간이 많이 든다면 꼭 반영하지 않으셔도 됩니다.
미션 목표에 더 집중하셔도 좋겠어요 :)

Copy link

@malibinYun malibinYun left a comment

Choose a reason for hiding this comment

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

미션 고생 많으셨어요 :)
저와 함께 소통한 내용이 앞으로 개발하실 때 도움이 되셨으면 좋겠네요 🙂
미션은 이만 머지하겠습니다.
다른 미션들도 잘 마무리하시길 응원할게요! 💪

Comment on lines +20 to +26
private val movieModel = Movie.of(
"해리포터와 마법사의 돌 1",
LocalDate.of(2023, 3, 1),
LocalDate.of(2023, 3, 31),
152,
"해리포터 첫 번째 시리즈"
).toPresentation(R.drawable.img)

Choose a reason for hiding this comment

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

Movie가 변경되면 이 테스트 코드 크래시가 나겠네요 :)
Movie class와는 거의 상관 없는 이 테스트가 문제가 생기면 꽤나 당황스러울 게 예상되네요 🙂

테스트에는 정말 필요한 의존 관계만 갖게 작성해보세요 :)

Comment on lines +54 to +59
private fun startActivity(movie: MovieModel) {
val intent = Intent(ApplicationProvider.getApplicationContext(), MovieDetailActivity::class.java)
intent.putExtra(MovieDetailActivity.MOVIE_KEY, movieModel)
activityScenario = ActivityScenario.launch(intent)
}
}

Choose a reason for hiding this comment

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

JUnit4에서 @Before는 파라미터를 받을 수 없고, 안드로이드에서는 JUnit4를 사용해야하는데 다른 방법이 있을까요?!

Before를 통해 클래스 필드를 공통적으로 선언해주고,
특정 테스트 메서드에서만 해당 필드를 필요한 데이터로 변경해줄 수 있겠지요 :)

Comment on lines +42 to +43
private fun onClick(title: String, dateTime: LocalDateTime, count: Int) {
val intent = Intent(this, SeatSelectActivity::class.java)

Choose a reason for hiding this comment

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

클릭되었을 때는 알겠는데, 메서드가 무슨 일을 하는 지는 구현을 전부 보아야 파악할 수 있겠네요 :)
메서드 자체가 무슨 일을 하는 지 이름을 지어주는 것은 어떨까요?

Comment on lines +21 to +22
private val onClick: (String, LocalDateTime, Int) -> Unit,
) {

Choose a reason for hiding this comment

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

무엇이 클릭 되었을 때인지도 나타내주면 좋겠네요 :)
on"What"Click

Comment on lines +12 to +13

sealed class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) {

Choose a reason for hiding this comment

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

ViewHolder에 sealed class를 활용하셨네요 👍
CustomViewHolder란 이름은 좀 더 고민해보면 좋겠네요 :)

Comment on lines +3 to +4
enum class ListViewType {
AD_VIEWTYPE, NORMAL_VIEWTYPE;

Choose a reason for hiding this comment

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

이 View Type 정보는 어딘가로 응집할 수 있어 보이네요 :)

Comment on lines +9 to +11
class MovieListAdapter(
private val adInterval: Int,
private val movies: List<MovieModel>,

Choose a reason for hiding this comment

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

다른 n개 종류의 ViewHolder가 표시되고, 이 View 또한 고유의 interval이 생긴다면 어떨까요?

이러한 데이터 종류를 다형성으로 묶어서 한 번에 List로 넘겨보는 방법에 대해서 고민해보세요 :)

@malibinYun malibinYun merged commit 2fb8cb9 into woowacourse:hyemdooly Apr 24, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants