Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
da1a8f6
feat(PlaceDetail): 장소 상세 화면 UI 구현 및 이미지 줌 기능 추가
oungsi2000 Jan 3, 2026
5fd6447
fix(URLText): 유효하지 않은 링크 클릭되는 버그 해결
oungsi2000 Jan 3, 2026
5ef7215
refactor(PlaceDetail): Compose 마이그레이션 및 StateFlow 전환
oungsi2000 Jan 3, 2026
83c3ead
test: PlaceDetailViewModelTest 상태 검증 방식 변경
oungsi2000 Jan 3, 2026
87da1c7
refactor(PlaceDetail): 공통 이미지 컴포넌트 적용 및 레거시 코드 제거(Adapter)
oungsi2000 Jan 7, 2026
cb56901
fix(PlaceDetail): scrollable->verticalScroll로 변경
oungsi2000 Jan 7, 2026
6558783
refactor(PlaceDetail): 장소 상세 정보 UI 컴포넌트 분리
oungsi2000 Jan 11, 2026
e882025
fix: 이미지 페이지와 dialog page의 불일치 문제 해결, 사이드이펙트 LaunchedEffect로 변경
oungsi2000 Jan 11, 2026
e7f2e87
refactor(PlaceDetail): ViewModel 생성자 어노테이션 수정
oungsi2000 Jan 11, 2026
8fd2ba4
refactor(PlaceDetail): PlaceDetailViewModel 초기화 로직 리팩토링
oungsi2000 Jan 11, 2026
21604e1
refactor: currentPage 제거 및 R.string.format_date 사용
oungsi2000 Jan 11, 2026
d242361
feat: backToPreviousButton contentDescription 추가
oungsi2000 Jan 11, 2026
1291476
style(PlaceDetail): 뒤로가기 버튼 패딩 순서 수정 및 불필요한 패딩 제거
oungsi2000 Jan 11, 2026
8daf55e
test(PlaceDetail): PlaceDetailViewModelTest에서 InstantTaskExecutorRule 제거
oungsi2000 Jan 11, 2026
25b990b
test(PlaceDetail): PlaceDetailViewModelTest 검증 로직 안정성 강화
oungsi2000 Jan 11, 2026
a1d05fb
refactor(PlaceDetail): 정보 항목 UI 리팩토링 및 설명 기본값 확장
oungsi2000 Jan 13, 2026
5be8481
feat(PlaceDetail): 상태 표시줄과 겹치지 않도록 UI 수정
oungsi2000 Jan 13, 2026
29e963c
fix(Common): URL 정규식 검증 로직 개선
oungsi2000 Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.daedan.festabook.presentation.common.component

import android.util.Patterns
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.material3.LocalTextStyle
Expand Down Expand Up @@ -31,6 +30,7 @@ import com.daedan.festabook.presentation.theme.FestabookColor
fun URLText(
text: String,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
Expand All @@ -53,23 +53,21 @@ fun URLText(
val linkedText =
buildAnnotatedString {
append(text)
val urlPattern = Patterns.WEB_URL
val matcher = urlPattern.matcher(text)
while (matcher.find()) {
WEB_REGEX.findAll(text).forEach { result ->
addStyle(
style =
SpanStyle(
color = FestabookColor.gray500,
textDecoration = TextDecoration.Underline,
),
start = matcher.start(),
end = matcher.end(),
start = result.range.first,
end = result.range.last + 1,
)
addStringAnnotation(
tag = "URL",
annotation = matcher.group(),
start = matcher.start(),
end = matcher.end(),
annotation = result.value,
start = result.range.first,
end = result.range.last + 1,
)
}
}
Expand All @@ -85,7 +83,7 @@ fun URLText(
.firstOrNull()
?.let { annotation ->
uriHandler.openUri(annotation.item)
}
} ?: onClick()
}
}
},
Expand All @@ -110,3 +108,6 @@ fun URLText(
style = style,
)
}

