Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3cad7c1
:sparkles: :: 티켓 오픈 구조 선작업
Hyeonseo4799 Jul 30, 2026
280f496
:sparkles: :: 티켓 오픈 인터렉션 및 애니메이션 작업
Hyeonseo4799 Aug 4, 2026
93e9cb6
:green_heart: :: CI 빌드 실패 수정
Hyeonseo4799 Aug 4, 2026
de33016
:memo: :: stability 파일 변경
Hyeonseo4799 Aug 4, 2026
ac0679a
:sparkles: :: 티켓 이미지 설정
Hyeonseo4799 Aug 18, 2026
dfbff8f
Merge branch 'develop' into feature/297_ticket_open
Hyeonseo4799 Aug 18, 2026
e402ead
:sparkles: :: 디졸브 애니메이션 및 티켓 이미지 설정 수정사항
Hyeonseo4799 Aug 18, 2026
143da18
:memo: :: stability 파일 반영
Hyeonseo4799 Aug 18, 2026
15ba873
Merge branch 'develop' into feature/297_ticket_open
Hyeonseo4799 Aug 31, 2026
3d72f43
:feat: :: 푸시 진입을 통한 애니메이션 노출 로직 적용
Hyeonseo4799 Aug 31, 2026
9034d5e
:sparkles: :: 클릭 가드 로직 및 오픈 이력 저장 로직 추가
Hyeonseo4799 Aug 31, 2026
4e0dc03
:bug: :: 캡슐 오픈 이력 로컬 저장 버그 수정
Hyeonseo4799 Sep 1, 2026
6c2d01c
:memo: :: stability 파일 업데이트
Hyeonseo4799 Sep 1, 2026
d364f63
:art: :: 구조 수정
Hyeonseo4799 Sep 16, 2026
8e28ce7
Merge branch 'develop' into feature/297_ticket_open
Hyeonseo4799 Sep 16, 2026
c5f4233
:bug: :: 병합 후 코드 수정
Hyeonseo4799 Sep 16, 2026
ac54389
:art: :: 개행 수정
Hyeonseo4799 Sep 16, 2026
46ff382
:memo: :: stability 파일 추가
Hyeonseo4799 Sep 16, 2026
053c2b0
:memo: :: stability 파일 수정
Hyeonseo4799 Sep 16, 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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies {
implementation(project(":feature:preview"))
implementation(project(":feature:memory"))
implementation(project(":feature:watering"))
implementation(project(":feature:open"))
implementation(project(":core:designsystem"))
implementation(project(":core:navigation"))
implementation(project(":core:util"))
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/idiotfrogs/memoryseal/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.idiotfrogs.message.MessageRoute
import com.idiotfrogs.navigation.LocalComposeMSNavigator
import com.idiotfrogs.navigation.MSNavigatorImpl
import com.idiotfrogs.navigation.Routes
import com.idiotfrogs.open.OpenRoute
import com.idiotfrogs.preview.PreviewRoute
import com.idiotfrogs.profile.editprofile.EditProfileRoute
import com.idiotfrogs.profile.profile.ProfileRoute
Expand Down Expand Up @@ -150,6 +151,7 @@ class MainActivity : ComponentActivity() {
}
entry<Routes.Watering> { WateringRoute(capsuleId = it.id) }
entry<Routes.WateringDetail> { WateringDetailRoute(capsuleId = it.id) }
entry<Routes.Open> { OpenRoute(capsuleId = it.id) }
},
)
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions common/resource/src/main/res/raw/ticket_open.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ interface LocalDataSource {
val refreshToken: Flow<String>
val accessTokenExpiresIn: Flow<Long>

val capsuleIds: Flow<Set<String>>

suspend fun setTokens(
accessToken: String,
refreshToken: String,
accessTokenExpiresIn: Long,
)

suspend fun clearTokens()

suspend fun addCapsuleId(capsuleId: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
Expand All @@ -17,6 +18,8 @@ class LocalDataSourceImpl @Inject constructor(
private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")

private val ACCESS_TOKEN_EXPIRES_IN_KEY = longPreferencesKey("access_token_expires_in")

private val CAPSULE_ID_KEY = stringSetPreferencesKey("capsule_id")
}

override val accessToken: Flow<String> =
Expand All @@ -34,6 +37,11 @@ class LocalDataSourceImpl @Inject constructor(
preferences[ACCESS_TOKEN_EXPIRES_IN_KEY] ?: 0L
}

override val capsuleIds: Flow<Set<String>> =
dataStore.data.map { preferences ->
preferences[CAPSULE_ID_KEY] ?: emptySet()
}

override suspend fun setTokens(
accessToken: String,
refreshToken: String,
Expand All @@ -48,7 +56,16 @@ class LocalDataSourceImpl @Inject constructor(

override suspend fun clearTokens() {
dataStore.edit { preferences ->
preferences.clear()
preferences.remove(ACCESS_TOKEN_KEY)
preferences.remove(REFRESH_TOKEN_KEY)
preferences.remove(ACCESS_TOKEN_EXPIRES_IN_KEY)
}
}

override suspend fun addCapsuleId(capsuleId: String) {
dataStore.edit { preferences ->
preferences[CAPSULE_ID_KEY] =
(preferences[CAPSULE_ID_KEY] ?: emptySet()) + capsuleId
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ sealed interface Routes: NavKey {
data class Watering(val id: Long) : Routes
@Serializable
data class WateringDetail(val id: Long) : Routes
@Serializable
data class Open(val id: Long) : Routes
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.idiotfrogs.data.repository.local

import com.idiotfrogs.local.LocalDataSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class LocalRepositoryImpl @Inject constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ class GetMyTimeCapsuleUseCase @Inject constructor(
) {
suspend operator fun invoke(): Result<Map<TimeCapsuleRole, List<MyTimeCapsuleResponse>>> =
safeCatching {
timeCapsuleRepository.getMyTimeCapsule()
.filter { it.timeCapsuleStatus != TimeCapsuleStatus.OPENED }
.groupBy { it.role }
timeCapsuleRepository.getMyTimeCapsule().groupBy { it.role }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.idiotfrogs.domain.usecase.timecapsule

import com.idiotfrogs.data.repository.timecapsule.TimeCapsuleRepository
import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse
import com.idiotfrogs.model.timecapsule.TimeCapsuleRole
import com.idiotfrogs.model.timecapsule.TimeCapsuleStatus
import com.idiotfrogs.util.safeCatching
import javax.inject.Inject

class GetOpenedMyTimeCapsuleUseCase @Inject constructor(
private val timeCapsuleRepository: TimeCapsuleRepository
) {
suspend operator fun invoke(): Result<Map<TimeCapsuleRole, List<MyTimeCapsuleResponse>>> =
safeCatching {
timeCapsuleRepository.getMyTimeCapsule()
.filter { it.timeCapsuleStatus == TimeCapsuleStatus.OPENED }
.groupBy { it.role }
}
}
7 changes: 5 additions & 2 deletions feature/home/src/main/java/com/idiotfrogs/home/HomeScreen.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.idiotfrogs.home

import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
Expand Down Expand Up @@ -29,6 +30,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
Expand Down Expand Up @@ -69,7 +71,7 @@ import org.orbitmvi.orbit.compose.collectSideEffect

@Composable
fun HomeRoute(
viewModel: HomeViewModel = hiltViewModel()
viewModel: HomeViewModel = hiltViewModel(),
) {
val navigator = LocalComposeMSNavigator.current
val uiState by viewModel.collectAsState()
Expand All @@ -87,6 +89,7 @@ fun HomeRoute(
HomeSideEffect.NavigateToProfile -> navigator.navigate(Routes.Profile)
is HomeSideEffect.NavigateToDetail -> navigator.navigate(Routes.Detail(it.id))
HomeSideEffect.ShowToast -> showToast = true
is HomeSideEffect.ShowOpenAnimation -> navigator.navigate(Routes.Open(it.id))
}
}

Expand Down Expand Up @@ -261,7 +264,7 @@ fun HomeScreen(
items(data) {
HomeTicket(
modifier = Modifier.noRippleClickable {
onAction(HomeAction.TimeCapsuleClicked(it.timeCapsuleId))
onAction.invoke(HomeAction.TimeCapsuleClicked(it.timeCapsuleId))
},
buried = it.timeCapsuleStatus == TimeCapsuleStatus.BURIED,
createdAt = it.createdAt.toYearMonthDay(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class HomeViewModel @Inject constructor(
private val requestCollaboratorUseCase: RequestCollaboratorUseCase,
private val fcmTokenProvider: FcmTokenProvider,
private val putFcmTokenUseCase: PutFcmTokenUseCase,
): BaseViewModel<HomeUiState, HomeSideEffect, HomeAction>() {
) : BaseViewModel<HomeUiState, HomeSideEffect, HomeAction>() {

override val container: Container<HomeUiState, HomeSideEffect> = container(
initialState = HomeUiState(),
Expand Down Expand Up @@ -100,9 +100,9 @@ class HomeViewModel @Inject constructor(
when (action) {
HomeAction.CreateClicked -> postSideEffect(HomeSideEffect.NavigateToCreate)
HomeAction.ProfileClicked -> postSideEffect(HomeSideEffect.NavigateToProfile)
is HomeAction.TimeCapsuleClicked -> postSideEffect(HomeSideEffect.NavigateToDetail(action.id))
is HomeAction.JoinCodeSubmitted -> requestCollaborator(PendingCollaboratorsRequest(action.code))
HomeAction.Refresh -> fetchHome()
is HomeAction.TimeCapsuleClicked -> intent { postSideEffect(HomeSideEffect.NavigateToDetail(action.id)) }
}
}
}
Expand All @@ -124,8 +124,8 @@ data class HomeData(
sealed interface HomeAction {
data object CreateClicked : HomeAction
data object ProfileClicked : HomeAction
data class TimeCapsuleClicked(val id: Long) : HomeAction
data class JoinCodeSubmitted(val code: String) : HomeAction
data class TimeCapsuleClicked(val id: Long) : HomeAction
data object Refresh : HomeAction
}

Expand All @@ -135,4 +135,5 @@ sealed interface HomeSideEffect {
data class NavigateToDetail(val id: Long) : HomeSideEffect

data object ShowToast : HomeSideEffect
data class ShowOpenAnimation(val id: Long) : HomeSideEffect
}
1 change: 1 addition & 0 deletions feature/open/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
19 changes: 19 additions & 0 deletions feature/open/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
id("convention.android.feature")
}

android {
namespace = "com.idiotfrogs.open"
}

dependencies {
implementation(project(":common:extension"))

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext)
androidTestImplementation(libs.androidx.test.espresso)

implementation(libs.lottie.compose)
}
Empty file added feature/open/consumer-rules.pro
Empty file.
21 changes: 21 additions & 0 deletions feature/open/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.idiotfrogs.open

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.idiotfrogs.open.test", appContext.packageName)
}
}
4 changes: 4 additions & 0 deletions feature/open/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
99 changes: 99 additions & 0 deletions feature/open/src/main/java/com/idiotfrogs/open/OpenScreen.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.idiotfrogs.open

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import com.idiotfrogs.designsystem.util.noRippleClickable
import com.idiotfrogs.navigation.LocalComposeMSNavigator
import com.idiotfrogs.navigation.Routes
import com.idiotfrogs.open.component.OpenAnimation
import com.idiotfrogs.open.component.OpenInteraction
import com.idiotfrogs.resource.R
import org.orbitmvi.orbit.compose.collectAsState
import org.orbitmvi.orbit.compose.collectSideEffect

enum class OpenStep { INTERACTION, ANIMATION }

@Composable
fun OpenRoute(
capsuleId: Long,
viewModel: OpenViewModel = hiltViewModel<OpenViewModel, OpenViewModel.Factory>(key = capsuleId.toString()) { it.create(capsuleId) },
) {
val navigator = LocalComposeMSNavigator.current
val uiState by viewModel.collectAsState()

viewModel.collectSideEffect { event ->
when (event) {
is OpenSideEffect.NavigateToDetail -> navigator.navigate(Routes.Detail(event.capsuleId))
}
}

uiState.data?.let { data ->
OpenedScreen(
data = data,
onAction = viewModel::onAction
)
}
}

@Composable
fun OpenedScreen(
data: OpenData,
onAction: (OpenAction) -> Unit,
) {
var currentOpenStep by remember { mutableStateOf(OpenStep.INTERACTION) }
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.ticket_open))
val ready = composition != null
val visibleState = remember { MutableTransitionState(false) }

LaunchedEffect(currentOpenStep, ready) {
if (currentOpenStep == OpenStep.ANIMATION && ready) {
visibleState.targetState = true
}
}

when (currentOpenStep) {
OpenStep.INTERACTION -> {
OpenInteraction(
image = data.imageUrl,
onFinish = { currentOpenStep = OpenStep.ANIMATION}
)
}
// 가드 조건(if) - 매칭된 조건에 대한 추가 검사 제공
OpenStep.ANIMATION if ready -> {
Box(
modifier = Modifier
.noRippleClickable { /** no-op */ }
.fillMaxSize()
.background(Color.White)
) {
AnimatedVisibility(
visibleState = visibleState,
enter = fadeIn(tween(500))
) {
OpenAnimation(
image = data.imageUrl,
composition = { composition!! }, // 위에서 체크해서 non-null, 람다를 통한 지연 읽기
confirmClick = { onAction.invoke(OpenAction.DoneClick) }
)
}
}
}
else -> Unit
}
}
Loading
Loading