Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
60c862a
feat(home): HomeHeader 컴포저블 추가
etama123 Dec 19, 2025
3af132e
feat(Home): HomeFestivalInfo 컴포저블 추가
etama123 Dec 19, 2025
bb90581
feat(home): HomeLineupHeader 컴포저블 추가
etama123 Dec 19, 2025
19e9842
feat(home): HomePosterList 컴포저블 구현 및 무한 스크롤 페이저 적용
etama123 Dec 19, 2025
c73796e
feat(home): 홈 화면 아티스트 및 라인업 아이템 컴포저블 구현
etama123 Dec 22, 2025
9ef8418
feat(home): 홈 화면 Compose 전환 및 관련 로직 리팩토링
etama123 Dec 26, 2025
920ea5c
refactor(home): 홈 화면 컴포저블 및 코드 정리
etama123 Dec 27, 2025
0ce44cf
refactor(Home): 홈 화면 UI 컴포즈 컴포넌트 리팩토링 및 스타일 개선
etama123 Dec 28, 2025
e0c981e
refactor(Home): LineupUiState 데이터 구조 변경
etama123 Dec 28, 2025
a9d84b4
refactor(HomeLineupItem): 날짜 영역 레이아웃 및 디바이더 구현 방식 변경
etama123 Dec 28, 2025
f849f6f
refactor(Home): 홈 화면 문자열 리소스 정리 및 접근성 개선
etama123 Dec 28, 2025
1e98ebb
refactor(HomeViewModel): `@Inject` 어노테이션 위치 수정
etama123 Jan 2, 2026
9427a8f
refactor(MainActivity): BottomNavigationView의 중복 선택 방지 및 flow 수집 방식 변경
etama123 Jan 2, 2026
c8325fe
refactor(HomeFestivalInfo): Column 내부 padding 적용 및 개별 Text padding 제거
etama123 Jan 2, 2026
dba3acb
build: Landscapist 라이브러리 의존성 추가
etama123 Jan 7, 2026
db92686
feat(common): FestabookImage 공통 컴포저블 구현
etama123 Jan 7, 2026
aec51d3
feat(home): 홈 화면 UI 상태 관리 로직 개선 및 `FestabookImage` 적용
etama123 Jan 7, 2026
ff3745a
Merge branch 'develop' into feat/11
etama123 Jan 7, 2026
14522f3
style(MainActivity, HomeFragment, etc): 코드 포맷팅 및 스타일 가이드 적용
etama123 Jan 7, 2026
88cbab0
style(HomeArtistItem, HomeLineupItem, HomePosterList, FestabookImage)…
etama123 Jan 7, 2026
85477d6
refactor(Home): 디자인 시스템 적용 및 Fragment 주입 방식 리팩토링
etama123 Jan 7, 2026
69775f6
test(HomeViewModelTest): LiveData를 StateFlow로 전환함에 따른 테스트 코드 리팩토링
etama123 Jan 7, 2026
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
175 changes: 20 additions & 155 deletions app/src/main/java/com/daedan/festabook/presentation/home/HomeFragment.kt
Original file line number Diff line number Diff line change
@@ -1,184 +1,49 @@
package com.daedan.festabook.presentation.home

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.fragment.app.Fragment
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import com.daedan.festabook.R
import com.daedan.festabook.databinding.FragmentHomeBinding
import com.daedan.festabook.di.fragment.FragmentKey
import com.daedan.festabook.logging.logger
import com.daedan.festabook.logging.model.home.ExploreClickLogData
import com.daedan.festabook.logging.model.home.HomeViewLogData
import com.daedan.festabook.logging.model.home.ScheduleClickLogData
import com.daedan.festabook.presentation.common.BaseFragment
import com.daedan.festabook.presentation.common.formatFestivalPeriod
import com.daedan.festabook.presentation.common.showErrorSnackBar
import com.daedan.festabook.presentation.explore.ExploreActivity
import com.daedan.festabook.presentation.home.adapter.CenterItemMotionEnlarger
import com.daedan.festabook.presentation.home.adapter.FestivalUiState
import com.daedan.festabook.presentation.home.adapter.LineUpItemOfDayAdapter
import com.daedan.festabook.presentation.home.adapter.PosterAdapter
import com.daedan.festabook.presentation.home.component.HomeScreen
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import timber.log.Timber

@ContributesIntoMap(scope = AppScope::class, binding = binding<Fragment>())
@ContributesIntoMap(scope = AppScope::class, binding = binding<androidx.fragment.app.Fragment>())
Copy link
Contributor

Choose a reason for hiding this comment

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

임포트를 안하고 사용하신 이유가 있으실까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😅 수정했습니다