private val WEB_REGEX =
"""(https?|ftp|file)://[a-zA-Z0-9+&@#/%?=~_|!:,.;]+(?<![.,:;])""".toRegex()
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,29 @@ package com.daedan.festabook.presentation.placeDetail
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.databinding.DataBindingUtil
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.toArgb
import androidx.lifecycle.ViewModelProvider
import androidx.viewpager2.widget.ViewPager2
import com.daedan.festabook.R
import com.daedan.festabook.databinding.ActivityPlaceDetailBinding
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.daedan.festabook.di.appGraph
import com.daedan.festabook.logging.logger
import com.daedan.festabook.presentation.common.getObject
import com.daedan.festabook.presentation.common.showErrorSnackBar
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.adapter.NoticeAdapter
import com.daedan.festabook.presentation.news.notice.model.NoticeUiModel
import com.daedan.festabook.presentation.placeDetail.adapter.PlaceImageViewPagerAdapter
import com.daedan.festabook.presentation.placeDetail.logging.PlaceDetailImageSwipe
import com.daedan.festabook.presentation.placeDetail.model.ImageUiModel
import com.daedan.festabook.presentation.placeDetail.component.PlaceDetailScreen
import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel
import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiState
import com.daedan.festabook.presentation.placeMap.model.PlaceUiModel
import com.daedan.festabook.presentation.theme.FestabookColor
import dev.zacsweers.metro.Inject
import timber.log.Timber

class PlaceDetailActivity :
AppCompatActivity(),
NewsClickListener {
class PlaceDetailActivity : ComponentActivity() {
@Inject
private lateinit var viewModelFactory: PlaceDetailViewModel.Factory

private val noticeAdapter by lazy {
NoticeAdapter(this)
}

private val placeImageAdapter by lazy {
PlaceImageViewPagerAdapter()
}

private lateinit var viewModel: PlaceDetailViewModel

private val binding: ActivityPlaceDetailBinding by lazy {
DataBindingUtil.setContentView(this, R.layout.activity_place_detail)
}

override fun onCreate(savedInstanceState: Bundle?) {
appGraph.inject(this)
super.onCreate(savedInstanceState)
Expand All @@ -61,6 +36,7 @@ class PlaceDetailActivity :
finish()
return
}

viewModel =
ViewModelProvider(
this,
Expand All @@ -71,120 +47,25 @@ class PlaceDetailActivity :
),
)[PlaceDetailViewModel::class.java]

enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}

setUpBinding()
setUpObserver()
Timber.d("detailActivity : ${viewModel.placeDetail.value}")
}

private fun setUpBinding() {
binding.lifecycleOwner = this
binding.rvPlaceNotice.adapter = noticeAdapter
binding.vpPlaceImages.adapter = placeImageAdapter
binding.tvLocation.setExpandedWhenClicked()
binding.tvHost.setExpandedWhenClicked()
binding.ivBackToPrevious.setOnClickListener {
finish()
}
}

private fun setUpObserver() {
viewModel.placeDetail.observe(this) { result ->

when (result) {
is PlaceDetailUiState.Error -> {
Timber.w(result.throwable, "PlaceDetailActivity: ${result.throwable.message}")
showErrorSnackBar(result.throwable)
}

is PlaceDetailUiState.Loading -> {
showSkeleton()
Timber.d("Loading")
}

is PlaceDetailUiState.Success -> {
hideSkeleton()
loadPlaceDetail(result.placeDetail)
}
}
}
}

private fun loadPlaceDetail(placeDetail: PlaceDetailUiModel) {
binding.placeDetail = placeDetail

if (placeDetail.images.isEmpty()) {
placeImageAdapter.submitList(
listOf(ImageUiModel()),
setContent {
enableEdgeToEdge(
statusBarStyle =
SystemBarStyle.light(
scrim = FestabookColor.white.toArgb(),
darkScrim = FestabookColor.white.toArgb(),
),
)
val placeDetailUiState by viewModel.placeDetail.collectAsStateWithLifecycle()
PlaceDetailScreen(
uiState = placeDetailUiState,
onBackToPreviousClick = { finish() },
)
} else {
placeImageAdapter.submitList(placeDetail.images)
binding.clImageIndicator.setViewPager(binding.vpPlaceImages)
}
binding.vpPlaceImages.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int,
) {
binding.logger.log(
PlaceDetailImageSwipe(
baseLogData = binding.logger.getBaseLogData(),
startIndex = position,
),
)
}
},
)
// 임시로 곰지사항을 보이지 않게 하였습니다. 추후 복구 예정입니다
// if (placeDetail.notices.isEmpty()) {
// binding.rvPlaceNotice.visibility = View.GONE
// binding.tvNoNoticeDescription.visibility = View.VISIBLE
// } else {
// noticeAdapter.submitList(placeDetail.notices)
// }
}

