From 989fed4682e5e99ab40d18c6b78901d3be6e1fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Tue, 25 Nov 2025 17:29:52 +0900 Subject: [PATCH 01/23] =?UTF-8?q?feat:=20=EC=86=8C=EC=8B=9D=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20StateFlow=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/news/NewsViewModel.kt | 24 +++++++++---------- .../news/notice/NoticeFragment.kt | 19 +++------------ .../news/notice/component/NoticeScreen.kt | 19 +++++++++++++++ 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt index 539da37..9e16d9f 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt @@ -25,6 +25,9 @@ import com.daedan.festabook.presentation.news.notice.model.toUiModel import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metro.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @ContributesIntoMap(AppScope::class) @@ -35,12 +38,9 @@ class NewsViewModel( private val faqRepository: FAQRepository, private val lostItemRepository: LostItemRepository, ) : ViewModel() { - var noticeUiState by mutableStateOf(NoticeUiState.InitialLoading) - private set - - val isNoticeScreenRefreshing by derivedStateOf { - noticeUiState is NoticeUiState.Refreshing - } + private val _noticeUiState: MutableStateFlow = + MutableStateFlow(NoticeUiState.InitialLoading) + val noticeUiState: StateFlow = _noticeUiState.asStateFlow() var faqUiState by mutableStateOf(FAQUiState.InitialLoading) private set @@ -62,7 +62,7 @@ class NewsViewModel( fun loadAllNotices(state: NoticeUiState) { viewModelScope.launch { - noticeUiState = state + _noticeUiState.value = state val result = noticeRepository.fetchNotices() result .onSuccess { notices -> @@ -76,11 +76,11 @@ class NewsViewModel( notices.indexOfFirst { it.id == noticeIdToExpand }.let { if (it == -1) DEFAULT_POSITION else it } - noticeUiState = + _noticeUiState.value = NoticeUiState.Success(updatedNotices, expandPosition) noticeIdToExpand = null }.onFailure { - noticeUiState = NoticeUiState.Error(it) + _noticeUiState.value = NoticeUiState.Error(it) } } } @@ -100,7 +100,7 @@ class NewsViewModel( fun expandNotice(noticeId: Long) { this.noticeIdToExpand = noticeId val notices = - when (val currentState = noticeUiState) { + when (val currentState = _noticeUiState.value) { is NoticeUiState.Refreshing -> currentState.oldNotices is NoticeUiState.Success -> currentState.notices else -> return @@ -166,8 +166,8 @@ class NewsViewModel( } private fun updateNoticeUiState(onUpdate: (List) -> List) { - noticeUiState = - when (val currentState = noticeUiState) { + _noticeUiState.value = + when (val currentState = _noticeUiState.value) { is NoticeUiState.Success -> currentState.copy( notices = onUpdate(currentState.notices), diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/notice/NoticeFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/news/notice/NoticeFragment.kt index ba6e5c4..26c654a 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/notice/NoticeFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/notice/NoticeFragment.kt @@ -14,14 +14,14 @@ import com.daedan.festabook.di.appGraph import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.main.MainViewModel import com.daedan.festabook.presentation.news.NewsViewModel -import com.daedan.festabook.presentation.news.notice.adapter.NewsClickListener -import com.daedan.festabook.presentation.news.notice.component.NoticeScreen +import com.daedan.festabook.presentation.news.notice.component.NoticeScreenContainer class NoticeFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_notice override val defaultViewModelProviderFactory: ViewModelProvider.Factory get() = appGraph.metroViewModelFactory + private val newsViewModel: NewsViewModel by viewModels({ requireParentFragment() }) private val mainViewModel: MainViewModel by viewModels({ requireActivity() }) @@ -33,20 +33,7 @@ class NoticeFragment : BaseFragment() { ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - NoticeScreen( - uiState = newsViewModel.noticeUiState, - onNoticeClick = { notice -> - (requireParentFragment() as NewsClickListener) - .onNoticeClick(notice) - }, - isRefreshing = newsViewModel.isNoticeScreenRefreshing, - onRefresh = { - val currentUiState = newsViewModel.noticeUiState - val oldNotices = - if (currentUiState is NoticeUiState.Success) currentUiState.notices else emptyList() - newsViewModel.loadAllNotices(NoticeUiState.Refreshing(oldNotices)) - }, - ) + NoticeScreenContainer(newsViewModel = newsViewModel) } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt index 4edb88b..ffdc28d 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt @@ -15,11 +15,13 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.presentation.common.component.EmptyStateScreen import com.daedan.festabook.presentation.common.component.LoadingStateScreen import com.daedan.festabook.presentation.common.component.PULL_OFFSET_LIMIT import com.daedan.festabook.presentation.common.component.PullToRefreshContainer +import com.daedan.festabook.presentation.news.NewsViewModel import com.daedan.festabook.presentation.news.component.NewsItem import com.daedan.festabook.presentation.news.notice.NoticeUiState import com.daedan.festabook.presentation.news.notice.NoticeUiState.Companion.DEFAULT_POSITION @@ -28,6 +30,23 @@ import timber.log.Timber private const val PADDING: Int = 8 +@Composable +fun NoticeScreenContainer(newsViewModel: NewsViewModel) { + val uiState = newsViewModel.noticeUiState.collectAsStateWithLifecycle() + val isRefreshing = uiState.value is NoticeUiState.Refreshing + + NoticeScreen( + uiState = uiState.value, + onNoticeClick = { notice -> newsViewModel.toggleNoticeExpanded(notice) }, + isRefreshing = isRefreshing, + onRefresh = { + val oldNotices = + (uiState.value as? NoticeUiState.Success)?.notices ?: emptyList() + newsViewModel.loadAllNotices(NoticeUiState.Refreshing(oldNotices)) + }, + ) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun NoticeScreen( From 1ff88a5b56ea484edc50aad668f9c495dcb2c34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Tue, 25 Nov 2025 17:40:25 +0900 Subject: [PATCH 02/23] =?UTF-8?q?feat:=20FAQ=ED=99=94=EB=A9=B4=20StateFlow?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/news/NewsViewModel.kt | 18 +++++++++++------- .../presentation/news/faq/FAQFragment.kt | 7 ++----- .../news/faq/component/FAQScreen.kt | 9 +++++++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt index 9e16d9f..a3cdd4f 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt @@ -42,8 +42,12 @@ class NewsViewModel( MutableStateFlow(NoticeUiState.InitialLoading) val noticeUiState: StateFlow = _noticeUiState.asStateFlow() - var faqUiState by mutableStateOf(FAQUiState.InitialLoading) - private set + private val _faqUiState: MutableStateFlow = + MutableStateFlow(FAQUiState.InitialLoading) + val faqUiState: StateFlow = _faqUiState.asStateFlow() + +// var faqUiState by mutableStateOf(FAQUiState.InitialLoading) +// private set var lostUiState by mutableStateOf(LostUiState.InitialLoading) private set @@ -152,15 +156,15 @@ class NewsViewModel( private fun loadAllFAQs(state: FAQUiState = FAQUiState.InitialLoading) { viewModelScope.launch { - faqUiState = state + _faqUiState.value = state val result = faqRepository.getAllFAQ() result .onSuccess { faqItems -> - faqUiState = FAQUiState.Success(faqItems.map { it.toUiModel() }) + _faqUiState.value = FAQUiState.Success(faqItems.map { it.toUiModel() }) }.onFailure { - faqUiState = FAQUiState.Error(it) + _faqUiState.value = FAQUiState.Error(it) } } } @@ -178,8 +182,8 @@ class NewsViewModel( } private fun updateFAQUiState(onUpdate: (List) -> List) { - val currentState = faqUiState - faqUiState = + val currentState = _faqUiState.value + _faqUiState.value = when (currentState) { is FAQUiState.Success -> currentState.copy(faqs = onUpdate(currentState.faqs)) else -> currentState diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/faq/FAQFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/news/faq/FAQFragment.kt index 353e48f..2a4ed93 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/faq/FAQFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/faq/FAQFragment.kt @@ -13,8 +13,7 @@ import com.daedan.festabook.databinding.FragmentFaqBinding import com.daedan.festabook.di.appGraph import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.news.NewsViewModel -import com.daedan.festabook.presentation.news.faq.component.FAQScreen -import com.daedan.festabook.presentation.news.notice.adapter.NewsClickListener +import com.daedan.festabook.presentation.news.faq.component.FAQScreenContainer class FAQFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_faq @@ -31,9 +30,7 @@ class FAQFragment : BaseFragment() { ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - FAQScreen(uiState = viewModel.faqUiState, onFaqClick = { faqItemUiModel -> - (requireParentFragment() as NewsClickListener).onFAQClick(faqItemUiModel) - }) + FAQScreenContainer(newsViewModel = viewModel) } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt index 89482d2..395a1ea 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt @@ -10,8 +10,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.presentation.common.component.EmptyStateScreen +import com.daedan.festabook.presentation.news.NewsViewModel import com.daedan.festabook.presentation.news.component.NewsItem import com.daedan.festabook.presentation.news.faq.FAQUiState import com.daedan.festabook.presentation.news.faq.model.FAQItemUiModel @@ -19,6 +21,13 @@ import timber.log.Timber private const val PADDING: Int = 8 +@Composable +fun FAQScreenContainer(newsViewModel: NewsViewModel) { + val uiState = newsViewModel.faqUiState.collectAsStateWithLifecycle() + + FAQScreen(uiState = uiState.value, onFaqClick = { faq -> newsViewModel.toggleFAQExpanded(faq) }) +} + @Composable fun FAQScreen( uiState: FAQUiState, From 8a5b801eff11f73a4a4f05b909b9346bd8ce16bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Tue, 25 Nov 2025 18:05:28 +0900 Subject: [PATCH 03/23] =?UTF-8?q?feat:=20=EB=B6=84=EC=8B=A4=EB=AC=BC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20StateFlow=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/news/NewsViewModel.kt | 24 ++++++------------- .../news/lost/LostItemFragment.kt | 16 ++----------- .../news/lost/component/LostItemScreen.kt | 18 ++++++++++++++ 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt index a3cdd4f..c995cb9 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt @@ -1,9 +1,5 @@ package com.daedan.festabook.presentation.news -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.daedan.festabook.di.viewmodel.ViewModelKey @@ -46,15 +42,9 @@ class NewsViewModel( MutableStateFlow(FAQUiState.InitialLoading) val faqUiState: StateFlow = _faqUiState.asStateFlow() -// var faqUiState by mutableStateOf(FAQUiState.InitialLoading) -// private set - - var lostUiState by mutableStateOf(LostUiState.InitialLoading) - private set - - val isLostItemScreenRefreshing by derivedStateOf { - lostUiState is LostUiState.Refreshing - } + private val _lostUiState: MutableStateFlow = + MutableStateFlow(LostUiState.InitialLoading) + val lostUiState: StateFlow = _lostUiState.asStateFlow() private var noticeIdToExpand: Long? = null @@ -139,7 +129,7 @@ class NewsViewModel( fun loadAllLostItems(state: LostUiState) { viewModelScope.launch { - lostUiState = state + _lostUiState.value = state val result = lostItemRepository.getLost() val lostUiModels = @@ -150,7 +140,7 @@ class NewsViewModel( null -> LostUiModel.Guide() } } - lostUiState = LostUiState.Success(lostUiModels) + _lostUiState.value = LostUiState.Success(lostUiModels) } } @@ -191,8 +181,8 @@ class NewsViewModel( } private fun updateLostUiState(onUpdate: (List) -> List) { - val currentState = lostUiState - lostUiState = + val currentState = _lostUiState.value + _lostUiState.value = when (currentState) { is LostUiState.Success -> currentState.copy(lostItems = onUpdate(currentState.lostItems)) else -> currentState diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/lost/LostItemFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/news/lost/LostItemFragment.kt index 305c10d..37861c3 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/lost/LostItemFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/lost/LostItemFragment.kt @@ -13,8 +13,7 @@ import com.daedan.festabook.databinding.FragmentLostItemBinding import com.daedan.festabook.di.appGraph import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.news.NewsViewModel -import com.daedan.festabook.presentation.news.lost.component.LostItemScreen -import com.daedan.festabook.presentation.news.notice.adapter.NewsClickListener +import com.daedan.festabook.presentation.news.lost.component.LostItemScreenContainer class LostItemFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_lost_item @@ -32,18 +31,7 @@ class LostItemFragment : BaseFragment() { ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - val newsClickListener = requireParentFragment() as NewsClickListener - LostItemScreen( - lostUiState = viewModel.lostUiState, - onLostGuideClick = { newsClickListener.onLostGuideItemClick() }, - isRefreshing = viewModel.isLostItemScreenRefreshing, - onRefresh = { - val currentUiState = viewModel.lostUiState - val oldLostItems = - if (currentUiState is LostUiState.Success) currentUiState.lostItems else emptyList() - viewModel.loadAllLostItems(LostUiState.Refreshing(oldLostItems)) - }, - ) + LostItemScreenContainer(newsViewModel = viewModel) } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt index f140d80..5f09e67 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt @@ -21,11 +21,13 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.presentation.common.component.EmptyStateScreen import com.daedan.festabook.presentation.common.component.LoadingStateScreen import com.daedan.festabook.presentation.common.component.PULL_OFFSET_LIMIT import com.daedan.festabook.presentation.common.component.PullToRefreshContainer +import com.daedan.festabook.presentation.news.NewsViewModel import com.daedan.festabook.presentation.news.component.NewsItem import com.daedan.festabook.presentation.news.lost.LostUiState import com.daedan.festabook.presentation.news.lost.model.LostItemUiStatus @@ -35,6 +37,22 @@ import timber.log.Timber private const val SPAN_COUNT: Int = 2 private const val PADDING: Int = 8 +@Composable +fun LostItemScreenContainer(newsViewModel: NewsViewModel) { + val uiState = newsViewModel.lostUiState.collectAsStateWithLifecycle() + val isRefreshing = uiState.value is LostUiState.Refreshing + + LostItemScreen( + lostUiState = uiState.value, + onLostGuideClick = { newsViewModel.toggleLostGuideExpanded() }, + isRefreshing = isRefreshing, + onRefresh = { + val oldLostItems = (uiState.value as? LostUiState.Success)?.lostItems ?: emptyList() + newsViewModel.loadAllLostItems(LostUiState.Refreshing(oldLostItems)) + }, + ) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun LostItemScreen( From 94124c6dd5301f44f5109aca8786f29476e78bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Tue, 25 Nov 2025 19:17:02 +0900 Subject: [PATCH 04/23] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=EB=AA=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/daedan/festabook/presentation/news/NewsFragment.kt | 7 +++---- .../daedan/festabook/presentation/news/NewsViewModel.kt | 6 +++--- .../festabook/presentation/news/faq/component/FAQScreen.kt | 2 +- .../presentation/news/lost/component/LostItemScreen.kt | 2 +- .../presentation/news/notice/component/NoticeScreen.kt | 2 +- .../java/com/daedan/festabook/news/NewsViewModelTest.kt | 6 +++--- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt index 5286660..32a6be1 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt @@ -12,7 +12,6 @@ import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.main.MainViewModel import com.daedan.festabook.presentation.news.adapter.NewsPagerAdapter import com.daedan.festabook.presentation.news.faq.model.FAQItemUiModel -import com.daedan.festabook.presentation.news.lost.model.LostUiModel import com.daedan.festabook.presentation.news.notice.adapter.NewsClickListener import com.daedan.festabook.presentation.news.notice.model.NoticeUiModel import com.google.android.material.tabs.TabLayoutMediator @@ -54,15 +53,15 @@ class NewsFragment : } override fun onNoticeClick(notice: NoticeUiModel) { - newsViewModel.toggleNoticeExpanded(notice) + newsViewModel.toggleNotice(notice) } override fun onFAQClick(faqItem: FAQItemUiModel) { - newsViewModel.toggleFAQExpanded(faqItem) + newsViewModel.toggleFAQ(faqItem) } override fun onLostGuideItemClick() { - newsViewModel.toggleLostGuideExpanded() + newsViewModel.toggleLostGuide() } private fun setupNewsTabLayout() { diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt index c995cb9..17b7e97 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt @@ -79,7 +79,7 @@ class NewsViewModel( } } - fun toggleNoticeExpanded(notice: NoticeUiModel) { + fun toggleNotice(notice: NoticeUiModel) { updateNoticeUiState { notices -> notices.map { updatedNotice -> if (notice.id == updatedNotice.id) { @@ -103,7 +103,7 @@ class NewsViewModel( loadAllNotices(NoticeUiState.Refreshing(notices)) } - fun toggleFAQExpanded(faqItem: FAQItemUiModel) { + fun toggleFAQ(faqItem: FAQItemUiModel) { updateFAQUiState { faqItems -> faqItems.map { updatedFAQItem -> if (faqItem.questionId == updatedFAQItem.questionId) { @@ -115,7 +115,7 @@ class NewsViewModel( } } - fun toggleLostGuideExpanded() { + fun toggleLostGuide() { updateLostUiState { lostUiModels -> lostUiModels.map { lostUiModel -> if (lostUiModel is LostUiModel.Guide) { diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt index 395a1ea..4b4a1cd 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt @@ -25,7 +25,7 @@ private const val PADDING: Int = 8 fun FAQScreenContainer(newsViewModel: NewsViewModel) { val uiState = newsViewModel.faqUiState.collectAsStateWithLifecycle() - FAQScreen(uiState = uiState.value, onFaqClick = { faq -> newsViewModel.toggleFAQExpanded(faq) }) + FAQScreen(uiState = uiState.value, onFaqClick = { faq -> newsViewModel.toggleFAQ(faq) }) } @Composable diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt index 5f09e67..34777c5 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt @@ -44,7 +44,7 @@ fun LostItemScreenContainer(newsViewModel: NewsViewModel) { LostItemScreen( lostUiState = uiState.value, - onLostGuideClick = { newsViewModel.toggleLostGuideExpanded() }, + onLostGuideClick = { newsViewModel.toggleLostGuide() }, isRefreshing = isRefreshing, onRefresh = { val oldLostItems = (uiState.value as? LostUiState.Success)?.lostItems ?: emptyList() diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt index ffdc28d..77f924a 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt @@ -37,7 +37,7 @@ fun NoticeScreenContainer(newsViewModel: NewsViewModel) { NoticeScreen( uiState = uiState.value, - onNoticeClick = { notice -> newsViewModel.toggleNoticeExpanded(notice) }, + onNoticeClick = { notice -> newsViewModel.toggleNotice(notice) }, isRefreshing = isRefreshing, onRefresh = { val oldNotices = diff --git a/app/src/test/java/com/daedan/festabook/news/NewsViewModelTest.kt b/app/src/test/java/com/daedan/festabook/news/NewsViewModelTest.kt index af35b53..ff45996 100644 --- a/app/src/test/java/com/daedan/festabook/news/NewsViewModelTest.kt +++ b/app/src/test/java/com/daedan/festabook/news/NewsViewModelTest.kt @@ -168,7 +168,7 @@ class NewsViewModelTest { val notice = FAKE_NOTICES.first().toUiModel() // when - newsViewModel.toggleNoticeExpanded(notice) + newsViewModel.toggleNotice(notice) advanceUntilIdle() // then @@ -194,7 +194,7 @@ class NewsViewModelTest { val faq = FAKE_FAQS.first().toUiModel() // when - newsViewModel.toggleFAQExpanded(faq) + newsViewModel.toggleFAQ(faq) advanceUntilIdle() // then @@ -253,7 +253,7 @@ class NewsViewModelTest { ) // when - newsViewModel.toggleLostGuideExpanded() + newsViewModel.toggleLostGuide() // then val actual = newsViewModel.lostUiState.getOrAwaitValue() From 06f9348e0632beb268c26c7a4f246781444eebba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Tue, 25 Nov 2025 22:29:19 +0900 Subject: [PATCH 05/23] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=ED=8D=BC?= =?UTF-8?q?=ED=8B=B0=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/daedan/festabook/di/FestaBookAppGraph.kt | 3 +-- .../com/daedan/festabook/presentation/news/NewsFragment.kt | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/di/FestaBookAppGraph.kt b/app/src/main/java/com/daedan/festabook/di/FestaBookAppGraph.kt index 80fc673..cd93ed6 100644 --- a/app/src/main/java/com/daedan/festabook/di/FestaBookAppGraph.kt +++ b/app/src/main/java/com/daedan/festabook/di/FestaBookAppGraph.kt @@ -38,8 +38,7 @@ interface FestaBookAppGraph { // splashActivity @Provides - fun provideAppUpdateManager(application: Application): AppUpdateManager = - AppUpdateManagerFactory.create(application) + fun provideAppUpdateManager(application: Application): AppUpdateManager = AppUpdateManagerFactory.create(application) // logger val defaultFirebaseLogger: DefaultFirebaseLogger diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt index 32a6be1..c803edf 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt @@ -29,11 +29,11 @@ import dev.zacsweers.metro.binding class NewsFragment : BaseFragment(), NewsClickListener { + override val layoutId: Int = R.layout.fragment_news + @Inject override lateinit var defaultViewModelProviderFactory: ViewModelProvider.Factory - override val layoutId: Int = R.layout.fragment_news - private val newsPagerAdapter by lazy { NewsPagerAdapter(this) } From d70bf78776ce3e6b957b2beca202c9be4ee372aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Wed, 26 Nov 2025 16:46:52 +0900 Subject: [PATCH 06/23] =?UTF-8?q?feat:=20Festabook=20Compose=20Shape=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/theme/FestabookShapes.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/theme/FestabookShapes.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookShapes.kt b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookShapes.kt new file mode 100644 index 0000000..819b6ed --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookShapes.kt @@ -0,0 +1,26 @@ +package com.daedan.festabook.presentation.theme + +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +data class FestabookShapes( + val radius1: CornerBasedShape = RoundedCornerShape(6.dp), + val radius2: CornerBasedShape = RoundedCornerShape(10.dp), + val radius3: CornerBasedShape = RoundedCornerShape(16.dp), + val radius4: CornerBasedShape = RoundedCornerShape(20.dp), + val radius5: CornerBasedShape = RoundedCornerShape(24.dp), + val radiusFull: CornerBasedShape = RoundedCornerShape(999.dp), +) + +val festabookShapes = FestabookShapes() + +val FestabookShapesTheme = + Shapes( + extraSmall = festabookShapes.radius1, + small = festabookShapes.radius2, + medium = festabookShapes.radius3, + large = festabookShapes.radius4, + extraLarge = festabookShapes.radius5, + ) From c2664cc487ea8d1fafedf0ee7c9a054ad322e48e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Wed, 26 Nov 2025 20:05:21 +0900 Subject: [PATCH 07/23] =?UTF-8?q?refactor:=20newsClickListener=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../festabook/presentation/news/faq/adapter/FAQViewHolder.kt | 1 - .../presentation/news/lost/adapter/LostGuideItemViewHolder.kt | 1 - .../presentation/news/notice/adapter/NoticeViewHolder.kt | 1 - 3 files changed, 3 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/faq/adapter/FAQViewHolder.kt b/app/src/main/java/com/daedan/festabook/presentation/news/faq/adapter/FAQViewHolder.kt index 2114bad..7bdefd5 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/faq/adapter/FAQViewHolder.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/faq/adapter/FAQViewHolder.kt @@ -21,7 +21,6 @@ class FAQViewHolder( init { binding.root.setOnClickListener { faqItem?.let { - newsClickListener.onFAQClick(it) } ?: run { Timber.w("${this::class.java.simpleName} : FAQ 아이템이 null입니다.") } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/lost/adapter/LostGuideItemViewHolder.kt b/app/src/main/java/com/daedan/festabook/presentation/news/lost/adapter/LostGuideItemViewHolder.kt index 741eeaa..d96e756 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/lost/adapter/LostGuideItemViewHolder.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/lost/adapter/LostGuideItemViewHolder.kt @@ -19,7 +19,6 @@ class LostGuideItemViewHolder private constructor( init { binding.root.setOnClickListener { lostGuideItem?.let { - newsClickListener.onLostGuideItemClick() } } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/notice/adapter/NoticeViewHolder.kt b/app/src/main/java/com/daedan/festabook/presentation/news/notice/adapter/NoticeViewHolder.kt index 472fa80..cb31dd2 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/notice/adapter/NoticeViewHolder.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/notice/adapter/NoticeViewHolder.kt @@ -21,7 +21,6 @@ class NoticeViewHolder( init { binding.layoutNoticeItem.setOnClickListener { noticeItem?.let { - newsClickListener.onNoticeClick(it) } ?: run { Timber.w("${this::class.java.simpleName} 공지 아이템이 null입니다.") } From 94c1993e500d7cba3f09fdcbda9f0ce9e821e840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Wed, 26 Nov 2025 20:22:19 +0900 Subject: [PATCH 08/23] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/common/component/Header.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt b/app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt new file mode 100644 index 0000000..2dfc1f7 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt @@ -0,0 +1,22 @@ +package com.daedan.festabook.presentation.common.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import com.daedan.festabook.presentation.theme.FestabookTypography + +@Composable +fun Header( + title: String, + modifier: Modifier = Modifier, + style: TextStyle = FestabookTypography.displayLarge, +) { + Text( + text = title, + style = style, + modifier = modifier.padding(top = 40.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), + ) +} From faa8ed56ff533ef00f0996be9f2aee09e87a4127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Wed, 26 Nov 2025 21:23:17 +0900 Subject: [PATCH 09/23] =?UTF-8?q?feat:=20=EC=86=8C=EC=8B=9D=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20viewPager,=20tabLayout=20Compose=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/news/NewsFragment.kt | 80 ++++++------- .../presentation/news/component/NewsScreen.kt | 107 ++++++++++++++++++ .../news/faq/component/FAQScreen.kt | 11 +- .../news/lost/component/LostItemScreen.kt | 7 +- .../news/notice/component/NoticeScreen.kt | 6 +- .../presentation/theme/FestabookTheme.kt | 20 ++++ 6 files changed, 187 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt create mode 100644 app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt index c803edf..cb0a9a4 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt @@ -1,7 +1,11 @@ package com.daedan.festabook.presentation.news import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider @@ -11,10 +15,8 @@ import com.daedan.festabook.di.fragment.FragmentKey import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.main.MainViewModel import com.daedan.festabook.presentation.news.adapter.NewsPagerAdapter -import com.daedan.festabook.presentation.news.faq.model.FAQItemUiModel -import com.daedan.festabook.presentation.news.notice.adapter.NewsClickListener -import com.daedan.festabook.presentation.news.notice.model.NoticeUiModel -import com.google.android.material.tabs.TabLayoutMediator +import com.daedan.festabook.presentation.news.component.NewsScreen +import com.daedan.festabook.presentation.theme.FestabookTheme import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metro.Inject @@ -26,9 +28,7 @@ import dev.zacsweers.metro.binding ) @FragmentKey(NewsFragment::class) @Inject -class NewsFragment : - BaseFragment(), - NewsClickListener { +class NewsFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_news @Inject @@ -40,39 +40,41 @@ class NewsFragment : private val newsViewModel: NewsViewModel by viewModels() private val mainViewModel: MainViewModel by viewModels({ requireActivity() }) - override fun onViewCreated( - view: View, + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - binding.lifecycleOwner = viewLifecycleOwner - setupNewsTabLayout() - mainViewModel.noticeIdToExpand.observe(viewLifecycleOwner) { - binding.vpNews.currentItem = NOTICE_TAB_INDEX + ): View = + ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + FestabookTheme { + NewsScreen(mainViewModel = mainViewModel, newsViewModel = newsViewModel) + } + } } - } - - override fun onNoticeClick(notice: NoticeUiModel) { - newsViewModel.toggleNotice(notice) - } - - override fun onFAQClick(faqItem: FAQItemUiModel) { - newsViewModel.toggleFAQ(faqItem) - } - override fun onLostGuideItemClick() { - newsViewModel.toggleLostGuide() - } - - private fun setupNewsTabLayout() { - binding.vpNews.adapter = newsPagerAdapter - TabLayoutMediator(binding.tlNews, binding.vpNews) { tab, position -> - val tabNameRes = NewsTab.entries[position].tabNameRes - tab.text = getString(tabNameRes) - }.attach() - } - - companion object { - private const val NOTICE_TAB_INDEX: Int = 0 - } +// override fun onViewCreated( +// view: View, +// savedInstanceState: Bundle?, +// ) { +// super.onViewCreated(view, savedInstanceState) +// binding.lifecycleOwner = viewLifecycleOwner +// setupNewsTabLayout() +// mainViewModel.noticeIdToExpand.observe(viewLifecycleOwner) { +// binding.vpNews.currentItem = NOTICE_TAB_INDEX +// } +// } +// +// private fun setupNewsTabLayout() { +// binding.vpNews.adapter = newsPagerAdapter +// TabLayoutMediator(binding.tlNews, binding.vpNews) { tab, position -> +// val tabNameRes = NewsTab.entries[position].tabNameRes +// tab.text = getString(tabNameRes) +// }.attach() +// } +// +// companion object { +// private const val NOTICE_TAB_INDEX: Int = 0 +// } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt new file mode 100644 index 0000000..1d1dc35 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt @@ -0,0 +1,107 @@ +package com.daedan.festabook.presentation.news.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.daedan.festabook.R +import com.daedan.festabook.presentation.common.component.Header +import com.daedan.festabook.presentation.main.MainViewModel +import com.daedan.festabook.presentation.news.NewsTab +import com.daedan.festabook.presentation.news.NewsViewModel +import com.daedan.festabook.presentation.news.faq.component.FAQScreenContainer +import com.daedan.festabook.presentation.news.lost.component.LostItemScreenContainer +import com.daedan.festabook.presentation.news.notice.component.NoticeScreenContainer +import com.daedan.festabook.presentation.theme.FestabookColor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun NewsScreen( + newsViewModel: NewsViewModel, + mainViewModel: MainViewModel, + modifier: Modifier = Modifier, +) { + val pageState = rememberPagerState { NewsTab.entries.size } + val scope = rememberCoroutineScope() + + Column(modifier = modifier.background(color = MaterialTheme.colorScheme.background)) { + Header(title = stringResource(R.string.news_title)) + NewsTabRow(pageState, scope) + NewsTabPage(pageState, newsViewModel) + } +} + +@Composable +private fun NewsTabRow( + pageState: PagerState, + scope: CoroutineScope, +) { + TabRow( + selectedTabIndex = pageState.currentPage, + containerColor = MaterialTheme.colorScheme.background, + contentColor = FestabookColor.black, + indicator = { tabPositions -> + TabRowDefaults.PrimaryIndicator( + color = FestabookColor.black, + width = tabPositions[pageState.currentPage].width, + modifier = Modifier.tabIndicatorOffset(currentTabPosition = tabPositions[pageState.currentPage]), + ) + }, + ) { + NewsTab.entries.forEachIndexed { index, title -> + Tab( + selected = pageState.currentPage == index, + unselectedContentColor = FestabookColor.gray500, + onClick = { scope.launch { pageState.animateScrollToPage(index) } }, + text = { Text(text = stringResource(title.tabNameRes)) }, + ) + } + } +} + +@Composable +private fun NewsTabPage( + pageState: PagerState, + newsViewModel: NewsViewModel, +) { + HorizontalPager( + state = pageState, + verticalAlignment = Alignment.Top, + ) { index -> + val tab = NewsTab.entries[index] + when (tab) { + NewsTab.NOTICE -> + NoticeScreenContainer( + newsViewModel = newsViewModel, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + NewsTab.FAQ -> + FAQScreenContainer( + newsViewModel = newsViewModel, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + NewsTab.LOST_ITEM -> + LostItemScreenContainer( + newsViewModel = newsViewModel, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt index 4b4a1cd..f0d4060 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt @@ -22,10 +22,17 @@ import timber.log.Timber private const val PADDING: Int = 8 @Composable -fun FAQScreenContainer(newsViewModel: NewsViewModel) { +fun FAQScreenContainer( + newsViewModel: NewsViewModel, + modifier: Modifier = Modifier, +) { val uiState = newsViewModel.faqUiState.collectAsStateWithLifecycle() - FAQScreen(uiState = uiState.value, onFaqClick = { faq -> newsViewModel.toggleFAQ(faq) }) + FAQScreen( + uiState = uiState.value, + onFaqClick = { faq -> newsViewModel.toggleFAQ(faq) }, + modifier = modifier, + ) } @Composable diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt index 34777c5..9a84ffa 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt @@ -38,7 +38,10 @@ private const val SPAN_COUNT: Int = 2 private const val PADDING: Int = 8 @Composable -fun LostItemScreenContainer(newsViewModel: NewsViewModel) { +fun LostItemScreenContainer( + newsViewModel: NewsViewModel, + modifier: Modifier = Modifier, +) { val uiState = newsViewModel.lostUiState.collectAsStateWithLifecycle() val isRefreshing = uiState.value is LostUiState.Refreshing @@ -50,6 +53,7 @@ fun LostItemScreenContainer(newsViewModel: NewsViewModel) { val oldLostItems = (uiState.value as? LostUiState.Success)?.lostItems ?: emptyList() newsViewModel.loadAllLostItems(LostUiState.Refreshing(oldLostItems)) }, + modifier = modifier, ) } @@ -74,7 +78,6 @@ fun LostItemScreen( PullToRefreshContainer( isRefreshing = isRefreshing, onRefresh = onRefresh, - modifier = modifier, ) { pullToRefreshState -> when (lostUiState) { LostUiState.InitialLoading -> LoadingStateScreen() diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt index 77f924a..98fcd9c 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt @@ -31,7 +31,10 @@ import timber.log.Timber private const val PADDING: Int = 8 @Composable -fun NoticeScreenContainer(newsViewModel: NewsViewModel) { +fun NoticeScreenContainer( + newsViewModel: NewsViewModel, + modifier: Modifier = Modifier, +) { val uiState = newsViewModel.noticeUiState.collectAsStateWithLifecycle() val isRefreshing = uiState.value is NoticeUiState.Refreshing @@ -44,6 +47,7 @@ fun NoticeScreenContainer(newsViewModel: NewsViewModel) { (uiState.value as? NoticeUiState.Success)?.notices ?: emptyList() newsViewModel.loadAllNotices(NoticeUiState.Refreshing(oldNotices)) }, + modifier = modifier, ) } diff --git a/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt new file mode 100644 index 0000000..9d1b0e7 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt @@ -0,0 +1,20 @@ +package com.daedan.festabook.presentation.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val LightColorScheme = + lightColorScheme( + background = FestabookColor.white, + ) + +@Composable +fun FestabookTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = LightColorScheme, + shapes = FestabookShapesTheme, + typography = FestabookTypography, + content = content, + ) +} From 86cc75b177ed80db9cc8eb4eae1d6c60036a5fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Wed, 26 Nov 2025 21:28:13 +0900 Subject: [PATCH 10/23] =?UTF-8?q?feat:=20Header=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EC=A0=80=EB=B8=94=20=ED=94=84=EB=A6=AC=EB=B7=B0=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/common/component/Header.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt b/app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt index 2dfc1f7..d322d84 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt @@ -1,10 +1,12 @@ package com.daedan.festabook.presentation.common.component +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.daedan.festabook.presentation.theme.FestabookTypography @@ -17,6 +19,15 @@ fun Header( Text( text = title, style = style, - modifier = modifier.padding(top = 40.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), + modifier = + modifier + .padding(top = 40.dp, bottom = 16.dp, start = 16.dp, end = 16.dp) + .fillMaxWidth(), ) } + +@Composable +@Preview(showBackground = true) +private fun HeaderPreview() { + Header(title = "FestaBook") +} From c452e4190d7e318b22241d8299db82eb9a6781d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Wed, 26 Nov 2025 22:11:35 +0900 Subject: [PATCH 11/23] =?UTF-8?q?refactor:=20=EC=9C=84=EC=9E=84=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/news/faq/component/FAQScreen.kt | 5 +++-- .../presentation/news/lost/component/LostItemScreen.kt | 8 ++++---- .../presentation/news/notice/component/NoticeScreen.kt | 9 +++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt index f0d4060..d458b02 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -26,10 +27,10 @@ fun FAQScreenContainer( newsViewModel: NewsViewModel, modifier: Modifier = Modifier, ) { - val uiState = newsViewModel.faqUiState.collectAsStateWithLifecycle() + val uiState by newsViewModel.faqUiState.collectAsStateWithLifecycle() FAQScreen( - uiState = uiState.value, + uiState = uiState, onFaqClick = { faq -> newsViewModel.toggleFAQ(faq) }, modifier = modifier, ) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt index 9a84ffa..d50fafe 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt @@ -42,15 +42,15 @@ fun LostItemScreenContainer( newsViewModel: NewsViewModel, modifier: Modifier = Modifier, ) { - val uiState = newsViewModel.lostUiState.collectAsStateWithLifecycle() - val isRefreshing = uiState.value is LostUiState.Refreshing + val uiState by newsViewModel.lostUiState.collectAsStateWithLifecycle() + val isRefreshing = uiState is LostUiState.Refreshing LostItemScreen( - lostUiState = uiState.value, + lostUiState = uiState, onLostGuideClick = { newsViewModel.toggleLostGuide() }, isRefreshing = isRefreshing, onRefresh = { - val oldLostItems = (uiState.value as? LostUiState.Success)?.lostItems ?: emptyList() + val oldLostItems = (uiState as? LostUiState.Success)?.lostItems ?: emptyList() newsViewModel.loadAllLostItems(LostUiState.Refreshing(oldLostItems)) }, modifier = modifier, diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt index 98fcd9c..fc459b2 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource @@ -35,16 +36,16 @@ fun NoticeScreenContainer( newsViewModel: NewsViewModel, modifier: Modifier = Modifier, ) { - val uiState = newsViewModel.noticeUiState.collectAsStateWithLifecycle() - val isRefreshing = uiState.value is NoticeUiState.Refreshing + val uiState by newsViewModel.noticeUiState.collectAsStateWithLifecycle() + val isRefreshing = uiState is NoticeUiState.Refreshing NoticeScreen( - uiState = uiState.value, + uiState = uiState, onNoticeClick = { notice -> newsViewModel.toggleNotice(notice) }, isRefreshing = isRefreshing, onRefresh = { val oldNotices = - (uiState.value as? NoticeUiState.Success)?.notices ?: emptyList() + (uiState as? NoticeUiState.Success)?.notices ?: emptyList() newsViewModel.loadAllNotices(NoticeUiState.Refreshing(oldNotices)) }, modifier = modifier, From 0803163c867e803540170b49b4353e45083444bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Wed, 26 Nov 2025 22:31:27 +0900 Subject: [PATCH 12/23] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=EC=8B=9C=20=ED=99=94=EB=A9=B4=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../festabook/presentation/main/MainActivity.kt | 16 ++++++++-------- .../festabook/presentation/main/MainViewModel.kt | 12 +----------- .../festabook/presentation/news/NewsViewModel.kt | 4 ++-- .../presentation/news/component/NewsScreen.kt | 14 ++++++++++++-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/MainActivity.kt b/app/src/main/java/com/daedan/festabook/presentation/main/MainActivity.kt index 8df33ef..4a01b03 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/MainActivity.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/MainActivity.kt @@ -33,6 +33,7 @@ import com.daedan.festabook.presentation.common.showToast import com.daedan.festabook.presentation.home.HomeFragment import com.daedan.festabook.presentation.home.HomeViewModel import com.daedan.festabook.presentation.news.NewsFragment +import com.daedan.festabook.presentation.news.NewsViewModel import com.daedan.festabook.presentation.placeMap.PlaceMapFragment import com.daedan.festabook.presentation.schedule.ScheduleFragment import com.daedan.festabook.presentation.setting.SettingFragment @@ -44,7 +45,6 @@ import timber.log.Timber class MainActivity : AppCompatActivity(), NotificationPermissionRequester { - @Inject override lateinit var defaultViewModelProviderFactory: ViewModelProvider.Factory @@ -59,6 +59,7 @@ class MainActivity : private val mainViewModel: MainViewModel by viewModels() private val homeViewModel: HomeViewModel by viewModels() + private val newsViewModel: NewsViewModel by viewModels() private val settingViewModel: SettingViewModel by viewModels() private val notificationPermissionManager by lazy { @@ -116,25 +117,24 @@ class MainActivity : ) { grantResults.forEachIndexed { index, result -> val text = permissions[index] - when(text) { + when (text) { Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION -> { + Manifest.permission.ACCESS_COARSE_LOCATION, + -> { if (!result.isGranted()) { showNotificationDeniedSnackbar( binding.root, this, - getString(R.string.map_request_location_permission_message) + getString(R.string.map_request_location_permission_message), ) } } } - } super.onRequestPermissionsResult(requestCode, permissions, grantResults) } - override fun shouldShowPermissionRationale(permission: String): Boolean = - shouldShowRequestPermissionRationale(permission) + override fun shouldShowPermissionRationale(permission: String): Boolean = shouldShowRequestPermissionRationale(permission) override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) @@ -145,7 +145,7 @@ class MainActivity : val canNavigateToNewsScreen = intent.getBooleanExtra(KEY_CAN_NAVIGATE_TO_NEWS, false) val noticeIdToExpand = intent.getLongExtra(KEY_NOTICE_ID_TO_EXPAND, INITIALIZED_ID) - if (noticeIdToExpand != INITIALIZED_ID) mainViewModel.expandNoticeItem(noticeIdToExpand) + if (noticeIdToExpand != INITIALIZED_ID) newsViewModel.expandNotice(noticeIdToExpand) if (canNavigateToNewsScreen) { binding.bnvMenu.selectedItemId = R.id.item_menu_news diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/MainViewModel.kt b/app/src/main/java/com/daedan/festabook/presentation/main/MainViewModel.kt index 4f453bb..a97d8f0 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/MainViewModel.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/MainViewModel.kt @@ -5,7 +5,6 @@ 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.DeviceRepository import com.daedan.festabook.domain.repository.FestivalRepository import com.daedan.festabook.presentation.common.Event @@ -25,13 +24,8 @@ class MainViewModel @Inject constructor( private val _backPressEvent: MutableLiveData> = MutableLiveData() val backPressEvent: LiveData> get() = _backPressEvent - private val _noticeIdToExpand: MutableLiveData = MutableLiveData() - val noticeIdToExpand: LiveData = _noticeIdToExpand - private val _isFirstVisit = - MutableLiveData( - festivalRepository.getIsFirstVisit().getOrDefault(true), - ) + MutableLiveData(festivalRepository.getIsFirstVisit().getOrDefault(true)) val isFirstVisit: LiveData get() = _isFirstVisit private var lastBackPressedTime: Long = 0 @@ -90,10 +84,6 @@ class MainViewModel @Inject constructor( } } - fun expandNoticeItem(announcementId: Long) { - _noticeIdToExpand.value = announcementId - } - companion object { private const val BACK_PRESS_INTERVAL: Long = 2000L } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt index 17b7e97..e3f8076 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt @@ -91,8 +91,8 @@ class NewsViewModel( } } - fun expandNotice(noticeId: Long) { - this.noticeIdToExpand = noticeId + fun expandNotice(noticeIdToExpand: Long) { + this.noticeIdToExpand = noticeIdToExpand val notices = when (val currentState = _noticeUiState.value) { is NoticeUiState.Refreshing -> currentState.oldNotices diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt index 1d1dc35..fd2ebb3 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt @@ -13,18 +13,21 @@ import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.presentation.common.component.Header -import com.daedan.festabook.presentation.main.MainViewModel import com.daedan.festabook.presentation.news.NewsTab import com.daedan.festabook.presentation.news.NewsViewModel import com.daedan.festabook.presentation.news.faq.component.FAQScreenContainer import com.daedan.festabook.presentation.news.lost.component.LostItemScreenContainer +import com.daedan.festabook.presentation.news.notice.NoticeUiState import com.daedan.festabook.presentation.news.notice.component.NoticeScreenContainer import com.daedan.festabook.presentation.theme.FestabookColor import kotlinx.coroutines.CoroutineScope @@ -33,12 +36,19 @@ import kotlinx.coroutines.launch @Composable fun NewsScreen( newsViewModel: NewsViewModel, - mainViewModel: MainViewModel, modifier: Modifier = Modifier, ) { val pageState = rememberPagerState { NewsTab.entries.size } val scope = rememberCoroutineScope() + val noticeUiState by newsViewModel.noticeUiState.collectAsStateWithLifecycle() + + LaunchedEffect(noticeUiState) { + if (noticeUiState is NoticeUiState.Success) { + pageState.animateScrollToPage(NewsTab.NOTICE.ordinal) + } + } + Column(modifier = modifier.background(color = MaterialTheme.colorScheme.background)) { Header(title = stringResource(R.string.news_title)) NewsTabRow(pageState, scope) From 5a825541763818caa3bc0b03f18fbb7ed931cafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Wed, 26 Nov 2025 22:32:22 +0900 Subject: [PATCH 13/23] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../news/adapter/NewsPagerAdapter.kt | 21 ------- .../presentation/news/faq/FAQFragment.kt | 40 ------------- .../news/lost/LostItemFragment.kt | 41 ------------- .../news/notice/NoticeFragment.kt | 57 ------------------- 4 files changed, 159 deletions(-) delete mode 100644 app/src/main/java/com/daedan/festabook/presentation/news/adapter/NewsPagerAdapter.kt delete mode 100644 app/src/main/java/com/daedan/festabook/presentation/news/faq/FAQFragment.kt delete mode 100644 app/src/main/java/com/daedan/festabook/presentation/news/lost/LostItemFragment.kt delete mode 100644 app/src/main/java/com/daedan/festabook/presentation/news/notice/NoticeFragment.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/adapter/NewsPagerAdapter.kt b/app/src/main/java/com/daedan/festabook/presentation/news/adapter/NewsPagerAdapter.kt deleted file mode 100644 index 9373d9f..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/news/adapter/NewsPagerAdapter.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.daedan.festabook.presentation.news.adapter - -import androidx.fragment.app.Fragment -import androidx.viewpager2.adapter.FragmentStateAdapter -import com.daedan.festabook.presentation.news.NewsTab -import com.daedan.festabook.presentation.news.faq.FAQFragment -import com.daedan.festabook.presentation.news.lost.LostItemFragment -import com.daedan.festabook.presentation.news.notice.NoticeFragment - -class NewsPagerAdapter( - fragment: Fragment, -) : FragmentStateAdapter(fragment) { - override fun getItemCount(): Int = NewsTab.entries.size - - override fun createFragment(position: Int): Fragment = - when (NewsTab.entries[position]) { - NewsTab.NOTICE -> NoticeFragment.newInstance() - NewsTab.FAQ -> FAQFragment.newInstance() - NewsTab.LOST_ITEM -> LostItemFragment.newInstance() - } -} diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/faq/FAQFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/news/faq/FAQFragment.kt deleted file mode 100644 index 2a4ed93..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/news/faq/FAQFragment.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.daedan.festabook.presentation.news.faq - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -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 com.daedan.festabook.R -import com.daedan.festabook.databinding.FragmentFaqBinding -import com.daedan.festabook.di.appGraph -import com.daedan.festabook.presentation.common.BaseFragment -import com.daedan.festabook.presentation.news.NewsViewModel -import com.daedan.festabook.presentation.news.faq.component.FAQScreenContainer - -class FAQFragment : BaseFragment() { - override val layoutId: Int = R.layout.fragment_faq - - override val defaultViewModelProviderFactory: ViewModelProvider.Factory - get() = appGraph.metroViewModelFactory - private val viewModel: NewsViewModel by viewModels({ requireParentFragment() }) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View = - ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - FAQScreenContainer(newsViewModel = viewModel) - } - } - - companion object { - fun newInstance() = FAQFragment() - } -} diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/lost/LostItemFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/news/lost/LostItemFragment.kt deleted file mode 100644 index 37861c3..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/news/lost/LostItemFragment.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.daedan.festabook.presentation.news.lost - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -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 com.daedan.festabook.R -import com.daedan.festabook.databinding.FragmentLostItemBinding -import com.daedan.festabook.di.appGraph -import com.daedan.festabook.presentation.common.BaseFragment -import com.daedan.festabook.presentation.news.NewsViewModel -import com.daedan.festabook.presentation.news.lost.component.LostItemScreenContainer - -class LostItemFragment : BaseFragment() { - override val layoutId: Int = R.layout.fragment_lost_item - - override val defaultViewModelProviderFactory: ViewModelProvider.Factory - get() = appGraph.metroViewModelFactory - - private val viewModel: NewsViewModel by viewModels({ requireParentFragment() }) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View = - ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - LostItemScreenContainer(newsViewModel = viewModel) - } - } - - companion object { - fun newInstance() = LostItemFragment() - } -} diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/notice/NoticeFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/news/notice/NoticeFragment.kt deleted file mode 100644 index 26c654a..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/news/notice/NoticeFragment.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.daedan.festabook.presentation.news.notice - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -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 com.daedan.festabook.R -import com.daedan.festabook.databinding.FragmentNoticeBinding -import com.daedan.festabook.di.appGraph -import com.daedan.festabook.presentation.common.BaseFragment -import com.daedan.festabook.presentation.main.MainViewModel -import com.daedan.festabook.presentation.news.NewsViewModel -import com.daedan.festabook.presentation.news.notice.component.NoticeScreenContainer - -class NoticeFragment : BaseFragment() { - override val layoutId: Int = R.layout.fragment_notice - - override val defaultViewModelProviderFactory: ViewModelProvider.Factory - get() = appGraph.metroViewModelFactory - - private val newsViewModel: NewsViewModel by viewModels({ requireParentFragment() }) - private val mainViewModel: MainViewModel by viewModels({ requireActivity() }) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View = - ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - NoticeScreenContainer(newsViewModel = newsViewModel) - } - } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - setupObserver() - } - - private fun setupObserver() { - mainViewModel.noticeIdToExpand.observe(viewLifecycleOwner) { noticeId -> - newsViewModel.expandNotice(noticeId) - } - } - - companion object { - fun newInstance() = NoticeFragment() - } -} From e50328fdfc02c01bd78e4f79ea6724329b53279d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Wed, 26 Nov 2025 22:33:47 +0900 Subject: [PATCH 14/23] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/news/NewsFragment.kt | 34 ++----------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt index cb0a9a4..3299f19 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/NewsFragment.kt @@ -13,8 +13,6 @@ import com.daedan.festabook.R import com.daedan.festabook.databinding.FragmentNewsBinding import com.daedan.festabook.di.fragment.FragmentKey import com.daedan.festabook.presentation.common.BaseFragment -import com.daedan.festabook.presentation.main.MainViewModel -import com.daedan.festabook.presentation.news.adapter.NewsPagerAdapter import com.daedan.festabook.presentation.news.component.NewsScreen import com.daedan.festabook.presentation.theme.FestabookTheme import dev.zacsweers.metro.AppScope @@ -34,11 +32,7 @@ class NewsFragment : BaseFragment() { @Inject override lateinit var defaultViewModelProviderFactory: ViewModelProvider.Factory - private val newsPagerAdapter by lazy { - NewsPagerAdapter(this) - } - private val newsViewModel: NewsViewModel by viewModels() - private val mainViewModel: MainViewModel by viewModels({ requireActivity() }) + private val newsViewModel: NewsViewModel by viewModels({ requireActivity() }) override fun onCreateView( inflater: LayoutInflater, @@ -49,32 +43,8 @@ class NewsFragment : BaseFragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { FestabookTheme { - NewsScreen(mainViewModel = mainViewModel, newsViewModel = newsViewModel) + NewsScreen(newsViewModel = newsViewModel) } } } - -// override fun onViewCreated( -// view: View, -// savedInstanceState: Bundle?, -// ) { -// super.onViewCreated(view, savedInstanceState) -// binding.lifecycleOwner = viewLifecycleOwner -// setupNewsTabLayout() -// mainViewModel.noticeIdToExpand.observe(viewLifecycleOwner) { -// binding.vpNews.currentItem = NOTICE_TAB_INDEX -// } -// } -// -// private fun setupNewsTabLayout() { -// binding.vpNews.adapter = newsPagerAdapter -// TabLayoutMediator(binding.tlNews, binding.vpNews) { tab, position -> -// val tabNameRes = NewsTab.entries[position].tabNameRes -// tab.text = getString(tabNameRes) -// }.attach() -// } -// -// companion object { -// private const val NOTICE_TAB_INDEX: Int = 0 -// } } From 42beb6ed574b3229ed07cf3d44533a9df4d813ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Wed, 26 Nov 2025 22:33:59 +0900 Subject: [PATCH 15/23] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EB=B7=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../festabook/presentation/news/faq/component/FAQScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt index d458b02..ca4d37a 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.presentation.common.component.EmptyStateScreen +import com.daedan.festabook.presentation.common.component.LoadingStateScreen import com.daedan.festabook.presentation.news.NewsViewModel import com.daedan.festabook.presentation.news.component.NewsItem import com.daedan.festabook.presentation.news.faq.FAQUiState @@ -49,7 +50,7 @@ fun FAQScreen( } } - is FAQUiState.InitialLoading -> Unit + is FAQUiState.InitialLoading -> LoadingStateScreen() is FAQUiState.Success -> { if (uiState.faqs.isEmpty()) { From fce5b7fd144d404a16e31892280565c208e2edba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Wed, 26 Nov 2025 22:39:40 +0900 Subject: [PATCH 16/23] =?UTF-8?q?refactor:=20loadAllFAQs=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20=EC=8B=9C=20=EC=B4=88=EA=B8=B0=20=EB=A1=9C=EB=94=A9?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=EB=AA=85=EC=8B=9C=EC=A0=81=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/daedan/festabook/presentation/news/NewsViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt index e3f8076..dc47a55 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/NewsViewModel.kt @@ -50,7 +50,7 @@ class NewsViewModel( init { loadAllNotices(NoticeUiState.InitialLoading) - loadAllFAQs() + loadAllFAQs(FAQUiState.InitialLoading) loadAllLostItems(LostUiState.InitialLoading) } @@ -144,7 +144,7 @@ class NewsViewModel( } } - private fun loadAllFAQs(state: FAQUiState = FAQUiState.InitialLoading) { + private fun loadAllFAQs(state: FAQUiState) { viewModelScope.launch { _faqUiState.value = state From 74a503640e3efbba9638a098382c1e21d0ec452b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Thu, 27 Nov 2025 18:32:36 +0900 Subject: [PATCH 17/23] =?UTF-8?q?refactor:=20=EC=86=8C=EC=8B=9D=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=BB=B4=ED=8F=AC=EC=A0=80=EB=B8=94=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/news/component/NewsScreen.kt | 103 +++++------------- .../news/component/NewsTabPage.kt | 66 +++++++++++ .../presentation/news/component/NewsTabRow.kt | 44 ++++++++ .../news/faq/component/FAQScreen.kt | 14 --- .../news/lost/component/LostItemScreen.kt | 20 ---- .../news/notice/component/NoticeScreen.kt | 21 ---- 6 files changed, 136 insertions(+), 132 deletions(-) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt create mode 100644 app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt index fd2ebb3..54f2bb0 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt @@ -2,36 +2,21 @@ package com.daedan.festabook.presentation.news.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow -import androidx.compose.material3.TabRowDefaults -import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.presentation.common.component.Header import com.daedan.festabook.presentation.news.NewsTab import com.daedan.festabook.presentation.news.NewsViewModel -import com.daedan.festabook.presentation.news.faq.component.FAQScreenContainer -import com.daedan.festabook.presentation.news.lost.component.LostItemScreenContainer +import com.daedan.festabook.presentation.news.lost.LostUiState import com.daedan.festabook.presentation.news.notice.NoticeUiState -import com.daedan.festabook.presentation.news.notice.component.NoticeScreenContainer -import com.daedan.festabook.presentation.theme.FestabookColor -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch @Composable fun NewsScreen( @@ -42,6 +27,11 @@ fun NewsScreen( val scope = rememberCoroutineScope() val noticeUiState by newsViewModel.noticeUiState.collectAsStateWithLifecycle() + val lostUiState by newsViewModel.lostUiState.collectAsStateWithLifecycle() + val faqUiState by newsViewModel.faqUiState.collectAsStateWithLifecycle() + + val isNoticeRefreshing = noticeUiState is NoticeUiState.Refreshing + val isLostItemRefreshing = noticeUiState is NoticeUiState.Refreshing LaunchedEffect(noticeUiState) { if (noticeUiState is NoticeUiState.Success) { @@ -52,66 +42,25 @@ fun NewsScreen( Column(modifier = modifier.background(color = MaterialTheme.colorScheme.background)) { Header(title = stringResource(R.string.news_title)) NewsTabRow(pageState, scope) - NewsTabPage(pageState, newsViewModel) - } -} - -@Composable -private fun NewsTabRow( - pageState: PagerState, - scope: CoroutineScope, -) { - TabRow( - selectedTabIndex = pageState.currentPage, - containerColor = MaterialTheme.colorScheme.background, - contentColor = FestabookColor.black, - indicator = { tabPositions -> - TabRowDefaults.PrimaryIndicator( - color = FestabookColor.black, - width = tabPositions[pageState.currentPage].width, - modifier = Modifier.tabIndicatorOffset(currentTabPosition = tabPositions[pageState.currentPage]), - ) - }, - ) { - NewsTab.entries.forEachIndexed { index, title -> - Tab( - selected = pageState.currentPage == index, - unselectedContentColor = FestabookColor.gray500, - onClick = { scope.launch { pageState.animateScrollToPage(index) } }, - text = { Text(text = stringResource(title.tabNameRes)) }, - ) - } - } -} - -@Composable -private fun NewsTabPage( - pageState: PagerState, - newsViewModel: NewsViewModel, -) { - HorizontalPager( - state = pageState, - verticalAlignment = Alignment.Top, - ) { index -> - val tab = NewsTab.entries[index] - when (tab) { - NewsTab.NOTICE -> - NoticeScreenContainer( - newsViewModel = newsViewModel, - modifier = Modifier.padding(horizontal = 16.dp), - ) - - NewsTab.FAQ -> - FAQScreenContainer( - newsViewModel = newsViewModel, - modifier = Modifier.padding(horizontal = 16.dp), - ) - - NewsTab.LOST_ITEM -> - LostItemScreenContainer( - newsViewModel = newsViewModel, - modifier = Modifier.padding(horizontal = 16.dp), - ) - } + NewsTabPage( + pageState = pageState, + noticeUiState = noticeUiState, + faqUiState = faqUiState, + lostUiState = lostUiState, + isNoticeRefreshing = isNoticeRefreshing, + isLostItemRefreshing = isLostItemRefreshing, + onNoticeRefresh = { + val oldNotices = + (noticeUiState as? NoticeUiState.Success)?.notices ?: emptyList() + newsViewModel.loadAllNotices(NoticeUiState.Refreshing(oldNotices)) + }, + onLostItemRefresh = { + val oldLostItems = (lostUiState as? LostUiState.Success)?.lostItems ?: emptyList() + newsViewModel.loadAllLostItems(LostUiState.Refreshing(oldLostItems)) + }, + onNoticeClick = { newsViewModel.toggleNotice(it) }, + onFaqClick = { newsViewModel.toggleFAQ(it) }, + onLostGuideClick = { newsViewModel.toggleLostGuide() }, + ) } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt new file mode 100644 index 0000000..e116f01 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt @@ -0,0 +1,66 @@ +package com.daedan.festabook.presentation.news.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.daedan.festabook.presentation.news.NewsTab +import com.daedan.festabook.presentation.news.faq.FAQUiState +import com.daedan.festabook.presentation.news.faq.component.FAQScreen +import com.daedan.festabook.presentation.news.faq.model.FAQItemUiModel +import com.daedan.festabook.presentation.news.lost.LostUiState +import com.daedan.festabook.presentation.news.lost.component.LostItemScreen +import com.daedan.festabook.presentation.news.notice.NoticeUiState +import com.daedan.festabook.presentation.news.notice.component.NoticeScreen +import com.daedan.festabook.presentation.news.notice.model.NoticeUiModel + +@Composable +fun NewsTabPage( + pageState: PagerState, + noticeUiState: NoticeUiState, + faqUiState: FAQUiState, + lostUiState: LostUiState, + onNoticeRefresh: () -> Unit, + onLostItemRefresh: () -> Unit, + isNoticeRefreshing: Boolean, + isLostItemRefreshing: Boolean, + onNoticeClick: (NoticeUiModel) -> Unit, + onFaqClick: (FAQItemUiModel) -> Unit, + onLostGuideClick: () -> Unit, +) { + HorizontalPager( + state = pageState, + verticalAlignment = Alignment.Top, + ) { index -> + val tab = NewsTab.entries[index] + when (tab) { + NewsTab.NOTICE -> + NoticeScreen( + uiState = noticeUiState, + onNoticeClick = onNoticeClick, + isRefreshing = isNoticeRefreshing, + onRefresh = onNoticeRefresh, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + NewsTab.FAQ -> + FAQScreen( + uiState = faqUiState, + onFaqClick = onFaqClick, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + NewsTab.LOST_ITEM -> + LostItemScreen( + lostUiState = lostUiState, + onLostGuideClick = onLostGuideClick, + isRefreshing = isLostItemRefreshing, + onRefresh = onLostItemRefresh, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt new file mode 100644 index 0000000..d29dd36 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt @@ -0,0 +1,44 @@ +package com.daedan.festabook.presentation.news.component + +import androidx.compose.foundation.pager.PagerState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.daedan.festabook.presentation.news.NewsTab +import com.daedan.festabook.presentation.theme.FestabookColor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun NewsTabRow( + pageState: PagerState, + scope: CoroutineScope, +) { + TabRow( + selectedTabIndex = pageState.currentPage, + containerColor = MaterialTheme.colorScheme.background, + contentColor = FestabookColor.black, + indicator = { tabPositions -> + TabRowDefaults.PrimaryIndicator( + color = FestabookColor.black, + width = tabPositions[pageState.currentPage].width, + modifier = Modifier.tabIndicatorOffset(currentTabPosition = tabPositions[pageState.currentPage]), + ) + }, + ) { + NewsTab.entries.forEachIndexed { index, title -> + Tab( + selected = pageState.currentPage == index, + unselectedContentColor = FestabookColor.gray500, + onClick = { scope.launch { pageState.animateScrollToPage(index) } }, + text = { Text(text = stringResource(title.tabNameRes)) }, + ) + } + } +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt index ca4d37a..69e37c5 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt @@ -23,20 +23,6 @@ import timber.log.Timber private const val PADDING: Int = 8 -@Composable -fun FAQScreenContainer( - newsViewModel: NewsViewModel, - modifier: Modifier = Modifier, -) { - val uiState by newsViewModel.faqUiState.collectAsStateWithLifecycle() - - FAQScreen( - uiState = uiState, - onFaqClick = { faq -> newsViewModel.toggleFAQ(faq) }, - modifier = modifier, - ) -} - @Composable fun FAQScreen( uiState: FAQUiState, diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt index d50fafe..5548160 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt @@ -37,26 +37,6 @@ import timber.log.Timber private const val SPAN_COUNT: Int = 2 private const val PADDING: Int = 8 -@Composable -fun LostItemScreenContainer( - newsViewModel: NewsViewModel, - modifier: Modifier = Modifier, -) { - val uiState by newsViewModel.lostUiState.collectAsStateWithLifecycle() - val isRefreshing = uiState is LostUiState.Refreshing - - LostItemScreen( - lostUiState = uiState, - onLostGuideClick = { newsViewModel.toggleLostGuide() }, - isRefreshing = isRefreshing, - onRefresh = { - val oldLostItems = (uiState as? LostUiState.Success)?.lostItems ?: emptyList() - newsViewModel.loadAllLostItems(LostUiState.Refreshing(oldLostItems)) - }, - modifier = modifier, - ) -} - @OptIn(ExperimentalMaterial3Api::class) @Composable fun LostItemScreen( diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt index fc459b2..2f155df 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt @@ -31,27 +31,6 @@ import timber.log.Timber private const val PADDING: Int = 8 -@Composable -fun NoticeScreenContainer( - newsViewModel: NewsViewModel, - modifier: Modifier = Modifier, -) { - val uiState by newsViewModel.noticeUiState.collectAsStateWithLifecycle() - val isRefreshing = uiState is NoticeUiState.Refreshing - - NoticeScreen( - uiState = uiState, - onNoticeClick = { notice -> newsViewModel.toggleNotice(notice) }, - isRefreshing = isRefreshing, - onRefresh = { - val oldNotices = - (uiState as? NoticeUiState.Success)?.notices ?: emptyList() - newsViewModel.loadAllNotices(NoticeUiState.Refreshing(oldNotices)) - }, - modifier = modifier, - ) -} - @OptIn(ExperimentalMaterial3Api::class) @Composable fun NoticeScreen( From 62ab9915847811417ecb9c88068119ca2b11bfda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Thu, 27 Nov 2025 18:49:22 +0900 Subject: [PATCH 18/23] =?UTF-8?q?feat:=20=ED=94=84=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../news/component/NewsTabPage.kt | 20 +++++++++++++++++++ .../presentation/news/component/NewsTabRow.kt | 12 +++++++++++ 2 files changed, 32 insertions(+) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt index e116f01..edc4897 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt @@ -3,9 +3,11 @@ package com.daedan.festabook.presentation.news.component import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.daedan.festabook.presentation.news.NewsTab import com.daedan.festabook.presentation.news.faq.FAQUiState @@ -64,3 +66,21 @@ fun NewsTabPage( } } } + +@Composable +@Preview +private fun NewsTabPagePreview() { + NewsTabPage( + pageState = rememberPagerState { 3 }, + noticeUiState = NoticeUiState.Success(emptyList(), 0), + faqUiState = FAQUiState.Success(emptyList()), + lostUiState = LostUiState.Success(emptyList()), + onNoticeRefresh = {}, + onLostItemRefresh = {}, + isNoticeRefreshing = false, + isLostItemRefreshing = false, + onNoticeClick = {}, + onFaqClick = {}, + onLostGuideClick = {}, + ) +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt index d29dd36..6a71b09 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt @@ -1,6 +1,7 @@ package com.daedan.festabook.presentation.news.component import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab import androidx.compose.material3.TabRow @@ -8,8 +9,10 @@ import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import com.daedan.festabook.presentation.news.NewsTab import com.daedan.festabook.presentation.theme.FestabookColor import kotlinx.coroutines.CoroutineScope @@ -42,3 +45,12 @@ fun NewsTabRow( } } } + +@Composable +@Preview +private fun NewsTabRowPreview() { + NewsTabRow( + pageState = rememberPagerState { 3 }, + scope = rememberCoroutineScope(), + ) +} From 2c57ef5b0f14a84b5bffa5aa6993efc38ed0ed0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Thu, 27 Nov 2025 18:54:39 +0900 Subject: [PATCH 19/23] =?UTF-8?q?refactor:=20modifier=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../daedan/festabook/presentation/news/component/NewsTabPage.kt | 2 ++ .../daedan/festabook/presentation/news/component/NewsTabRow.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt index edc4897..ab2fe99 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt @@ -32,10 +32,12 @@ fun NewsTabPage( onNoticeClick: (NoticeUiModel) -> Unit, onFaqClick: (FAQItemUiModel) -> Unit, onLostGuideClick: () -> Unit, + modifier: Modifier = Modifier, ) { HorizontalPager( state = pageState, verticalAlignment = Alignment.Top, + modifier = modifier, ) { index -> val tab = NewsTab.entries[index] when (tab) { diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt index 6a71b09..a8bc11b 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabRow.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.launch fun NewsTabRow( pageState: PagerState, scope: CoroutineScope, + modifier: Modifier = Modifier, ) { TabRow( selectedTabIndex = pageState.currentPage, @@ -34,6 +35,7 @@ fun NewsTabRow( modifier = Modifier.tabIndicatorOffset(currentTabPosition = tabPositions[pageState.currentPage]), ) }, + modifier = modifier, ) { NewsTab.entries.forEachIndexed { index, title -> Tab( From 839cf20fbeee9ef802f5e81e946cc2c592010c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Thu, 27 Nov 2025 18:56:47 +0900 Subject: [PATCH 20/23] =?UTF-8?q?fix:=20=EB=B6=84=EC=8B=A4=EB=AC=BC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../daedan/festabook/presentation/news/component/NewsScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt index 54f2bb0..728f9ee 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt @@ -31,7 +31,7 @@ fun NewsScreen( val faqUiState by newsViewModel.faqUiState.collectAsStateWithLifecycle() val isNoticeRefreshing = noticeUiState is NoticeUiState.Refreshing - val isLostItemRefreshing = noticeUiState is NoticeUiState.Refreshing + val isLostItemRefreshing = lostUiState is LostUiState.Refreshing LaunchedEffect(noticeUiState) { if (noticeUiState is NoticeUiState.Success) { From ba5df24a3f84661674d66b2a9620fceafd1b1449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Thu, 27 Nov 2025 19:26:52 +0900 Subject: [PATCH 21/23] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20spacing=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/common/component/Header.kt | 10 +++++++--- .../presentation/news/component/NewsItem.kt | 10 +++++----- .../news/component/NewsTabPage.kt | 8 ++++---- .../news/faq/component/FAQScreen.kt | 11 ++++++---- .../news/lost/component/LostItemScreen.kt | 12 +++++++---- .../news/notice/component/NoticeScreen.kt | 11 ++++++---- .../presentation/theme/FestabookSpacing.kt | 20 +++++++++++++++++++ .../presentation/theme/FestabookTheme.kt | 18 +++++++++++------ 8 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/theme/FestabookSpacing.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt b/app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt index d322d84..1efcadd 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/common/component/Header.kt @@ -7,8 +7,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.daedan.festabook.presentation.theme.FestabookTypography +import com.daedan.festabook.presentation.theme.festabookSpacing @Composable fun Header( @@ -21,8 +21,12 @@ fun Header( style = style, modifier = modifier - .padding(top = 40.dp, bottom = 16.dp, start = 16.dp, end = 16.dp) - .fillMaxWidth(), + .padding( + top = festabookSpacing.paddingTitleHorizontal, + bottom = festabookSpacing.paddingBody4, + start = festabookSpacing.paddingScreenGutter, + end = festabookSpacing.paddingScreenGutter, + ).fillMaxWidth(), ) } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsItem.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsItem.kt index c3f9876..d002aeb 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsItem.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsItem.kt @@ -21,11 +21,11 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.daedan.festabook.R import com.daedan.festabook.presentation.common.component.cardBackground import com.daedan.festabook.presentation.theme.FestabookColor import com.daedan.festabook.presentation.theme.FestabookTypography +import com.daedan.festabook.presentation.theme.festabookSpacing private const val ICON_ROTATION_EXPANDED: Float = 180F private const val ICON_ROTATION_COLLAPSED: Float = 0F @@ -54,7 +54,7 @@ fun NewsItem( indication = null, interactionSource = null, ) { onclick() } - .padding(16.dp), + .padding(festabookSpacing.paddingBody4), ) { Row( modifier = Modifier.fillMaxWidth(), @@ -62,14 +62,14 @@ fun NewsItem( ) { if (icon != null) { icon() - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(festabookSpacing.paddingBody2)) } Text( text = title, style = FestabookTypography.titleSmall, modifier = Modifier.weight(1f), ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(festabookSpacing.paddingBody2)) if (createdAt != null) { Text( text = createdAt, @@ -86,7 +86,7 @@ fun NewsItem( } if (isExpanded) { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(festabookSpacing.paddingBody2)) Text(text = description) } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt index ab2fe99..1fbf10a 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsTabPage.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.daedan.festabook.presentation.news.NewsTab import com.daedan.festabook.presentation.news.faq.FAQUiState import com.daedan.festabook.presentation.news.faq.component.FAQScreen @@ -18,6 +17,7 @@ import com.daedan.festabook.presentation.news.lost.component.LostItemScreen import com.daedan.festabook.presentation.news.notice.NoticeUiState import com.daedan.festabook.presentation.news.notice.component.NoticeScreen import com.daedan.festabook.presentation.news.notice.model.NoticeUiModel +import com.daedan.festabook.presentation.theme.festabookSpacing @Composable fun NewsTabPage( @@ -47,14 +47,14 @@ fun NewsTabPage( onNoticeClick = onNoticeClick, isRefreshing = isNoticeRefreshing, onRefresh = onNoticeRefresh, - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = festabookSpacing.paddingScreenGutter), ) NewsTab.FAQ -> FAQScreen( uiState = faqUiState, onFaqClick = onFaqClick, - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = festabookSpacing.paddingScreenGutter), ) NewsTab.LOST_ITEM -> @@ -63,7 +63,7 @@ fun NewsTabPage( onLostGuideClick = onLostGuideClick, isRefreshing = isLostItemRefreshing, onRefresh = onLostItemRefresh, - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = festabookSpacing.paddingScreenGutter), ) } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt index 69e37c5..5738544 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/faq/component/FAQScreen.kt @@ -19,10 +19,9 @@ import com.daedan.festabook.presentation.news.NewsViewModel import com.daedan.festabook.presentation.news.component.NewsItem import com.daedan.festabook.presentation.news.faq.FAQUiState import com.daedan.festabook.presentation.news.faq.model.FAQItemUiModel +import com.daedan.festabook.presentation.theme.festabookSpacing import timber.log.Timber -private const val PADDING: Int = 8 - @Composable fun FAQScreen( uiState: FAQUiState, @@ -44,8 +43,12 @@ fun FAQScreen( } else { LazyColumn( modifier = modifier, - contentPadding = PaddingValues(top = PADDING.dp, bottom = PADDING.dp), - verticalArrangement = Arrangement.spacedBy(PADDING.dp), + contentPadding = + PaddingValues( + top = festabookSpacing.paddingBody2, + bottom = festabookSpacing.paddingBody2, + ), + verticalArrangement = Arrangement.spacedBy(festabookSpacing.paddingBody2), ) { items( items = uiState.faqs, diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt index 5548160..6272bd0 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItemScreen.kt @@ -32,10 +32,10 @@ import com.daedan.festabook.presentation.news.component.NewsItem import com.daedan.festabook.presentation.news.lost.LostUiState import com.daedan.festabook.presentation.news.lost.model.LostItemUiStatus import com.daedan.festabook.presentation.news.lost.model.LostUiModel +import com.daedan.festabook.presentation.theme.festabookSpacing import timber.log.Timber private const val SPAN_COUNT: Int = 2 -private const val PADDING: Int = 8 @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -116,9 +116,13 @@ private fun LostItemContent( LazyVerticalGrid( modifier = modifier, columns = GridCells.Fixed(SPAN_COUNT), - contentPadding = PaddingValues(top = PADDING.dp, bottom = PADDING.dp), - verticalArrangement = Arrangement.spacedBy(PADDING.dp), - horizontalArrangement = Arrangement.spacedBy(PADDING.dp), + contentPadding = + PaddingValues( + top = festabookSpacing.paddingBody2, + bottom = festabookSpacing.paddingBody2, + ), + verticalArrangement = Arrangement.spacedBy(festabookSpacing.paddingBody2), + horizontalArrangement = Arrangement.spacedBy(festabookSpacing.paddingBody2), ) { item(span = { GridItemSpan(SPAN_COUNT) }) { val guide = lostItems.firstOrNull() as? LostUiModel.Guide diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt index 2f155df..fbc2815 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/notice/component/NoticeScreen.kt @@ -27,10 +27,9 @@ import com.daedan.festabook.presentation.news.component.NewsItem import com.daedan.festabook.presentation.news.notice.NoticeUiState import com.daedan.festabook.presentation.news.notice.NoticeUiState.Companion.DEFAULT_POSITION import com.daedan.festabook.presentation.news.notice.model.NoticeUiModel +import com.daedan.festabook.presentation.theme.festabookSpacing import timber.log.Timber -private const val PADDING: Int = 8 - @OptIn(ExperimentalMaterial3Api::class) @Composable fun NoticeScreen( @@ -97,8 +96,12 @@ private fun NoticeContent( LazyColumn( modifier = modifier, state = listState, - contentPadding = PaddingValues(top = PADDING.dp, bottom = PADDING.dp), - verticalArrangement = Arrangement.spacedBy(PADDING.dp), + contentPadding = + PaddingValues( + top = festabookSpacing.paddingBody2, + bottom = festabookSpacing.paddingBody2, + ), + verticalArrangement = Arrangement.spacedBy(festabookSpacing.paddingBody2), ) { items( items = notices, diff --git a/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookSpacing.kt b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookSpacing.kt new file mode 100644 index 0000000..3d12413 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookSpacing.kt @@ -0,0 +1,20 @@ +package com.daedan.festabook.presentation.theme + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class FestabookSpacing( + val paddingScreenGutter: Dp = 16.dp, + val paddingTitleHorizontal: Dp = 40.dp, + val paddingBody1: Dp = 4.dp, + val paddingBody2: Dp = 8.dp, + val paddingBody3: Dp = 12.dp, + val paddingBody4: Dp = 16.dp, +) + +val LocalSpacing = staticCompositionLocalOf { FestabookSpacing() } + +val festabookSpacing + @Composable get() = LocalSpacing.current diff --git a/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt index 9d1b0e7..d0dd8ae 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt @@ -3,6 +3,7 @@ package com.daedan.festabook.presentation.theme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider private val LightColorScheme = lightColorScheme( @@ -11,10 +12,15 @@ private val LightColorScheme = @Composable fun FestabookTheme(content: @Composable () -> Unit) { - MaterialTheme( - colorScheme = LightColorScheme, - shapes = FestabookShapesTheme, - typography = FestabookTypography, - content = content, - ) + val spacing = FestabookSpacing() + CompositionLocalProvider( + LocalSpacing provides spacing, + ) { + MaterialTheme( + colorScheme = LightColorScheme, + shapes = FestabookShapesTheme, + typography = FestabookTypography, + content = content, + ) + } } From dde92db711c2fa9edfdb7554a74c77476ddd445b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Thu, 27 Nov 2025 19:43:51 +0900 Subject: [PATCH 22/23] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20Shape=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/component/CardBackground.kt | 24 +++++++++++-------- .../news/lost/component/LostItem.kt | 20 ++++++++-------- .../presentation/theme/FestabookShapes.kt | 15 ++++-------- .../presentation/theme/FestabookTheme.kt | 3 ++- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/common/component/CardBackground.kt b/app/src/main/java/com/daedan/festabook/presentation/common/component/CardBackground.kt index e126200..cacbeba 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/common/component/CardBackground.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/common/component/CardBackground.kt @@ -4,38 +4,42 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.daedan.festabook.presentation.theme.FestabookColor +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.festabookShapes @Composable fun Modifier.cardBackground( backgroundColor: Color = FestabookColor.gray100, borderStroke: Dp = 1.dp, borderColor: Color = FestabookColor.gray200, - roundedCornerShape: Dp = 16.dp, + shape: Shape = festabookShapes.radius3, ): Modifier = background( color = backgroundColor, - shape = RoundedCornerShape(roundedCornerShape), + shape = shape, ).border( width = borderStroke, color = borderColor, - shape = RoundedCornerShape(roundedCornerShape), + shape = shape, ) @Composable @Preview(showBackground = true) private fun CardBackgroundPreview() { - Box( - modifier = - Modifier - .cardBackground() - .size(120.dp), - ) + FestabookTheme { + Box( + modifier = + Modifier + .cardBackground() + .size(120.dp), + ) + } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItem.kt b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItem.kt index 50e2635..bf3567e 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItem.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/lost/component/LostItem.kt @@ -3,18 +3,16 @@ package com.daedan.festabook.presentation.news.lost.component import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.daedan.festabook.R import com.daedan.festabook.presentation.common.component.CoilImage import com.daedan.festabook.presentation.common.component.cardBackground - -private const val ROUNDED_CORNER_SHAPE = 16 +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.festabookShapes @Composable fun LostItem( @@ -23,10 +21,10 @@ fun LostItem( onLostItemClick: () -> Unit = {}, ) { Card( - shape = RoundedCornerShape(ROUNDED_CORNER_SHAPE.dp), + shape = festabookShapes.radius3, modifier = modifier - .cardBackground(roundedCornerShape = ROUNDED_CORNER_SHAPE.dp) + .cardBackground() .aspectRatio(1f) .clickable(indication = null, interactionSource = null) { onLostItemClick() }, ) { @@ -41,8 +39,10 @@ fun LostItem( @Composable @Preview private fun LostItemPreview() { - LostItem( - url = "https://i.imgur.com/Zblctu7.png", - onLostItemClick = { }, - ) + FestabookTheme { + LostItem( + url = "https://i.imgur.com/Zblctu7.png", + onLostItemClick = { }, + ) + } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookShapes.kt b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookShapes.kt index 819b6ed..8f08a18 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookShapes.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookShapes.kt @@ -2,7 +2,8 @@ package com.daedan.festabook.presentation.theme import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Shapes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.unit.dp data class FestabookShapes( @@ -14,13 +15,7 @@ data class FestabookShapes( val radiusFull: CornerBasedShape = RoundedCornerShape(999.dp), ) -val festabookShapes = FestabookShapes() +val LocalShapes = staticCompositionLocalOf { FestabookShapes() } -val FestabookShapesTheme = - Shapes( - extraSmall = festabookShapes.radius1, - small = festabookShapes.radius2, - medium = festabookShapes.radius3, - large = festabookShapes.radius4, - extraLarge = festabookShapes.radius5, - ) +val festabookShapes: FestabookShapes + @Composable get() = LocalShapes.current diff --git a/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt index d0dd8ae..5a7b46b 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/theme/FestabookTheme.kt @@ -13,12 +13,13 @@ private val LightColorScheme = @Composable fun FestabookTheme(content: @Composable () -> Unit) { val spacing = FestabookSpacing() + val shapes = FestabookShapes() CompositionLocalProvider( LocalSpacing provides spacing, + LocalShapes provides shapes, ) { MaterialTheme( colorScheme = LightColorScheme, - shapes = FestabookShapesTheme, typography = FestabookTypography, content = content, ) From 134a09ec96fd93af30363ffa8b29c6a1c04d7000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=EB=AF=BC?= Date: Fri, 28 Nov 2025 09:48:24 +0900 Subject: [PATCH 23/23] =?UTF-8?q?refactor:=20NewsViewModelTest=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../festabook/news/NewsViewModelTest.kt | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/app/src/test/java/com/daedan/festabook/news/NewsViewModelTest.kt b/app/src/test/java/com/daedan/festabook/news/NewsViewModelTest.kt index ff45996..e5e1518 100644 --- a/app/src/test/java/com/daedan/festabook/news/NewsViewModelTest.kt +++ b/app/src/test/java/com/daedan/festabook/news/NewsViewModelTest.kt @@ -5,7 +5,6 @@ import com.daedan.festabook.domain.model.Lost import com.daedan.festabook.domain.repository.FAQRepository import com.daedan.festabook.domain.repository.LostItemRepository import com.daedan.festabook.domain.repository.NoticeRepository -import com.daedan.festabook.getOrAwaitValue import com.daedan.festabook.presentation.news.NewsViewModel import com.daedan.festabook.presentation.news.faq.FAQUiState import com.daedan.festabook.presentation.news.faq.model.toUiModel @@ -79,7 +78,7 @@ class NewsViewModelTest { // then val expected = FAKE_NOTICES.map { it.toUiModel() } - val actual = newsViewModel.noticeUiState + val actual = newsViewModel.noticeUiState.value coVerify { noticeRepository.fetchNotices() } assertThat(actual).isEqualTo( NoticeUiState.Success(expected, DEFAULT_POSITION), @@ -99,11 +98,11 @@ class NewsViewModelTest { ) // when - newsViewModel.loadAllLostItems() + newsViewModel.loadAllLostItems(LostUiState.InitialLoading) advanceUntilIdle() // then - val actual = newsViewModel.lostUiState.getOrAwaitValue() + val actual = newsViewModel.lostUiState.value coVerify { lostItemRepository.getLost() } assertThat(actual).isEqualTo(expected) } @@ -121,7 +120,7 @@ class NewsViewModelTest { // then val expected = NoticeUiState.Error(exception) - val actual = newsViewModel.noticeUiState + val actual = newsViewModel.noticeUiState.value coVerify { noticeRepository.fetchNotices() } assertThat(actual).isEqualTo(expected) } @@ -138,7 +137,7 @@ class NewsViewModelTest { // then val expected = FAKE_FAQS.map { it.toUiModel() } - val actual = newsViewModel.faqUiState + val actual = newsViewModel.faqUiState.value coVerify { faqRepository.getAllFAQ() } assertThat(actual).isEqualTo(FAQUiState.Success(expected)) } @@ -156,7 +155,7 @@ class NewsViewModelTest { // then val expected = FAQUiState.Error(exception) - val actual = newsViewModel.faqUiState + val actual = newsViewModel.faqUiState.value coVerify { faqRepository.getAllFAQ() } assertThat(actual).isEqualTo(expected) } @@ -183,7 +182,7 @@ class NewsViewModelTest { createdAt = LocalDateTime.of(2025, 1, 1, 0, 0, 0), ), ) - val actual = newsViewModel.noticeUiState + val actual = newsViewModel.noticeUiState.value assertThat(actual).isEqualTo(NoticeUiState.Success(expected, DEFAULT_POSITION)) } @@ -199,24 +198,10 @@ class NewsViewModelTest { // then val expected = listOf(faq.copy(isExpanded = true)) - val actual = newsViewModel.faqUiState + val actual = newsViewModel.faqUiState.value assertThat(actual).isEqualTo(FAQUiState.Success(expected)) } - @Test - fun `분실물 아이템의 클릭 이벤트를 발생시킬 수 있다`() = - runTest { - // given - val lostItem: LostUiModel.Item = mockk() - - // when - newsViewModel.lostItemClick(lostItem) - - // then - val actual = newsViewModel.lostItemClickEvent.getOrAwaitValue() - assertThat(actual.peekContent()).isEqualTo(lostItem) - } - @Test fun `처음 로드했을 때 펼처질 공지사항을 지정할 수 있다`() = runTest { @@ -233,7 +218,7 @@ class NewsViewModelTest { advanceUntilIdle() // then - val actual = newsViewModel.noticeUiState + val actual = newsViewModel.noticeUiState.value coVerify { noticeRepository.fetchNotices() } assertThat(actual).isEqualTo(NoticeUiState.Success(expected, 1)) } @@ -256,7 +241,7 @@ class NewsViewModelTest { newsViewModel.toggleLostGuide() // then - val actual = newsViewModel.lostUiState.getOrAwaitValue() + val actual = newsViewModel.lostUiState.value assertThat(actual).isEqualTo(expected) } }