From c26a47fca5caca5390d343a5634ce64a2fd1a8d5 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:44:25 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[refactor]=20#196:=20=EC=82=AC=EC=A7=84?= =?UTF-8?q?=EB=B3=84=20memo=20=EB=AA=A8=EB=93=9C=20=EB=A7=B5=EC=9D=84=20?= =?UTF-8?q?=EC=A0=84=EC=97=AD=20=EB=8B=A8=EC=9D=BC=20memoMode=EB=A1=9C=20?= =?UTF-8?q?=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PhotoDetailState의 memoModes: Map 제거 - 단일 memoMode: MemoMode 필드로 대체 - currentMemoMode / memoModeOf 헬퍼 제거 - ViewModel의 메모 인텐트 처리 단순화 - 페이지 전환 시 memo 내용만 갱신하고 모드는 유지 --- .../impl/photo_detail/PhotoDetailContract.kt | 6 +----- .../impl/photo_detail/PhotoDetailScreen.kt | 11 +++++------ .../impl/photo_detail/PhotoDetailViewModel.kt | 17 ++++++----------- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailContract.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailContract.kt index 6d1b3d6a..707bc3f7 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailContract.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailContract.kt @@ -1,8 +1,6 @@ package com.neki.android.feature.archive.impl.photo_detail import com.neki.android.core.model.Photo -import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.persistentMapOf enum class MemoMode { Closed, @@ -18,12 +16,10 @@ data class PhotoDetailState( val isShowDeleteDialog: Boolean = false, val isShowOptionPopup: Boolean = false, val memo: String = "", - val memoModes: ImmutableMap = persistentMapOf(), + val memoMode: MemoMode = MemoMode.Closed, ) { val currentIndex get() = if (photos.isEmpty()) 0 else currentPage.coerceIn(0, photos.lastIndex) val photo: Photo get() = photos.getOrElse(currentIndex) { Photo() } - val currentMemoMode: MemoMode get() = memoModes[photo.id] ?: MemoMode.Closed - fun memoModeOf(photoId: Long): MemoMode = memoModes[photoId] ?: MemoMode.Closed } sealed interface PhotoDetailIntent { diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt index c92901c6..28f5fe0d 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt @@ -111,8 +111,8 @@ internal fun PhotoDetailScreen( onIntent: (PhotoDetailIntent) -> Unit = {}, pagerState: PagerState = rememberPagerState { uiState.photos.size.coerceAtLeast(1) }, ) { - val isMemoActive = uiState.currentMemoMode == MemoMode.Expanded || - uiState.currentMemoMode == MemoMode.Editing + val isMemoActive = uiState.memoMode == MemoMode.Expanded || + uiState.memoMode == MemoMode.Editing val density = LocalDensity.current var actionBarHeightDp by remember { mutableStateOf(0.dp) } @@ -139,17 +139,16 @@ internal fun PhotoDetailScreen( val index = if (uiState.photos.isEmpty()) 0 else page.coerceIn(0, uiState.photos.lastIndex) val photo = uiState.photos.getOrNull(index) - val pageMemoMode = uiState.memoModeOf(photo?.id ?: 0L) val pageMemo = if (index == uiState.currentIndex) uiState.memo else photo?.memo.orEmpty() PhotoDetailImageItem( imageUrl = photo?.imageUrl, memo = pageMemo, - memoMode = pageMemoMode, + memoMode = uiState.memoMode, actionBarHeight = actionBarHeightDp, isScrollInProgress = pagerState.isScrollInProgress, - isTapEnabled = pageMemoMode != MemoMode.Expanded && pageMemoMode != MemoMode.Editing, + isTapEnabled = !isMemoActive, onClickLeft = { onIntent(PhotoDetailIntent.ClickLeftPhoto) }, onClickRight = { onIntent(PhotoDetailIntent.ClickRightPhoto) }, onClickMemoMore = { onIntent(PhotoDetailIntent.ClickMemoMore) }, @@ -161,7 +160,7 @@ internal fun PhotoDetailScreen( ) } - if (uiState.currentMemoMode != MemoMode.Editing) { + if (uiState.memoMode != MemoMode.Editing) { PhotoDetailActionBar( modifier = Modifier.onSizeChanged { size -> actionBarHeightDp = with(density) { size.height.toDp() } diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailViewModel.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailViewModel.kt index 0570167f..beae5bff 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailViewModel.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailViewModel.kt @@ -11,7 +11,6 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableSharedFlow @@ -132,40 +131,36 @@ class PhotoDetailViewModel @AssistedInject constructor( // Memo Intent is PhotoDetailIntent.MemoTextChanged -> reduce { copy(memo = intent.text) } PhotoDetailIntent.ClickMemoIcon -> reduce { - val photoId = photo.id - val current = memoModeOf(photoId) - val newMode = if (current == MemoMode.Closed) MemoMode.Preview else MemoMode.Closed - copy(memoModes = (memoModes + (photoId to newMode)).toImmutableMap()) + copy(memoMode = if (memoMode == MemoMode.Closed) MemoMode.Preview else MemoMode.Closed) } PhotoDetailIntent.ClickMemoMore -> reduce { - copy(memoModes = (memoModes + (photo.id to MemoMode.Expanded)).toImmutableMap()) + copy(memoMode = MemoMode.Expanded) } PhotoDetailIntent.ClickMemoText -> reduce { - copy(memoModes = (memoModes + (photo.id to MemoMode.Editing)).toImmutableMap()) + copy(memoMode = MemoMode.Editing) } PhotoDetailIntent.ClickMemoFold -> reduce { copy( memo = photo.memo, - memoModes = (memoModes + (photo.id to MemoMode.Preview)).toImmutableMap(), + memoMode = MemoMode.Preview, ) } PhotoDetailIntent.ClickMemoCancel -> reduce { copy( memo = photo.memo, - memoModes = (memoModes + (photo.id to MemoMode.Preview)).toImmutableMap(), + memoMode = MemoMode.Preview, ) } is PhotoDetailIntent.ClickMemoDone -> { - val photoId = state.photo.id reduce { copy( memo = intent.memo, - memoModes = (memoModes + (photoId to MemoMode.Preview)).toImmutableMap(), + memoMode = MemoMode.Preview, ) } saveMemo(state.copy(memo = intent.memo), reduce, postSideEffect) From 619a29932d1e337ef2d36b6a9ec038d4c16c9f30 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:44:52 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[refactor]=20#196:=20=EB=A9=94=EB=AA=A8=20U?= =?UTF-8?q?I/Dim=20=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4=EB=A5=BC=20Horizon?= =?UTF-8?q?talPager=20=EB=B0=96=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PhotoDetailScreen에서 Pager/Dim/MemoTextField를 감싸는 PhotoDetailPagerArea 컴포저블 추출 - dim 오버레이와 MemoTextField를 각 페이지 안이 아닌 pager 밖 공용 Box에 배치 - PhotoDetailImageItem 시그니처 단순화 (memo/MemoMode/액션 핸들러 제거, 이미지 + 좌우 탭만 유지) - 메모 관련 UI가 화면 전역 단일 인스턴스로 동작하여 페이지 전환 시 모드는 유지되고 내용만 갱신 --- .../impl/photo_detail/PhotoDetailScreen.kt | 125 +++++++++++++----- .../component/PhotoDetailImageItem.kt | 70 ---------- 2 files changed, 93 insertions(+), 102 deletions(-) diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt index 28f5fe0d..41413c55 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt @@ -1,10 +1,22 @@ package com.neki.android.feature.archive.impl.photo_detail +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.spring +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState @@ -16,17 +28,19 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.foundation.layout.width -import androidx.compose.ui.Alignment import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.neki.android.core.designsystem.DevicePreview +import com.neki.android.core.designsystem.modifier.noRippleClickableSingle import com.neki.android.core.designsystem.topbar.BackTitleOptionTopBar import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.model.Photo @@ -37,6 +51,7 @@ import com.neki.android.core.ui.compose.collectWithLifecycle import com.neki.android.core.ui.toast.NekiToast import com.neki.android.feature.archive.api.PhotoDetailResult import com.neki.android.feature.archive.impl.component.DeletePhotoDialog +import com.neki.android.feature.archive.impl.photo_detail.component.MemoTextField import com.neki.android.feature.archive.impl.photo_detail.component.PhotoDetailActionBar import com.neki.android.feature.archive.impl.photo_detail.component.PhotoDetailImageItem import com.neki.android.feature.archive.impl.util.ImageDownloader @@ -111,8 +126,6 @@ internal fun PhotoDetailScreen( onIntent: (PhotoDetailIntent) -> Unit = {}, pagerState: PagerState = rememberPagerState { uiState.photos.size.coerceAtLeast(1) }, ) { - val isMemoActive = uiState.memoMode == MemoMode.Expanded || - uiState.memoMode == MemoMode.Editing val density = LocalDensity.current var actionBarHeightDp by remember { mutableStateOf(0.dp) } @@ -127,38 +140,16 @@ internal fun PhotoDetailScreen( onClickIcon = { onIntent(PhotoDetailIntent.ClickOptionIcon) }, ) - HorizontalPager( + PhotoDetailPagerArea( modifier = Modifier .fillMaxWidth() .weight(1f) .clipToBounds(), - state = pagerState, - beyondViewportPageCount = 1, - userScrollEnabled = !isMemoActive, - ) { page -> - val index = if (uiState.photos.isEmpty()) 0 else page.coerceIn(0, uiState.photos.lastIndex) - - val photo = uiState.photos.getOrNull(index) - val pageMemo = if (index == uiState.currentIndex) uiState.memo - else photo?.memo.orEmpty() - - PhotoDetailImageItem( - imageUrl = photo?.imageUrl, - memo = pageMemo, - memoMode = uiState.memoMode, - actionBarHeight = actionBarHeightDp, - isScrollInProgress = pagerState.isScrollInProgress, - isTapEnabled = !isMemoActive, - onClickLeft = { onIntent(PhotoDetailIntent.ClickLeftPhoto) }, - onClickRight = { onIntent(PhotoDetailIntent.ClickRightPhoto) }, - onClickMemoMore = { onIntent(PhotoDetailIntent.ClickMemoMore) }, - onClickMemoText = { onIntent(PhotoDetailIntent.ClickMemoText) }, - onClickMemoFold = { onIntent(PhotoDetailIntent.ClickMemoFold) }, - onClickMemoCancel = { onIntent(PhotoDetailIntent.ClickMemoCancel) }, - onClickMemoDone = { onIntent(PhotoDetailIntent.ClickMemoDone(it)) }, - onMemoTextChanged = { onIntent(PhotoDetailIntent.MemoTextChanged(it)) }, - ) - } + uiState = uiState, + actionBarHeightDp = actionBarHeightDp, + pagerState = pagerState, + onIntent = onIntent, + ) if (uiState.memoMode != MemoMode.Editing) { PhotoDetailActionBar( @@ -203,6 +194,76 @@ internal fun PhotoDetailScreen( } } +@Composable +private fun PhotoDetailPagerArea( + modifier: Modifier, + uiState: PhotoDetailState, + actionBarHeightDp: Dp, + pagerState: PagerState, + onIntent: (PhotoDetailIntent) -> Unit, +) { + val isMemoActive = uiState.memoMode == MemoMode.Expanded || + uiState.memoMode == MemoMode.Editing + + Box(modifier = modifier) { + HorizontalPager( + modifier = Modifier.fillMaxSize(), + state = pagerState, + beyondViewportPageCount = 1, + userScrollEnabled = !isMemoActive, + ) { page -> + val index = if (uiState.photos.isEmpty()) 0 else page.coerceIn(0, uiState.photos.lastIndex) + val photo = uiState.photos.getOrNull(index) + + PhotoDetailImageItem( + imageUrl = photo?.imageUrl, + isScrollInProgress = pagerState.isScrollInProgress, + isTapEnabled = !isMemoActive, + onClickLeft = { onIntent(PhotoDetailIntent.ClickLeftPhoto) }, + onClickRight = { onIntent(PhotoDetailIntent.ClickRightPhoto) }, + ) + } + + AnimatedVisibility( + visible = isMemoActive, + enter = fadeIn(), + exit = fadeOut(), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x80202227)) + .noRippleClickableSingle { onIntent(PhotoDetailIntent.ClickMemoFold) }, + ) + } + + AnimatedVisibility( + modifier = Modifier + .align(Alignment.BottomCenter) + .then( + if (uiState.memoMode == MemoMode.Editing) Modifier.imePadding() + else Modifier.windowInsetsPadding( + WindowInsets.ime.exclude(WindowInsets(bottom = actionBarHeightDp)), + ), + ), + visible = uiState.memoMode != MemoMode.Closed, + enter = expandVertically(expandFrom = Alignment.Top), + exit = shrinkVertically(shrinkTowards = Alignment.Top), + ) { + MemoTextField( + memo = uiState.memo, + memoMode = uiState.memoMode, + onClickMemoMore = { onIntent(PhotoDetailIntent.ClickMemoMore) }, + onClickMemoText = { onIntent(PhotoDetailIntent.ClickMemoText) }, + onClickMemoFold = { onIntent(PhotoDetailIntent.ClickMemoFold) }, + onClickMemoCancel = { onIntent(PhotoDetailIntent.ClickMemoCancel) }, + onClickMemoDone = { onIntent(PhotoDetailIntent.ClickMemoDone(it)) }, + onMemoTextChanged = { onIntent(PhotoDetailIntent.MemoTextChanged(it)) }, + ) + } + } +} + @DevicePreview @Composable private fun PhotoDetailScreenPreview() { diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailImageItem.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailImageItem.kt index 9c0725c2..a59423e7 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailImageItem.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailImageItem.kt @@ -1,35 +1,17 @@ package com.neki.android.feature.archive.impl.photo_detail.component -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.neki.android.core.designsystem.modifier.noRippleClickableSingle -import com.neki.android.feature.archive.impl.photo_detail.MemoMode import net.engawapg.lib.zoomable.ScrollGesturePropagation import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.zoomable @@ -37,22 +19,11 @@ import net.engawapg.lib.zoomable.zoomable @Composable internal fun PhotoDetailImageItem( imageUrl: String?, - memo: String, - memoMode: MemoMode, - actionBarHeight: Dp = 0.dp, isScrollInProgress: Boolean, isTapEnabled: Boolean, onClickLeft: () -> Unit, onClickRight: () -> Unit, - onClickMemoMore: () -> Unit, - onClickMemoText: () -> Unit, - onClickMemoFold: () -> Unit, - onClickMemoCancel: () -> Unit, - onClickMemoDone: (String) -> Unit, - onMemoTextChanged: (String) -> Unit, ) { - val isMemoActive = memoMode == MemoMode.Expanded || memoMode == MemoMode.Editing - val bottomPadding = if (memoMode == MemoMode.Editing) actionBarHeight else 0.dp val zoomState = rememberZoomState() var contentWidth by remember { mutableIntStateOf(0) } @@ -62,7 +33,6 @@ internal fun PhotoDetailImageItem( AsyncImage( modifier = Modifier .fillMaxSize() - .padding(bottom = bottomPadding) .onSizeChanged { contentWidth = it.width } .zoomable( zoomState = zoomState, @@ -86,45 +56,5 @@ internal fun PhotoDetailImageItem( contentScale = ContentScale.Fit, onSuccess = { state -> zoomState.setContentSize(state.painter.intrinsicSize) }, ) - - // dim 오버레이 - AnimatedVisibility( - visible = isMemoActive, - enter = fadeIn(), - exit = fadeOut(), - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color(0x80202227)) - .noRippleClickableSingle { onClickMemoFold() }, - ) - } - - // 메모 텍스트 영역 - AnimatedVisibility( - modifier = Modifier - .align(Alignment.BottomCenter) - .then( - if (memoMode == MemoMode.Editing) Modifier.imePadding() - else Modifier.windowInsetsPadding( - WindowInsets.ime.exclude(WindowInsets(bottom = actionBarHeight)), - ), - ), - visible = memoMode != MemoMode.Closed, - enter = expandVertically(expandFrom = Alignment.Top), - exit = shrinkVertically(shrinkTowards = Alignment.Top), - ) { - MemoTextField( - memo = memo, - memoMode = memoMode, - onClickMemoMore = onClickMemoMore, - onClickMemoText = onClickMemoText, - onClickMemoFold = onClickMemoFold, - onClickMemoCancel = onClickMemoCancel, - onClickMemoDone = onClickMemoDone, - onMemoTextChanged = onMemoTextChanged, - ) - } } } From afb97a724c695362f6d3eeec2b136271cbcec079 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:02:44 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[refactor]=20#196:=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=8A=A4=EC=99=80=EC=9D=B4=ED=94=84=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=20=EC=8B=9C=EC=A0=90=EC=97=90=20=EB=A9=94=EB=AA=A8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20Closed=EB=A1=9C=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PhotoDetailIntent.PageScrollStarted 인텐트 추가 - PhotoDetailRoute에서 pagerState.isScrollInProgress 관찰 LaunchedEffect 추가 - 스와이프가 시작되는 순간 memoMode를 Closed로 전환 (settle까지 기다리지 않음) - 다음 사진의 메모를 보려면 메모 아이콘을 다시 탭해야 함 --- .../archive/impl/photo_detail/PhotoDetailContract.kt | 1 + .../archive/impl/photo_detail/PhotoDetailScreen.kt | 9 +++++++++ .../archive/impl/photo_detail/PhotoDetailViewModel.kt | 2 ++ 3 files changed, 12 insertions(+) diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailContract.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailContract.kt index 707bc3f7..e382f6da 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailContract.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailContract.kt @@ -33,6 +33,7 @@ sealed interface PhotoDetailIntent { data object ClickLeftPhoto : PhotoDetailIntent data object ClickRightPhoto : PhotoDetailIntent data class PageChanged(val page: Int) : PhotoDetailIntent + data object PageScrollStarted : PhotoDetailIntent // ActionBar Intent data object ClickDownloadIcon : PhotoDetailIntent diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt index 41413c55..ae12c4e2 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt @@ -56,6 +56,7 @@ import com.neki.android.feature.archive.impl.photo_detail.component.PhotoDetailA import com.neki.android.feature.archive.impl.photo_detail.component.PhotoDetailImageItem import com.neki.android.feature.archive.impl.util.ImageDownloader import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import timber.log.Timber @@ -81,6 +82,14 @@ internal fun PhotoDetailRoute( } } + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.isScrollInProgress } + .filter { it } + .collect { + viewModel.store.onIntent(PhotoDetailIntent.PageScrollStarted) + } + } + viewModel.store.sideEffects.collectWithLifecycle { sideEffect -> when (sideEffect) { PhotoDetailSideEffect.NavigateBack -> navigateBack() diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailViewModel.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailViewModel.kt index beae5bff..535631ab 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailViewModel.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailViewModel.kt @@ -110,6 +110,8 @@ class PhotoDetailViewModel @AssistedInject constructor( preloadIfNeeded(reduce) } + PhotoDetailIntent.PageScrollStarted -> reduce { copy(memoMode = MemoMode.Closed) } + // ActionBar Intent PhotoDetailIntent.ClickDownloadIcon -> postSideEffect(PhotoDetailSideEffect.DownloadImage(state.photo.imageUrl)) PhotoDetailIntent.ClickFavoriteIcon -> handleFavoriteToggle(state, reduce) From 612b85a1a59541f2aad113408b7346f47e34f7ee Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:47:01 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[fix]=20#196:=20Editing=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=98=81=EC=97=AD=EC=9D=B4=20=EC=BB=A4=EC=A7=80=EB=8A=94=20?= =?UTF-8?q?=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PhotoDetailPagerArea의 HorizontalPager에 Editing 시 bottom padding 추가 - ActionBar가 숨겨질 때 pager 영역이 그만큼 확장되어 이미지가 커 보이던 문제 해결 --- .../feature/archive/impl/photo_detail/PhotoDetailScreen.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt index ae12c4e2..f5f9f914 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.pager.HorizontalPager @@ -216,7 +217,9 @@ private fun PhotoDetailPagerArea( Box(modifier = modifier) { HorizontalPager( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(bottom = if (uiState.memoMode == MemoMode.Editing) actionBarHeightDp else 0.dp), state = pagerState, beyondViewportPageCount = 1, userScrollEnabled = !isMemoActive, From 504aac179fbc350f4916866e659a43b0c19657df Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:00:10 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[feat]=20#196:=20PhotoDetail=20=EB=A9=94?= =?UTF-8?q?=EB=AA=A8=20=EC=9E=91=EC=84=B1=20=EC=97=AC=EB=B6=80=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EB=B6=84?= =?UTF-8?q?=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../archive/impl/photo_detail/PhotoDetailScreen.kt | 1 + .../impl/photo_detail/component/PhotoDetailActionBar.kt | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt index f5f9f914..50f328c0 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt @@ -167,6 +167,7 @@ internal fun PhotoDetailScreen( actionBarHeightDp = with(density) { size.height.toDp() } }, isFavorite = uiState.photo.isFavorite, + hasMemo = uiState.photo.memo.isNotEmpty(), onClickDownload = { onIntent(PhotoDetailIntent.ClickDownloadIcon) }, onClickFavorite = { onIntent(PhotoDetailIntent.ClickFavoriteIcon) }, onClickMemo = { onIntent(PhotoDetailIntent.ClickMemoIcon) }, diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailActionBar.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailActionBar.kt index d9a60059..5a680435 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailActionBar.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailActionBar.kt @@ -19,6 +19,7 @@ import com.neki.android.core.designsystem.ui.theme.NekiTheme @Composable internal fun PhotoDetailActionBar( isFavorite: Boolean, + hasMemo: Boolean, modifier: Modifier = Modifier, onClickDownload: () -> Unit = {}, onClickFavorite: () -> Unit = {}, @@ -61,7 +62,10 @@ internal fun PhotoDetailActionBar( ) { Icon( modifier = Modifier.size(28.dp), - imageVector = ImageVector.vectorResource(R.drawable.icon_memo), + imageVector = ImageVector.vectorResource( + if (hasMemo) R.drawable.icon_memo_filled + else R.drawable.icon_memo_stroked, + ), contentDescription = null, tint = NekiTheme.colorScheme.gray700, ) @@ -88,6 +92,7 @@ private fun PhotoDetailActionBarClosedPreview() { NekiTheme { PhotoDetailActionBar( isFavorite = false, + hasMemo = false, ) } } @@ -98,6 +103,7 @@ private fun PhotoDetailActionBarMemoActivePreview() { NekiTheme { PhotoDetailActionBar( isFavorite = true, + hasMemo = true, ) } } From 34a743749b2f456189a01275702e68d2b73e609b Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:07:09 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[chore]=20#196:=20=EB=A9=94=EB=AA=A8=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=EB=A6=AC=EC=86=8C=EC=8A=A4(fille?= =?UTF-8?q?d/stroked)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PhotoDetailActionBar에서 참조하는 icon_memo_filled, icon_memo_stroked 드로어블 커밋 누락 수정 - CI 빌드 실패 해결 --- .../main/res/drawable/icon_memo_filled.xml | 12 ++++++++ .../main/res/drawable/icon_memo_stroked.xml | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 core/designsystem/src/main/res/drawable/icon_memo_filled.xml create mode 100644 core/designsystem/src/main/res/drawable/icon_memo_stroked.xml diff --git a/core/designsystem/src/main/res/drawable/icon_memo_filled.xml b/core/designsystem/src/main/res/drawable/icon_memo_filled.xml new file mode 100644 index 00000000..6d098d03 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/icon_memo_filled.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/designsystem/src/main/res/drawable/icon_memo_stroked.xml b/core/designsystem/src/main/res/drawable/icon_memo_stroked.xml new file mode 100644 index 00000000..efc422c5 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/icon_memo_stroked.xml @@ -0,0 +1,29 @@ + + + + + + +