private fun showSkeleton() {
binding.layoutContent.visibility = View.GONE
binding.sflScheduleSkeleton.visibility = View.VISIBLE
binding.sflScheduleSkeleton.startShimmer()
}

private fun hideSkeleton() {
binding.layoutContent.visibility = View.VISIBLE
binding.sflScheduleSkeleton.visibility = View.GONE
binding.sflScheduleSkeleton.stopShimmer()
}

private fun TextView.setExpandedWhenClicked(defaultMaxLines: Int = DEFAULT_MAX_LINES) {
setOnClickListener {
maxLines =
if (maxLines == defaultMaxLines) {
Integer.MAX_VALUE
} else {
defaultMaxLines
}
}
}

override fun onNoticeClick(notice: NoticeUiModel) {
viewModel.toggleNoticeExpanded(notice)
Timber.d("detailActivity : ${viewModel.placeDetail.value}")
}

override fun onFAQClick(faqItem: FAQItemUiModel) = Unit

override fun onLostGuideItemClick() = Unit

companion object {
private const val DEFAULT_MAX_LINES = 1
private const val KEY_PLACE_UI_MODEL = "placeUiModel"
private const val KEY_PLACE_DETAIL_UI_MODEL = "placeDetailUiModel"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
package com.daedan.festabook.presentation.placeDetail

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.daedan.festabook.domain.repository.PlaceDetailRepository
import com.daedan.festabook.presentation.news.notice.model.NoticeUiModel
import com.daedan.festabook.presentation.placeDetail.model.ImageUiModel
import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel
import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiState
import com.daedan.festabook.presentation.placeDetail.model.toUiModel
import com.daedan.festabook.presentation.placeMap.model.PlaceUiModel
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class PlaceDetailViewModel @AssistedInject constructor(
@AssistedInject
class PlaceDetailViewModel(
private val placeDetailRepository: PlaceDetailRepository,
@Assisted private val place: PlaceUiModel?,
@Assisted private val receivedPlaceDetail: PlaceDetailUiModel?,
Expand All @@ -31,36 +33,41 @@ class PlaceDetailViewModel @AssistedInject constructor(
}

private val _placeDetail =
MutableLiveData<PlaceDetailUiState>(
MutableStateFlow<PlaceDetailUiState>(
PlaceDetailUiState.Loading,
)
val placeDetail: LiveData<PlaceDetailUiState> = _placeDetail
val placeDetail: StateFlow<PlaceDetailUiState> = _placeDetail

init {
if (receivedPlaceDetail != null) {
_placeDetail.value = PlaceDetailUiState.Success(receivedPlaceDetail)
} else if (place != null) {
loadPlaceDetail(place.id)
receivedPlaceDetail?.let {
val placeDetailUiModel =
if (it.images.isEmpty()) it.copy(images = listOf(ImageUiModel())) else it
_placeDetail.value = PlaceDetailUiState.Success(placeDetailUiModel)
}
place?.let { loadPlaceDetail(it.id) }
}

fun loadPlaceDetail(placeId: Long) {
viewModelScope.launch {
val result = placeDetailRepository.getPlaceDetail(placeId)
result
.onSuccess { placeDetail ->
val placeDetailUiModel =
if (placeDetail.sortedImages.isEmpty()) {
placeDetail.toUiModel().copy(images = listOf(ImageUiModel()))
} else {
placeDetail.toUiModel()
}
_placeDetail.value =
PlaceDetailUiState.Success(
placeDetail.toUiModel(),
)
PlaceDetailUiState.Success(placeDetailUiModel)
}.onFailure {
_placeDetail.value = PlaceDetailUiState.Error(it)
}
}
}

fun toggleNoticeExpanded(notice: NoticeUiModel) {
val currentState = _placeDetail.value ?: return
val currentState = _placeDetail.value
if (currentState !is PlaceDetailUiState.Success) return
_placeDetail.value =
currentState.copy(
Expand Down

This file was deleted.

Loading