@FragmentKey(HomeFragment::class)
class HomeFragment @Inject constructor(
private val centerItemMotionEnlarger: RecyclerView.OnScrollListener,
) : BaseFragment<FragmentHomeBinding>() {
class HomeFragment @Inject constructor() : BaseFragment<FragmentHomeBinding>() {
override val layoutId: Int = R.layout.fragment_home

@Inject
override lateinit var defaultViewModelProviderFactory: ViewModelProvider.Factory
private val viewModel: HomeViewModel by viewModels({ requireActivity() })

private val posterAdapter: PosterAdapter by lazy {
PosterAdapter()
}

private val lineupOfDayAdapter: LineUpItemOfDayAdapter by lazy {
LineUpItemOfDayAdapter()
}

override fun onViewCreated(
view: View,
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
setupObservers()
setupAdapters()
setupNavigateToScheduleButton()
setupNavigateToExploreButton()
}

private fun setupNavigateToExploreButton() {
binding.layoutTitleWithIcon.setOnClickListener {
binding.logger.log(ExploreClickLogData(binding.logger.getBaseLogData()))

startActivity(ExploreActivity.newIntent(requireContext()))
}
}

private fun setupNavigateToScheduleButton() {
binding.btnNavigateToSchedule.setOnClickListener {
binding.logger.log(
ScheduleClickLogData(
baseLogData = binding.logger.getBaseLogData(),
),
)

viewModel.navigateToScheduleClick()
}
}

private fun setupObservers() {
viewModel.festivalUiState.observe(viewLifecycleOwner) { festivalUiState ->
when (festivalUiState) {
is FestivalUiState.Loading -> {}
is FestivalUiState.Success -> handleSuccessState(festivalUiState)
is FestivalUiState.Error -> {
showErrorSnackBar(festivalUiState.throwable)
Timber.w(
festivalUiState.throwable,
"HomeFragment: ${festivalUiState.throwable.message}",
)
}
}
}
viewModel.lineupUiState.observe(viewLifecycleOwner) { lineupUiState ->
when (lineupUiState) {
is LineupUiState.Loading -> {}
is LineupUiState.Success -> {
lineupOfDayAdapter.submitList(lineupUiState.lineups.getLineupItems())
}

is LineupUiState.Error -> {
showErrorSnackBar(lineupUiState.throwable)
Timber.w(
lineupUiState.throwable,
"HomeFragment: ${lineupUiState.throwable.message}",
)
}
}
}
}

private fun setupAdapters() {
binding.rvHomePoster.adapter = posterAdapter
binding.rvHomeLineup.adapter = lineupOfDayAdapter
attachSnapHelper()
addScrollEffectListener()
}

private fun handleSuccessState(festivalUiState: FestivalUiState.Success) {
binding.tvHomeOrganizationTitle.text =
festivalUiState.organization.universityName
binding.tvHomeFestivalTitle.text =
festivalUiState.organization.festival.festivalName
binding.tvHomeFestivalDate.text =
formatFestivalPeriod(
festivalUiState.organization.festival.startDate,
festivalUiState.organization.festival.endDate,
)

val posterUrls =
festivalUiState.organization.festival.festivalImages
.sortedBy { it.sequence }
.map { it.imageUrl }

if (posterUrls.isNotEmpty()) {
posterAdapter.submitList(posterUrls) {
scrollToInitialPosition(posterUrls.size)
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
HomeScreen(
viewModel = viewModel,
onNavigateToExplore = {
startActivity(ExploreActivity.newIntent(requireContext()))
},
)
}
}
binding.logger.log(
HomeViewLogData(
baseLogData = binding.logger.getBaseLogData(),
universityName = festivalUiState.organization.universityName,
festivalId = festivalUiState.organization.id,
),
)
}

private fun attachSnapHelper() {
PagerSnapHelper().attachToRecyclerView(binding.rvHomePoster)
}

private fun scrollToInitialPosition(size: Int) {
val safeMaxValue = Int.MAX_VALUE / INFINITE_SCROLL_SAFETY_FACTOR
val initialPosition = safeMaxValue - (safeMaxValue % size)

val layoutManager = binding.rvHomePoster.layoutManager as? LinearLayoutManager ?: return

val itemWidth = resources.getDimensionPixelSize(R.dimen.poster_item_width)
val offset = (binding.rvHomePoster.width / 2) - (itemWidth / 2)

layoutManager.scrollToPositionWithOffset(initialPosition, offset)

binding.rvHomePoster.post {
(centerItemMotionEnlarger as CenterItemMotionEnlarger).expandCenterItem(binding.rvHomePoster)
}
}

private fun addScrollEffectListener() {
binding.rvHomePoster.addOnScrollListener(centerItemMotionEnlarger)
}

override fun onDestroyView() {
binding.rvHomePoster.clearOnScrollListeners()
super.onDestroyView()
}

companion object {
private const val INFINITE_SCROLL_SAFETY_FACTOR = 4
}
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
package com.daedan.festabook.presentation.home

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.daedan.festabook.di.viewmodel.ViewModelKey
import com.daedan.festabook.di.viewmodel.ViewModelScope
import com.daedan.festabook.domain.repository.FestivalRepository
import com.daedan.festabook.presentation.common.SingleLiveData
import com.daedan.festabook.presentation.home.adapter.FestivalUiState
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

@ContributesIntoMap(AppScope::class)
@ViewModelKey(HomeViewModel::class)
class HomeViewModel @Inject constructor(
private val festivalRepository: FestivalRepository,
Copy link
Contributor

Choose a reason for hiding this comment

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

클래스에 @Inject를 붙이는걸로 변경하는게 좋을 것 같아용

Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분 아직 반영이 안되어있네요~

) : ViewModel() {
private val _festivalUiState = MutableLiveData<FestivalUiState>()
val festivalUiState: LiveData<FestivalUiState> get() = _festivalUiState
private val _festivalUiState = MutableStateFlow<FestivalUiState>(FestivalUiState.Loading)
val festivalUiState: StateFlow<FestivalUiState> = _festivalUiState.asStateFlow()

private val _lineupUiState = MutableLiveData<LineupUiState>()
val lineupUiState: LiveData<LineupUiState> get() = _lineupUiState
private val _lineupUiState = MutableStateFlow<LineupUiState>(LineupUiState.Loading)
val lineupUiState: StateFlow<LineupUiState> = _lineupUiState.asStateFlow()

private val _navigateToScheduleEvent: SingleLiveData<Unit> = SingleLiveData()
val navigateToScheduleEvent: LiveData<Unit> get() = _navigateToScheduleEvent
private val _navigateToScheduleEvent =
MutableSharedFlow<Unit>(replay = 0, extraBufferCapacity = 1)
val navigateToScheduleEvent: SharedFlow<Unit> = _navigateToScheduleEvent.asSharedFlow()

init {
loadFestival()
Expand All @@ -48,7 +51,7 @@ class HomeViewModel @Inject constructor(
}

fun navigateToScheduleClick() {
_navigateToScheduleEvent.setValue(Unit)
_navigateToScheduleEvent.tryEmit(Unit)
}

private fun loadLineup() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.daedan.festabook.presentation.home.component

import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.daedan.festabook.presentation.common.component.CoilImage
import com.daedan.festabook.presentation.theme.FestabookColor
import com.daedan.festabook.presentation.theme.FestabookTypography

@Composable
fun HomeArtistItem(
artistName: String,
artistImageUrl: String,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.width(68.dp),
) {
CoilImage(
url = artistImageUrl,
contentDescription = null,
modifier =
Modifier
.fillMaxWidth()
.aspectRatio(1f)
.clip(HomeArtistItem.ArtistImage)
.border(1.dp, FestabookColor.gray300, HomeArtistItem.ArtistImage),
)

Spacer(modifier = Modifier.height(4.dp))

Text(
text = artistName,
style = FestabookTypography.labelLarge,
color = FestabookColor.gray700,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}

object HomeArtistItem {
val ArtistImage = RoundedCornerShape(
topStartPercent = 50,
topEndPercent = 50,
Copy link
Contributor

Choose a reason for hiding this comment

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

private 을 다는건 어떻게 생각하시나요?

bottomEndPercent = 50,
bottomStartPercent = 5,
)
}

@Preview
@Composable
private fun HomeArtistItemPreview() {
HomeArtistItem(
artistName = "실리카겔",
Comment on lines +66 to +68
Copy link
Contributor

Choose a reason for hiding this comment

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

이건 TMI일 수도 있는데, Theme으로 감싸주지 않고 Preview를 실행하는건 실제 사용할 때와 다르게 동작할 수 있어 주의해야 하는 것으로 알고있어요 !

artistImageUrl = "sample",
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.daedan.festabook.presentation.home.component

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.daedan.festabook.presentation.theme.FestabookColor
import com.daedan.festabook.presentation.theme.FestabookTypography

@Composable
fun HomeFestivalInfo(
festivalName: String,
festivalDate: String,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxWidth(),
) {
Text(
text = festivalName,
style = FestabookTypography.displayMedium,
color = FestabookColor.black,
modifier = Modifier.padding(horizontal = 20.dp),
)

Spacer(modifier = Modifier.height(8.dp))

Text(
text = festivalDate,
style = FestabookTypography.bodyLarge,
color = FestabookColor.gray500,
modifier = Modifier.padding(horizontal = 20.dp),
)
}
}
Comment on lines 22 to 39
Copy link
Contributor

Choose a reason for hiding this comment

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

Text에서 padding을 주지 않고 Column에서 한 번에 주는건 어떄요?

Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분도 아직 반영이 안되어 있어요


@Preview(showBackground = true)
@Composable
private fun HomeFestivalInfoPreview() {
HomeFestivalInfo(
festivalName = "2025 가천 Water Festival\n: AQUA WAVE",
festivalDate = "2025년 10월 15일 - 10월 17일",
)
}
Loading