diff --git a/.github/ISSUE_TEMPLATE/issue-form.yml b/.github/ISSUE_TEMPLATE/issue-form.yml
index 0352a42ab..48bf08c60 100644
--- a/.github/ISSUE_TEMPLATE/issue-form.yml
+++ b/.github/ISSUE_TEMPLATE/issue-form.yml
@@ -1,16 +1,7 @@
name: '이슈 생성'
-description: '이슈 생성과 동시에 Jira와 연동됩니다.'
+description: '새 이슈를 생성합니다'
title: '이슈 제목'
body:
- - type: input
- id: epicId
- attributes:
- label: '💎 에픽 ID'
- description: '에픽 ID를 입력해주세요'
- placeholder: 'KAN-1'
- validations:
- required: true
-
- type: input
id: dueDate
attributes:
diff --git a/README.md b/README.md
index 15c33757e..0c718480a 100644
--- a/README.md
+++ b/README.md
@@ -3,42 +3,53 @@
-
+
+ PlayStore ·
+ About us ·
+ Previous Repository
+
No More Research
Acon

-
-
-
-
-
-
-
+
+
+ | 홈 |
+ 리뷰 등록 |
+ 북마크 및 장소상세 |
+ 장소 업로드 |
+
+
+  |
+  |
+  |
+  |
+
+
---
## ⚙️ Teck Stacks
-| 분류 | 사용 기술 |
-|------|------------|
-| Language | Kotlin |
-| UI | Jetpack Compose, Material 3 |
+| 분류 | 사용 기술 |
+|------|---------------------------------------------|
+| Language | Kotlin |
+| UI | Jetpack Compose, Material 3 |
| Architecture | MVI Orbit, Multi-module, Clean Architecture |
-| DI | Hilt |
-| Network | Retrofit, OkHttp |
-| CI/CD | GitHub Actions, Firebase App Distribution |
-| Etc | Play-services, Amplitude, Haze |
+| DI | Hilt |
+| Network | Retrofit, OkHttp |
+| CI/CD | GitHub Actions, Firebase App Distribution |
+| Etc | Play-services, Amplitude, Haze, Branch.io |
---
## 🙌 Contributors
-[@Thirfir](https://github.com/ThirFir)
+[@Thirfir](https://github.com/ThirFir) (2025.1 ~ )
-[@1971123-seongmin](https://github.com/1971123-seongmin)
+[@1971123-seongmin](https://github.com/1971123-seongmin) (2025.1 ~ )
-[@tunaunnie](https://github.com/tunaunnie)
+[@tunaunnie](https://github.com/tunaunnie) (2025.1 ~ 2025.2)
-[@0se0](https://github.com/0se0)
+[@0se0](https://github.com/0se0) (2025.1 ~ 2025.2)
diff --git a/core/designsystem/src/main/java/com/acon/acon/core/designsystem/animation/SlideUpAnimation.kt b/core/designsystem/src/main/java/com/acon/acon/core/designsystem/animation/SlideUpAnimation.kt
new file mode 100644
index 000000000..affc1e900
--- /dev/null
+++ b/core/designsystem/src/main/java/com/acon/acon/core/designsystem/animation/SlideUpAnimation.kt
@@ -0,0 +1,86 @@
+package com.acon.acon.core.designsystem.animation
+
+import android.annotation.SuppressLint
+import androidx.compose.animation.core.Easing
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.offset
+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.composed
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.acon.acon.core.designsystem.R
+import kotlinx.coroutines.delay
+
+@SuppressLint("UseOfNonLambdaOffsetOverload")
+@Composable
+fun Modifier.slideUpAnimation(
+ order: Int = 1,
+ delay: Int = 0,
+ duration: Int = 500,
+ easing: Easing = LinearOutSlowInEasing,
+ yOffset: Dp = 100.dp,
+ titleDelay: Int = 300,
+ contentDelay: Int = 600,
+ hasCaption: Boolean = false,
+ onAnimationEnded: () -> Unit = {}
+): Modifier = composed {
+ val animationDelay = remember(order, hasCaption) {
+ when (order) {
+ 1 -> delay
+ 2 -> delay + titleDelay
+ 3 -> if (hasCaption) delay + titleDelay * 2 else 0
+ 4 -> if (hasCaption) {
+ delay + titleDelay * 2 + contentDelay
+ } else {
+ delay + titleDelay + contentDelay
+ }
+ else -> delay
+ }
+ }
+
+ var visible by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ delay(animationDelay.toLong())
+ visible = true
+ }
+
+ val offsetY by animateDpAsState(
+ targetValue = if (visible) 0.dp else yOffset,
+ animationSpec = tween(
+ durationMillis = duration,
+ easing = easing
+ ),
+ label = stringResource(R.string.slide_up_offset)
+ )
+
+ val alpha by animateFloatAsState(
+ targetValue = if (visible) 1f else 0f,
+ animationSpec = tween(
+ durationMillis = duration,
+ easing = easing
+ ),
+ label = stringResource(R.string.slide_up_alpha)
+ )
+
+ LaunchedEffect(offsetY) {
+ if (offsetY == 0.dp) {
+ onAnimationEnded()
+ }
+ }
+
+ Modifier
+ .offset(y = offsetY)
+ .alpha(alpha)
+}
\ No newline at end of file
diff --git a/core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/dialog/v2/AconTwoActionDialog.kt b/core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/dialog/v2/AconTwoActionDialog.kt
index 3f1e8350d..67d033250 100644
--- a/core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/dialog/v2/AconTwoActionDialog.kt
+++ b/core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/dialog/v2/AconTwoActionDialog.kt
@@ -34,6 +34,7 @@ fun AconTwoActionDialog(
onAction1: () -> Unit,
onAction2: () -> Unit,
onDismissRequest: () -> Unit,
+ isTextAlign: Boolean = false,
action1Color: Color = AconTheme.color.White,
action2Color: Color = AconTheme.color.Action,
modifier: Modifier = Modifier,
@@ -65,6 +66,7 @@ fun AconTwoActionDialog(
style = AconTheme.typography.Title4,
fontWeight = FontWeight.SemiBold,
color = AconTheme.color.White,
+ textAlign = if (isTextAlign) TextAlign.Center else TextAlign.Unspecified,
modifier = Modifier
.padding(top = 24.dp, bottom = 22.dp)
.padding(horizontal = 16.dp)
diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml
index f2fc56751..d5b7284c4 100644
--- a/core/designsystem/src/main/res/values/strings.xml
+++ b/core/designsystem/src/main/res/values/strings.xml
@@ -12,6 +12,7 @@
선택
제출하기
끝내기
+ 그만두기
제보하기
계속 작성
다음
@@ -31,6 +32,10 @@
다음에 하기
홈으로 가기
+
+ slide_up_offset
+ slide_up_alpha
+
마무리 하기
업데이트
@@ -212,7 +217,7 @@
upload_process_step_transition
- 등록을 취소하시겠습니까?
+ 여기서 그만두면\n작성중인 내용이 사라져요
장소 등록
필수 입력
선택 입력
diff --git a/core/model/src/main/java/com/acon/acon/core/model/type/FeatureType.kt b/core/model/src/main/java/com/acon/acon/core/model/type/FeatureType.kt
index bd553a6f1..228bead6f 100644
--- a/core/model/src/main/java/com/acon/acon/core/model/type/FeatureType.kt
+++ b/core/model/src/main/java/com/acon/acon/core/model/type/FeatureType.kt
@@ -29,7 +29,7 @@ sealed interface RestaurantFeatureType : FeatureType {
sealed interface CafeFeatureType : FeatureType {
enum class CafeType: FeatureType {
- GOOD_FOR_WORK,
+ WORK_FRIENDLY,
NOT_GOOD_FOR_WORK;
}
}
diff --git a/data/src/main/kotlin/com/acon/acon/data/api/remote/noauth/SpotNoAuthApi.kt b/data/src/main/kotlin/com/acon/acon/data/api/remote/noauth/SpotNoAuthApi.kt
index 24e7b997b..0e1125e65 100644
--- a/data/src/main/kotlin/com/acon/acon/data/api/remote/noauth/SpotNoAuthApi.kt
+++ b/data/src/main/kotlin/com/acon/acon/data/api/remote/noauth/SpotNoAuthApi.kt
@@ -1,8 +1,10 @@
package com.acon.acon.data.api.remote.noauth
import com.acon.acon.data.dto.request.RecentNavigationLocationRequest
+import com.acon.acon.data.dto.request.SpotListRequest
import com.acon.acon.data.dto.response.MenuBoardListResponse
import com.acon.acon.data.dto.response.SpotDetailResponse
+import com.acon.acon.data.dto.response.SpotListResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
@@ -11,6 +13,11 @@ import retrofit2.http.Query
interface SpotNoAuthApi {
+ @POST("/api/v1/spots")
+ suspend fun fetchSpotList(
+ @Body request: SpotListRequest
+ ): SpotListResponse
+
@POST("/api/v1/guided-spots")
suspend fun fetchRecentNavigationLocation(
@Body request: RecentNavigationLocationRequest
diff --git a/data/src/main/kotlin/com/acon/acon/data/datasource/remote/SpotRemoteDataSource.kt b/data/src/main/kotlin/com/acon/acon/data/datasource/remote/SpotRemoteDataSource.kt
index d79bc6311..8249b70d1 100644
--- a/data/src/main/kotlin/com/acon/acon/data/datasource/remote/SpotRemoteDataSource.kt
+++ b/data/src/main/kotlin/com/acon/acon/data/datasource/remote/SpotRemoteDataSource.kt
@@ -1,5 +1,6 @@
package com.acon.acon.data.datasource.remote
+import com.acon.acon.core.model.type.UserType
import com.acon.acon.data.api.remote.auth.SpotAuthApi
import com.acon.acon.data.api.remote.noauth.SpotNoAuthApi
import com.acon.acon.data.dto.request.AddBookmarkRequest
@@ -16,8 +17,10 @@ class SpotRemoteDataSource @Inject constructor(
private val spotAuthApi: SpotAuthApi
) {
- suspend fun fetchSpotList(request: SpotListRequest): SpotListResponse {
- return spotAuthApi.fetchSpotList(request)
+ suspend fun fetchSpotList(request: SpotListRequest, userType: UserType): SpotListResponse {
+ return if (userType == UserType.GUEST)
+ spotNoAuthApi.fetchSpotList(request)
+ else spotAuthApi.fetchSpotList(request)
}
suspend fun fetchRecentNavigationLocation(request: RecentNavigationLocationRequest) {
diff --git a/data/src/main/kotlin/com/acon/acon/data/repository/SpotRepositoryImpl.kt b/data/src/main/kotlin/com/acon/acon/data/repository/SpotRepositoryImpl.kt
index 306e3ad47..3ee4843cc 100644
--- a/data/src/main/kotlin/com/acon/acon/data/repository/SpotRepositoryImpl.kt
+++ b/data/src/main/kotlin/com/acon/acon/data/repository/SpotRepositoryImpl.kt
@@ -13,6 +13,7 @@ import com.acon.acon.data.dto.request.FilterListRequest
import com.acon.acon.data.dto.request.RecentNavigationLocationRequest
import com.acon.acon.data.dto.request.SpotListRequest
import com.acon.acon.data.error.runCatchingWith
+import com.acon.acon.data.session.SessionHandler
import com.acon.acon.domain.error.spot.AddBookmarkError
import com.acon.acon.domain.error.spot.DeleteBookmarkError
import com.acon.acon.domain.error.spot.FetchMenuBoardsError
@@ -21,12 +22,14 @@ import com.acon.acon.domain.error.spot.FetchSpotListError
import com.acon.acon.domain.error.spot.GetSpotDetailInfoError
import com.acon.acon.domain.repository.ProfileRepository
import com.acon.acon.domain.repository.SpotRepository
+import kotlinx.coroutines.flow.first
import javax.inject.Inject
class SpotRepositoryImpl @Inject constructor(
private val spotRemoteDataSource: SpotRemoteDataSource,
private val profileInfoCache: ProfileInfoCache,
- private val profileRepository: ProfileRepository
+ private val profileRepository: ProfileRepository,
+ private val sessionHandler: SessionHandler
) : SpotRepository {
override suspend fun fetchSpotList(
@@ -48,7 +51,7 @@ class SpotRepositoryImpl @Inject constructor(
)
}
),
- )
+ ), sessionHandler.getUserType().first()
).toSpotList()
}
}
diff --git a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/UploadPlaceViewModel.kt b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/UploadPlaceViewModel.kt
index e3e865e43..e728bdd76 100644
--- a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/UploadPlaceViewModel.kt
+++ b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/UploadPlaceViewModel.kt
@@ -16,6 +16,7 @@ import com.acon.acon.domain.repository.MapSearchRepository
import com.acon.acon.domain.repository.UploadRepository
import com.acon.acon.feature.upload.BuildConfig
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@@ -25,6 +26,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okhttp3.ConnectionPool
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -33,6 +36,7 @@ import okhttp3.internal.toImmutableList
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.viewmodel.container
import timber.log.Timber
+import java.util.concurrent.TimeUnit
import javax.inject.Inject
@OptIn(FlowPreview::class)
@@ -150,11 +154,7 @@ class UploadPlaceViewModel @Inject constructor(
}
fun updateCafeOptionType(cafeOption: CafeFeatureType.CafeType) = intent {
- if(cafeOption == CafeFeatureType.CafeType.NOT_GOOD_FOR_WORK) {
- reduce { state.copy(selectedCafeOption = null) }
- } else {
- reduce { state.copy(selectedCafeOption = cafeOption) }
- }
+ reduce { state.copy(selectedFeature = SelectedFeature.Cafe(cafeOption)) }
}
fun updatePriceOptionType(priceOption: PriceFeatureType.PriceOptionType) = intent {
@@ -163,14 +163,14 @@ class UploadPlaceViewModel @Inject constructor(
fun updateRestaurantType(type: RestaurantFeatureType.RestaurantType) = intent {
reduce {
- val currentSelectedTypes = state.selectedRestaurantTypes.toMutableList()
+ val currentSelectedTypes = (state.selectedFeature as? SelectedFeature.Restaurant)?.types?.toMutableList() ?: mutableListOf()
if (currentSelectedTypes.contains(type)) {
currentSelectedTypes.remove(type)
} else {
currentSelectedTypes.add(type)
}
- state.copy(selectedRestaurantTypes = currentSelectedTypes)
+ state.copy(selectedFeature = SelectedFeature.Restaurant(currentSelectedTypes))
}
}
@@ -270,6 +270,13 @@ class UploadPlaceViewModel @Inject constructor(
}
}
+ fun onSlideAnimationEnd(route: String) = intent {
+ reduce {
+ val updatedMap = state.hasAnimated.toMutableMap().apply { this[route] = true }
+ state.copy(hasAnimated = updatedMap)
+ }
+ }
+
fun onNavigateToBack() = intent {
postSideEffect(UploadPlaceSideEffect.OnNavigateToBack)
}
@@ -281,27 +288,28 @@ class UploadPlaceViewModel @Inject constructor(
private fun createFeatureList() = intent {
val featureRequests = mutableListOf()
- when(state.selectedRestaurantTypes.isEmpty()) {
- true -> {
- state.selectedCafeOption?.let { cafeOption ->
+ when (val feature = state.selectedFeature) {
+ is SelectedFeature.Cafe -> {
+ if (feature.option == CafeFeatureType.CafeType.WORK_FRIENDLY) {
featureRequests.add(
Feature(
category = CategoryType.CAFE_FEATURE,
- optionList = listOf(cafeOption)
+ optionList = listOf(feature.option)
)
)
}
}
- false -> {
+ is SelectedFeature.Restaurant -> {
featureRequests.add(
Feature(
category = CategoryType.RESTAURANT_FEATURE,
- optionList = state.selectedRestaurantTypes.map { it }
+ optionList = feature.types
)
)
}
- }
+ null -> TODO()
+ }
state.selectedPriceOption?.let { priceOption ->
featureRequests.add(
Feature(
@@ -320,7 +328,7 @@ class UploadPlaceViewModel @Inject constructor(
fun onSubmitUploadPlace(onSuccess:() -> Unit) = intent {
createFeatureList()
-
+ reduce { state.copy(isNextBtnEnabled = false) }
when (state.selectedImageUris?.isEmpty()) {
true -> {
submitUploadPlace(onSuccess)
@@ -338,7 +346,6 @@ class UploadPlaceViewModel @Inject constructor(
onSuccess:() -> Unit,
imageList: List = emptyList()
) = intent {
-
uploadRepository.submitUploadPlace(
spotName = state.selectedSpotByMap?.title ?: "",
address = state.selectedSpotByMap?.address ?: "",
@@ -347,97 +354,118 @@ class UploadPlaceViewModel @Inject constructor(
recommendedMenu = state.recommendMenu ?: "",
imageList = imageList
).onSuccess {
+ reduce { state.copy(isNextBtnEnabled = true) }
onSuccess()
}.onFailure {
+ reduce { state.copy(isNextBtnEnabled = true) }
postSideEffect(UploadPlaceSideEffect.ShowToastUploadFailed)
}
}
private fun uploadAllImagesAndSubmit(onSuccess: () -> Unit) = intent {
- val uris = state.selectedImageUris
+ val uris = state.selectedImageUris ?: emptyList()
+
+ if (uris.isEmpty()) {
+ submitUploadPlace(onSuccess = onSuccess, imageList = emptyList())
+ return@intent
+ }
- val fileNames = try {
+ val presignedResults = runCatching {
coroutineScope {
- uris?.map { imageUri ->
- async {
- val presignedResult = try {
- uploadRepository.getUploadPlacePreSignedUrl().getOrThrow()
- } catch (e: Exception) {
- postSideEffect(UploadPlaceSideEffect.ShowToastUploadImageFailed)
- throw e
- }
- putPlaceImageToPreSignedUrl(imageUri, presignedResult.preSignedUrl)
- presignedResult.fileName
+ (0 until uris.size).map {
+ async(Dispatchers.IO) {
+ uploadRepository.getUploadPlacePreSignedUrl().getOrThrow()
}
- }?.awaitAll()
+ }.awaitAll()
}
- } catch (e: Exception) {
+ }.onFailure {
postSideEffect(UploadPlaceSideEffect.ShowToastUploadImageFailed)
- null
- }
+ return@intent
+ }.getOrThrow()
- val bucketUrls = fileNames?.map { fileName ->
- "${BuildConfig.BUCKET_URL}$fileName"
+ val uploadSuccessful = runCatching {
+ coroutineScope {
+ uris.zip(presignedResults).map { (imageUri, presignedResult) ->
+ async(Dispatchers.IO) {
+ putPlaceImageToPreSignedUrlOptimized(imageUri, presignedResult.preSignedUrl)
+ }
+ }.awaitAll().all { it }
+ }
+ }.onFailure {
+ postSideEffect(UploadPlaceSideEffect.ShowToastUploadImageFailed)
+ return@intent
+ }.getOrThrow()
+
+ if (!uploadSuccessful) {
+ postSideEffect(UploadPlaceSideEffect.ShowToastUploadImageFailed)
+ return@intent
}
- submitUploadPlace(
- onSuccess = onSuccess,
- imageList = bucketUrls ?: emptyList()
- )
+ val bucketUrls = presignedResults.map { "${BuildConfig.BUCKET_URL}${it.fileName}" }
+ submitUploadPlace(onSuccess = onSuccess, imageList = bucketUrls)
}
- private fun putPlaceImageToPreSignedUrl(
+ private suspend fun putPlaceImageToPreSignedUrlOptimized(
imageUri: Uri,
preSignedUrl: String
- ) = intent {
+ ): Boolean = withContext(Dispatchers.IO) {
val context = getApplication().applicationContext
- val client = OkHttpClient()
- try {
+ return@withContext try {
val byteArray: ByteArray
val mimeType: String
if (imageUri.scheme == "content") {
- val inputStream = context.contentResolver.openInputStream(imageUri)
- byteArray = inputStream?.readBytes()
- ?: throw IllegalArgumentException("이미지 읽기 실패")
+ context.contentResolver.openInputStream(imageUri).use { inputStream ->
+ byteArray = inputStream?.readBytes()
+ ?: throw IllegalArgumentException("이미지 읽기 실패")
+ }
mimeType = context.contentResolver.getType(imageUri) ?: "image/jpeg"
-
} else {
Timber.tag(TAG).e("지원하지 않는 URI scheme: %s", imageUri.toString())
throw IllegalArgumentException("지원하지 않는 URI scheme")
}
- val fileBody =
- byteArray.toRequestBody(mimeType.toMediaTypeOrNull(), 0, byteArray.size)
-
+ val fileBody = byteArray.toRequestBody(mimeType.toMediaTypeOrNull(), 0, byteArray.size)
val request = Request.Builder()
.url(preSignedUrl)
.put(fileBody)
.addHeader("Content-Type", mimeType)
.build()
- val response = client.newCall(request).execute()
-
- if (response.isSuccessful) {
- Timber.tag(TAG).d("이미지 업로드 성공")
- } else {
- Timber.tag(TAG).e("이미지 업로드 실패, code: %d", response.code)
- postSideEffect(UploadPlaceSideEffect.ShowToastUploadImageFailed)
+ client.newCall(request).execute().use { response ->
+ if (response.isSuccessful) {
+ Timber.tag(TAG).d("이미지 업로드 성공")
+ true
+ } else {
+ Timber.tag(TAG).e("이미지 업로드 실패, code: ${response.code}")
+ false
+ }
}
} catch (e: Exception) {
- Timber.tag(TAG).e(e, "이미지 업로드 과정에서 예외 발생: %s", e.message)
- postSideEffect(UploadPlaceSideEffect.ShowToastUploadImageFailed)
+ Timber.tag(TAG).e(e, "이미지 업로드 과정에서 예외 발생: ${e.message}")
+ false
}
}
companion object {
+ private val client: OkHttpClient by lazy {
+ OkHttpClient.Builder()
+ .connectionPool(ConnectionPool(20, 5, TimeUnit.MINUTES))
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .retryOnConnectionFailure(true)
+ .build()
+ }
+
const val TAG = "UploadPlaceViewModel"
}
}
@Immutable
data class UploadPlaceUiState(
+ val hasAnimated: Map = emptyMap(),
val isPreviousBtnEnabled: Boolean = false,
val isNextBtnEnabled: Boolean = false,
val showExitUploadPlaceDialog: Boolean = false,
@@ -451,8 +479,7 @@ data class UploadPlaceUiState(
val selectedSpotByMap: SearchedSpotByMap? = null,
val selectedSpotType: SpotType? = null,
val selectedPriceOption: PriceFeatureType.PriceOptionType? = null,
- val selectedCafeOption: CafeFeatureType.CafeType? = null,
- val selectedRestaurantTypes: List = emptyList(),
+ val selectedFeature: SelectedFeature? = null,
val recommendMenu: String? = "",
val selectedImageUris: List? = emptyList(),
val uploadFileName: String = "",
@@ -462,6 +489,11 @@ data class UploadPlaceUiState(
val currentStep: Int = 0
)
+sealed class SelectedFeature {
+ data class Cafe(val option: CafeFeatureType.CafeType) : SelectedFeature()
+ data class Restaurant(val types: List) : SelectedFeature()
+}
+
sealed interface UploadPlaceSideEffect {
data object ShowToastUploadFailed : UploadPlaceSideEffect
data object ShowToastUploadImageFailed : UploadPlaceSideEffect
diff --git a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/UploadPlaceScreen.kt b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/UploadPlaceScreen.kt
index a4a6e30b6..eed7a1598 100644
--- a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/UploadPlaceScreen.kt
+++ b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/UploadPlaceScreen.kt
@@ -5,6 +5,7 @@ import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
+import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
@@ -58,6 +59,7 @@ import com.acon.acon.feature.upload.screen.composable.menu.UploadPlaceEnterMenuS
import org.orbitmvi.orbit.compose.collectAsState
import org.orbitmvi.orbit.compose.collectSideEffect
+private const val animationDuration = 500
private const val maxStepIndex = 6
@Composable
@@ -75,7 +77,11 @@ fun UploadPlaceScreen(
val currentStep = state.currentStep
BackHandler {
- viewModel.onRequestExitUploadPlaceDialog()
+ if(currentStep != maxStepIndex) {
+ viewModel.onRequestExitUploadPlaceDialog()
+ } else {
+ viewModel.onNavigateToBack()
+ }
}
viewModel.collectSideEffect {
@@ -112,7 +118,7 @@ fun UploadPlaceScreen(
AconTwoActionDialog(
title = stringResource(R.string.upload_place_exit),
action1 = stringResource(R.string.cancel),
- action2 = stringResource(R.string.exit),
+ action2 = stringResource(R.string.quit),
onDismissRequest = {},
onAction1 = {
viewModel.onDismissExitUploadPlaceDialog()
@@ -120,7 +126,9 @@ fun UploadPlaceScreen(
onAction2 = {
viewModel.onNavigateToBack()
},
- modifier = Modifier.width(dialogWidth)
+ isTextAlign = true,
+ modifier = Modifier
+ .width(dialogWidth)
)
}
@@ -180,9 +188,23 @@ fun UploadPlaceScreen(
targetState = currentStep,
transitionSpec = {
if (targetState > initialState) {
- slideInVertically { height -> height } togetherWith slideOutVertically { height -> -height }
+ slideInVertically(
+ animationSpec = tween(durationMillis = animationDuration)
+ ) { height -> height }
+ .togetherWith(
+ slideOutVertically(
+ animationSpec = tween(durationMillis = animationDuration)
+ ) { height -> -height }
+ )
} else {
- slideInVertically { height -> -height } togetherWith slideOutVertically { height -> height }
+ slideInVertically(
+ animationSpec = tween(durationMillis = animationDuration)
+ ) { height -> -height }
+ .togetherWith(
+ slideOutVertically(animationSpec = tween(durationMillis = animationDuration)) {
+ height -> height
+ }
+ )
}.using(SizeTransform(clip = true))
},
label = stringResource(R.string.upload_process_step_transition),
@@ -196,28 +218,33 @@ fun UploadPlaceScreen(
onClickReportPlace = viewModel::onClickReportPlace,
onHideSearchedPlaceList = viewModel::onHideSearchedPlaceList,
onSearchedSpotClick = viewModel::onSearchSpotByMapClicked,
- onSearchQueryOrSelectionChanged = viewModel::onSearchQueryOrSelectionChanged
+ onSearchQueryOrSelectionChanged = viewModel::onSearchQueryOrSelectionChanged,
+ onAnimationEnded = viewModel::onSlideAnimationEnd
)
1 -> UploadSelectPlaceScreen(
state = state,
onSelectSpotType = viewModel::updateSpotType,
- onUpdateNextPageBtnEnabled = viewModel::updateNextBtnEnabled
+ onUpdateNextPageBtnEnabled = viewModel::updateNextBtnEnabled,
+ onAnimationEnded = viewModel::onSlideAnimationEnd
)
2 -> UploadSelectPlaceDetailScreen(
state = state,
onUpdateCafeOptionType = viewModel::updateCafeOptionType,
onUpdateRestaurantType = viewModel::updateRestaurantType,
- onUpdateNextPageBtnEnabled = viewModel::updateNextBtnEnabled
+ onUpdateNextPageBtnEnabled = viewModel::updateNextBtnEnabled,
+ onAnimationEnded = viewModel::onSlideAnimationEnd
)
3 -> UploadPlaceEnterMenuScreen(
state = state,
onSearchQueryChanged = viewModel::onSearchQueryChanged,
- onUpdateNextPageBtnEnabled = viewModel::updateNextBtnEnabled
+ onUpdateNextPageBtnEnabled = viewModel::updateNextBtnEnabled,
+ onAnimationEnded = viewModel::onSlideAnimationEnd
)
4 -> UploadSelectPriceScreen(
state = state,
onUpdatePriceOptionType = viewModel::updatePriceOptionType,
- onUpdateNextPageBtnEnabled = viewModel::updateNextBtnEnabled
+ onUpdateNextPageBtnEnabled = viewModel::updateNextBtnEnabled,
+ onAnimationEnded = viewModel::onSlideAnimationEnd
)
5 -> UploadPlaceImageScreen(
state = state,
@@ -226,10 +253,11 @@ fun UploadPlaceScreen(
onAddSpotImageUri = viewModel::onAddImageUris,
onRemoveSpotImageUri = viewModel::onRemoveImageUri,
onUpdateNextPageBtnEnabled = viewModel::updateNextBtnEnabled,
- onRequestUploadPlaceLimitPouUp = viewModel::onRequestUploadPlaceLimitPouUp
+ onRequestUploadPlaceLimitPouUp = viewModel::onRequestUploadPlaceLimitPouUp,
+ onAnimationEnded = viewModel::onSlideAnimationEnd
)
6 -> UploadPlaceCompleteScreen(
- onClickGoHome = viewModel::onNavigateToBack
+ onClickGoHome = viewModel::onNavigateToBack,
)
}
}
diff --git a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/image/UploadPlaceImageScreen.kt b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/image/UploadPlaceImageScreen.kt
index 7250cbd92..a5d1d6717 100644
--- a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/image/UploadPlaceImageScreen.kt
+++ b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/image/UploadPlaceImageScreen.kt
@@ -36,6 +36,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.acon.acon.core.designsystem.R
+import com.acon.acon.core.designsystem.animation.slideUpAnimation
import com.acon.acon.core.designsystem.component.dialog.v2.AconTwoActionDialog
import com.acon.acon.core.designsystem.component.popup.CustomToast
import com.acon.acon.core.designsystem.effect.LocalHazeState
@@ -58,8 +59,10 @@ internal fun UploadPlaceImageScreen(
onAddSpotImageUri:(uris: List) -> Unit,
onRemoveSpotImageUri:(uri: Uri) -> Unit,
onUpdateNextPageBtnEnabled: (Boolean) -> Unit,
- onRequestUploadPlaceLimitPouUp: () -> Unit
+ onRequestUploadPlaceLimitPouUp: () -> Unit,
+ onAnimationEnded: (String) -> Unit
) {
+ val hasAnimated = state.hasAnimated["7"] ?: false
val selectedUris = state.selectedImageUris ?: emptyList()
val screenHeightDp = getScreenHeight()
@@ -141,7 +144,9 @@ internal fun UploadPlaceImageScreen(
text = stringResource(R.string.optional_field),
style = AconTheme.typography.Body1,
color = AconTheme.color.Gray300,
- modifier = Modifier.padding(top = 40.dp)
+ modifier = Modifier
+ .padding(top = 40.dp)
+ .then(if (!hasAnimated) Modifier.slideUpAnimation(order = 1) else Modifier)
)
Spacer(Modifier.height(4.dp))
@@ -149,16 +154,28 @@ internal fun UploadPlaceImageScreen(
text = stringResource(R.string.upload_place_image_title),
style = AconTheme.typography.Headline3,
color = AconTheme.color.White,
- modifier = Modifier.padding(2.dp)
+ modifier = Modifier
+ .padding(2.dp)
+ .then(if (!hasAnimated) Modifier.slideUpAnimation(order = 2) else Modifier)
)
Spacer(Modifier.height(32.dp))
- Box {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(imageBoxHeight)
+ .clip(RoundedCornerShape(10.dp))
+ .then(
+ if (!hasAnimated) Modifier.slideUpAnimation(
+ order = 4,
+ onAnimationEnded = { onAnimationEnded("7") }
+ ) else Modifier
+ )
+ ) {
if (selectedUris.isEmpty()) {
Box(
modifier = Modifier
- .fillMaxWidth()
- .height(imageBoxHeight)
+ .fillMaxSize()
.clip(RoundedCornerShape(10.dp))
.background(AconTheme.color.GlassWhiteDisabled)
.aspectRatio(1f)
@@ -272,7 +289,8 @@ private fun UploadPlaceImageScreenPreview() {
onAddSpotImageUri = {},
onRemoveSpotImageUri = {},
onUpdateNextPageBtnEnabled = {},
- onRequestUploadPlaceLimitPouUp = {}
+ onRequestUploadPlaceLimitPouUp = {},
+ onAnimationEnded = {}
)
}
}
\ No newline at end of file
diff --git a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/place/UploadSelectPlaceDetailScreen.kt b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/place/UploadSelectPlaceDetailScreen.kt
index cb6be0c8d..20cbdda37 100644
--- a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/place/UploadSelectPlaceDetailScreen.kt
+++ b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/place/UploadSelectPlaceDetailScreen.kt
@@ -22,10 +22,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.acon.acon.core.designsystem.R
+import com.acon.acon.core.designsystem.animation.slideUpAnimation
import com.acon.acon.core.designsystem.theme.AconTheme
import com.acon.acon.core.model.type.CafeFeatureType
import com.acon.acon.core.model.type.RestaurantFeatureType
import com.acon.acon.core.model.type.SpotType
+import com.acon.acon.feature.upload.screen.SelectedFeature
import com.acon.acon.feature.upload.screen.UploadPlaceUiState
import com.acon.acon.feature.upload.screen.composable.add.UploadPlaceSelectItem
import com.acon.acon.feature.upload.screen.composable.type.getNameResId
@@ -36,18 +38,22 @@ internal fun UploadSelectPlaceDetailScreen(
state: UploadPlaceUiState,
onUpdateCafeOptionType: (CafeFeatureType.CafeType) -> Unit,
onUpdateRestaurantType: (RestaurantFeatureType.RestaurantType) -> Unit,
- onUpdateNextPageBtnEnabled: (Boolean) -> Unit
+ onUpdateNextPageBtnEnabled: (Boolean) -> Unit,
+ onAnimationEnded: (String) -> Unit
) {
+ val hasAnimatedRestaurant = state.hasAnimated["3"] ?: false
+ val hasAnimatedCafe = state.hasAnimated["4"] ?: false
+
val allRestaurantTypes = remember {
persistentListOf(*RestaurantFeatureType.RestaurantType.entries.toTypedArray())
}
val isNextPageBtnEnabled by remember(state) {
derivedStateOf {
- when (state.selectedSpotType) {
- SpotType.RESTAURANT -> state.selectedRestaurantTypes.isNotEmpty()
- SpotType.CAFE -> state.selectedCafeOption != null
- else -> false
+ when(val feature = state.selectedFeature) {
+ is SelectedFeature.Restaurant -> feature.types.isNotEmpty()
+ is SelectedFeature.Cafe -> true
+ null -> false
}
}
}
@@ -68,14 +74,17 @@ internal fun UploadSelectPlaceDetailScreen(
text = stringResource(R.string.required_field),
style = AconTheme.typography.Body1,
color = AconTheme.color.Danger,
- modifier = Modifier.padding(top = 40.dp)
+ modifier = Modifier
+ .padding(top = 40.dp)
+ .then(if (!hasAnimatedRestaurant) Modifier.slideUpAnimation(order = 1) else Modifier)
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.upload_place_select_restaurant_title),
style = AconTheme.typography.Headline3,
- color = AconTheme.color.White
+ color = AconTheme.color.White,
+ modifier = Modifier.then(if (!hasAnimatedRestaurant) Modifier.slideUpAnimation(order = 2) else Modifier)
)
Spacer(Modifier.height(10.dp))
@@ -83,14 +92,28 @@ internal fun UploadSelectPlaceDetailScreen(
text = stringResource(R.string.upload_place_select_cafe_sub_title),
style = AconTheme.typography.Title5,
color = AconTheme.color.Gray500,
- fontWeight = FontWeight.Normal
+ fontWeight = FontWeight.Normal,
+ modifier = Modifier
+ .then(
+ if (!hasAnimatedRestaurant) Modifier.slideUpAnimation(
+ hasCaption = true,
+ order = 3
+ ) else Modifier
+ )
)
Spacer(Modifier.height(32.dp))
Column(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 4.dp),
+ .padding(horizontal = 4.dp)
+ .then(
+ if (!hasAnimatedRestaurant) Modifier.slideUpAnimation(
+ hasCaption = true,
+ order = 4,
+ onAnimationEnded = { onAnimationEnded("3") }
+ ) else Modifier
+ ),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
allRestaurantTypes.chunked(2).fastForEach { pair ->
@@ -102,7 +125,7 @@ internal fun UploadSelectPlaceDetailScreen(
UploadPlaceSelectItem(
title = stringResource(id = restaurantType.getNameResId()),
modifier = Modifier.weight(1f),
- isSelected = state.selectedRestaurantTypes.contains(restaurantType),
+ isSelected = (state.selectedFeature as? SelectedFeature.Restaurant)?.types?.contains(restaurantType) == true,
onClickUploadPlaceSelectItem = {
onUpdateRestaurantType(restaurantType)
}
@@ -118,33 +141,46 @@ internal fun UploadSelectPlaceDetailScreen(
text = stringResource(R.string.required_field),
style = AconTheme.typography.Body1,
color = AconTheme.color.Danger,
- modifier = Modifier.padding(top = 40.dp)
+ modifier = Modifier
+ .padding(top = 40.dp)
+ .then(if (!hasAnimatedCafe) Modifier.slideUpAnimation(order = 1) else Modifier)
+
+
)
Spacer(Modifier.height(10.dp))
Text(
text = stringResource(R.string.upload_place_select_restaurant_cafe),
style = AconTheme.typography.Headline3,
- color = AconTheme.color.White
+ color = AconTheme.color.White,
+ modifier = Modifier
+ .then(if (!hasAnimatedCafe) Modifier.slideUpAnimation(order = 2) else Modifier)
)
Spacer(Modifier.height(32.dp))
Column(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 8.dp),
+ .padding(horizontal = 8.dp)
+ .then(
+ if (!hasAnimatedCafe) Modifier.slideUpAnimation(
+ hasCaption = false,
+ order = 4,
+ onAnimationEnded = { onAnimationEnded("4") }
+ ) else Modifier
+ ),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
UploadPlaceSelectItem(
- title = stringResource(CafeFeatureType.CafeType.GOOD_FOR_WORK.getNameResId()),
- isSelected = state.selectedCafeOption == CafeFeatureType.CafeType.GOOD_FOR_WORK,
+ title = stringResource(CafeFeatureType.CafeType.WORK_FRIENDLY.getNameResId()),
+ isSelected = (state.selectedFeature as? SelectedFeature.Cafe)?.option == CafeFeatureType.CafeType.WORK_FRIENDLY,
onClickUploadPlaceSelectItem = {
- onUpdateCafeOptionType(CafeFeatureType.CafeType.GOOD_FOR_WORK)
+ onUpdateCafeOptionType(CafeFeatureType.CafeType.WORK_FRIENDLY)
}
)
UploadPlaceSelectItem(
title = stringResource(CafeFeatureType.CafeType.NOT_GOOD_FOR_WORK.getNameResId()),
- isSelected = state.selectedCafeOption == CafeFeatureType.CafeType.NOT_GOOD_FOR_WORK,
+ isSelected = (state.selectedFeature as? SelectedFeature.Cafe)?.option == CafeFeatureType.CafeType.NOT_GOOD_FOR_WORK,
onClickUploadPlaceSelectItem = {
onUpdateCafeOptionType(CafeFeatureType.CafeType.NOT_GOOD_FOR_WORK)
}
@@ -165,7 +201,8 @@ private fun UploadSelectPlaceDetailScreenPreview() {
state = UploadPlaceUiState(),
onUpdateCafeOptionType = {},
onUpdateRestaurantType = {},
- onUpdateNextPageBtnEnabled = {}
+ onUpdateNextPageBtnEnabled = {},
+ onAnimationEnded = {}
)
}
}
\ No newline at end of file
diff --git a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/place/UploadSelectPlaceScreen.kt b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/place/UploadSelectPlaceScreen.kt
index b20a8e97b..3dca4548b 100644
--- a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/place/UploadSelectPlaceScreen.kt
+++ b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/place/UploadSelectPlaceScreen.kt
@@ -20,6 +20,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.acon.acon.core.designsystem.R
+import com.acon.acon.core.designsystem.animation.slideUpAnimation
import com.acon.acon.core.designsystem.theme.AconTheme
import com.acon.acon.core.model.type.SpotType
import com.acon.acon.feature.upload.screen.UploadPlaceUiState
@@ -30,8 +31,11 @@ import com.acon.acon.feature.upload.screen.composable.type.getNameResId
internal fun UploadSelectPlaceScreen(
state: UploadPlaceUiState,
onSelectSpotType: (SpotType) -> Unit,
- onUpdateNextPageBtnEnabled: (Boolean) -> Unit
+ onUpdateNextPageBtnEnabled: (Boolean) -> Unit,
+ onAnimationEnded: (String) -> Unit
) {
+ val hasAnimated = state.hasAnimated["2"] ?: false
+
val isNextPageBtnEnabled by remember(state) {
derivedStateOf {
state.selectedSpotType != null
@@ -52,14 +56,17 @@ internal fun UploadSelectPlaceScreen(
text = stringResource(R.string.required_field),
style = AconTheme.typography.Body1,
color = AconTheme.color.Danger,
- modifier = Modifier.padding(top = 40.dp)
+ modifier = Modifier
+ .padding(top = 40.dp)
+ .then(if (!hasAnimated) Modifier.slideUpAnimation(order = 1) else Modifier)
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.upload_place_select_place_title),
style = AconTheme.typography.Headline3,
- color = AconTheme.color.White
+ color = AconTheme.color.White,
+ modifier = Modifier.then(if (!hasAnimated) Modifier.slideUpAnimation(order = 2) else Modifier)
)
Spacer(Modifier.height(10.dp))
@@ -67,14 +74,28 @@ internal fun UploadSelectPlaceScreen(
text = stringResource(R.string.upload_place_select_place_sub_title),
style = AconTheme.typography.Title5,
color = AconTheme.color.Gray500,
- fontWeight = FontWeight.Normal
+ fontWeight = FontWeight.Normal,
+ modifier = Modifier
+ .then(
+ if (!hasAnimated) Modifier.slideUpAnimation(
+ hasCaption = true,
+ order = 3
+ ) else Modifier
+ )
)
Spacer(Modifier.height(32.dp))
Column(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 8.dp),
+ .padding(horizontal = 8.dp)
+ .then(
+ if (!hasAnimated) Modifier.slideUpAnimation(
+ hasCaption = true,
+ order = 4,
+ onAnimationEnded = { onAnimationEnded("2") }
+ ) else Modifier
+ ),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
UploadPlaceSelectItem(
@@ -103,6 +124,7 @@ private fun UploadSelectPlaceScreenPreview() {
state = UploadPlaceUiState(),
onSelectSpotType = {},
onUpdateNextPageBtnEnabled = {},
+ onAnimationEnded = {}
)
}
}
\ No newline at end of file
diff --git a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/price/UploadSelectPriceScreen.kt b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/price/UploadSelectPriceScreen.kt
index eaed138b1..f1893a446 100644
--- a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/price/UploadSelectPriceScreen.kt
+++ b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/price/UploadSelectPriceScreen.kt
@@ -19,6 +19,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.acon.acon.core.designsystem.R
+import com.acon.acon.core.designsystem.animation.slideUpAnimation
import com.acon.acon.core.designsystem.theme.AconTheme
import com.acon.acon.core.model.type.PriceFeatureType
import com.acon.acon.feature.upload.screen.UploadPlaceUiState
@@ -29,8 +30,11 @@ import com.acon.acon.feature.upload.screen.composable.type.getNameResId
internal fun UploadSelectPriceScreen(
state: UploadPlaceUiState,
onUpdatePriceOptionType: (PriceFeatureType.PriceOptionType) -> Unit,
- onUpdateNextPageBtnEnabled: (Boolean) -> Unit
+ onUpdateNextPageBtnEnabled: (Boolean) -> Unit,
+ onAnimationEnded: (String) -> Unit
) {
+ val hasAnimated = state.hasAnimated["6"] ?: false
+
val isNextPageBtnEnabled by remember(state) {
derivedStateOf {
state.selectedPriceOption != null
@@ -51,7 +55,9 @@ internal fun UploadSelectPriceScreen(
text = stringResource(R.string.required_field),
style = AconTheme.typography.Body1,
color = AconTheme.color.Danger,
- modifier = Modifier.padding(top = 40.dp)
+ modifier = Modifier
+ .padding(top = 40.dp)
+ .then(if (!hasAnimated) Modifier.slideUpAnimation(order = 1) else Modifier)
)
Spacer(Modifier.height(4.dp))
@@ -59,14 +65,23 @@ internal fun UploadSelectPriceScreen(
text = stringResource(R.string.upload_place_select_price_title),
style = AconTheme.typography.Headline3,
color = AconTheme.color.White,
- modifier = Modifier.padding(2.dp)
+ modifier = Modifier
+ .padding(2.dp)
+ .then(if (!hasAnimated) Modifier.slideUpAnimation(order = 2) else Modifier)
)
Spacer(Modifier.height(32.dp))
Column(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 8.dp),
+ .padding(horizontal = 8.dp)
+ .then(
+ if (!hasAnimated) Modifier.slideUpAnimation(
+ hasCaption = true,
+ order = 3,
+ onAnimationEnded = { onAnimationEnded("6") }
+ ) else Modifier
+ ),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
UploadPlaceSelectItem(
@@ -103,7 +118,8 @@ private fun UploadSelectPriceScreenPreview(
UploadSelectPriceScreen(
state = UploadPlaceUiState(),
onUpdatePriceOptionType = {},
- onUpdateNextPageBtnEnabled = {}
+ onUpdateNextPageBtnEnabled = {},
+ onAnimationEnded = {}
)
}
}
\ No newline at end of file
diff --git a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/search/UploadPlaceSearchScreen.kt b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/search/UploadPlaceSearchScreen.kt
index 005e906d4..c2e5a7084 100644
--- a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/search/UploadPlaceSearchScreen.kt
+++ b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/add/search/UploadPlaceSearchScreen.kt
@@ -51,6 +51,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import com.acon.acon.core.designsystem.R
+import com.acon.acon.core.designsystem.animation.slideUpAnimation
import com.acon.acon.core.designsystem.component.dialog.v2.AconTwoActionDialog
import com.acon.acon.core.designsystem.component.textfield.v2.AconSearchTextField
import com.acon.acon.core.designsystem.effect.LocalHazeState
@@ -70,7 +71,8 @@ internal fun UploadPlaceSearchScreen(
onClickReportPlace: () -> Unit,
onHideSearchedPlaceList: () -> Unit,
onSearchedSpotClick: (SearchedSpotByMap, onUpdateTextField: () -> Unit) -> Unit,
- onSearchQueryOrSelectionChanged: (String, Boolean) -> Unit
+ onSearchQueryOrSelectionChanged: (String, Boolean) -> Unit,
+ onAnimationEnded: (String) -> Unit
) {
val lifecycleOwner = LocalLifecycleOwner.current
@@ -90,6 +92,8 @@ internal fun UploadPlaceSearchScreen(
animationSpec = tween(durationMillis = 300)
)
+ val hasAnimated = state.hasAnimated["1"] ?: false
+
LaunchedEffect(Unit) {
snapshotFlow { query }.collect {
onSearchQueryOrSelectionChanged(it.text, isSelection)
@@ -128,6 +132,7 @@ internal fun UploadPlaceSearchScreen(
}
}
+
Column(
modifier = Modifier
.fillMaxSize()
@@ -155,17 +160,21 @@ internal fun UploadPlaceSearchScreen(
text = stringResource(R.string.required_field),
style = AconTheme.typography.Body1,
color = AconTheme.color.Danger,
- modifier = Modifier.padding(top = 40.dp)
+ modifier = Modifier
+ .padding(top = 40.dp)
+ .then(if (!hasAnimated) Modifier.slideUpAnimation(order = 1) else Modifier)
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.upload_place_search_title),
style = AconTheme.typography.Headline3,
- color = AconTheme.color.White
+ color = AconTheme.color.White,
+ modifier = Modifier
+ .then(if (!hasAnimated) Modifier.slideUpAnimation(order = 2) else Modifier)
)
- Spacer(Modifier.height(20.dp))
+ Spacer(Modifier.height(32.dp))
}
Column {
@@ -189,36 +198,42 @@ internal fun UploadPlaceSearchScreen(
),
modifier = Modifier
.fillMaxWidth()
+ .then(
+ if (!hasAnimated) Modifier.slideUpAnimation(
+ order = 4,
+ onAnimationEnded = { onAnimationEnded("1") }
+ ) else Modifier
+ )
.onFocusChanged { focusState ->
if (focusState.isFocused) {
showOffset = true
}
}
)
- Box {
- if (state.showSearchedSpotsByMap) {
- SearchedSpots(
- searchedSpotsByMap = state.searchedSpotsByMap.toImmutableList(),
- onItemClick = {
- keyboardController?.hide()
- focusManager.clearFocus()
- onSearchedSpotClick(it) {
- isSelection = true
- query = TextFieldValue(
- text = it.title,
- selection = TextRange(it.title.length)
- )
- showOffset = false
- }
- },
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 10.dp)
- .height(300.dp)
- .clip(RoundedCornerShape(10.dp))
- .background(AconTheme.color.GlassWhiteLight)
- )
- }
+ }
+ Box {
+ if (state.showSearchedSpotsByMap) {
+ SearchedSpots(
+ searchedSpotsByMap = state.searchedSpotsByMap.toImmutableList(),
+ onItemClick = {
+ keyboardController?.hide()
+ focusManager.clearFocus()
+ onSearchedSpotClick(it) {
+ isSelection = true
+ query = TextFieldValue(
+ text = it.title,
+ selection = TextRange(it.title.length)
+ )
+ showOffset = false
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 10.dp)
+ .height(300.dp)
+ .clip(RoundedCornerShape(10.dp))
+ .background(AconTheme.color.GlassWhiteLight)
+ )
}
}
}
@@ -316,7 +331,8 @@ private fun UploadPlaceSearchScreenPreview() {
onClickReportPlace = {},
onHideSearchedPlaceList = {},
onSearchedSpotClick = { _, _ -> },
- onSearchQueryOrSelectionChanged = { _, _ -> }
+ onSearchQueryOrSelectionChanged = { _, _ -> },
+ onAnimationEnded = {}
)
}
}
\ No newline at end of file
diff --git a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/menu/UploadEnterMenuScreen.kt b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/menu/UploadEnterMenuScreen.kt
index bded388ec..9530fbd95 100644
--- a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/menu/UploadEnterMenuScreen.kt
+++ b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/menu/UploadEnterMenuScreen.kt
@@ -86,9 +86,11 @@ internal fun UploadEnterMenuScreen(
Spacer(Modifier.height(32.dp))
AconSearchTextField(
value = query,
- onValueChange = {
- query = it
- isSelection = false
+ onValueChange = { newValue ->
+ if (newValue.text.length <= 30) {
+ query = newValue
+ isSelection = false
+ }
},
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
diff --git a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/menu/UploadPlaceEnterMenuScreen.kt b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/menu/UploadPlaceEnterMenuScreen.kt
index 196578ed4..07e5b52c0 100644
--- a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/menu/UploadPlaceEnterMenuScreen.kt
+++ b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/menu/UploadPlaceEnterMenuScreen.kt
@@ -30,6 +30,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.acon.acon.core.designsystem.R
+import com.acon.acon.core.designsystem.animation.slideUpAnimation
import com.acon.acon.core.designsystem.component.textfield.v2.AconOutlinedSearchTextField
import com.acon.acon.core.designsystem.theme.AconTheme
import com.acon.acon.feature.upload.screen.UploadPlaceUiState
@@ -38,8 +39,11 @@ import com.acon.acon.feature.upload.screen.UploadPlaceUiState
internal fun UploadPlaceEnterMenuScreen(
state: UploadPlaceUiState,
onSearchQueryChanged: (String) -> Unit,
- onUpdateNextPageBtnEnabled: (Boolean) -> Unit
+ onUpdateNextPageBtnEnabled: (Boolean) -> Unit,
+ onAnimationEnded: (String) -> Unit
) {
+ val hasAnimated = state.hasAnimated["5"] ?: false
+
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
@@ -65,42 +69,57 @@ internal fun UploadPlaceEnterMenuScreen(
})
}
) {
- Text(
- text = stringResource(R.string.required_field),
- style = AconTheme.typography.Body1,
- color = AconTheme.color.Danger,
- modifier = Modifier.padding(top = 40.dp)
- )
+ Column {
+ Text(
+ text = stringResource(R.string.required_field),
+ style = AconTheme.typography.Body1,
+ color = AconTheme.color.Danger,
+ modifier = Modifier
+ .padding(top = 40.dp)
+ .then(if (!hasAnimated) Modifier.slideUpAnimation(order = 1) else Modifier)
+ )
- Text(
- text = stringResource(R.string.upload_place_enter_menu_title),
- style = AconTheme.typography.Headline3,
- color = AconTheme.color.White,
- modifier = Modifier.padding(top = 4.dp, start = 2.dp)
- )
+ Text(
+ text = stringResource(R.string.upload_place_enter_menu_title),
+ style = AconTheme.typography.Headline3,
+ color = AconTheme.color.White,
+ modifier = Modifier
+ .padding(top = 4.dp, start = 2.dp)
+ .then(if (!hasAnimated) Modifier.slideUpAnimation(order = 2) else Modifier)
+ )
- Spacer(Modifier.height(32.dp))
- AconOutlinedSearchTextField(
- value = query,
- onValueChange = { newValue ->
- if (newValue.text.length <= 20) {
- query = newValue
- isSelection = false
- }
- },
- keyboardOptions = KeyboardOptions(
- imeAction = ImeAction.Done,
- keyboardType = KeyboardType.Text
- ),
- keyboardActions = KeyboardActions(
- onDone = {
- keyboardController?.hide()
- focusManager.clearFocus()
- }
- ),
- placeholder = stringResource(R.string.upload_place_enter_menu_placeholder),
- modifier = Modifier.fillMaxWidth()
- )
+ Spacer(Modifier.height(32.dp))
+ }
+ Column {
+ AconOutlinedSearchTextField(
+ value = query,
+ onValueChange = { newValue ->
+ if (newValue.text.length <= 30) {
+ query = newValue
+ isSelection = false
+ }
+ },
+ keyboardOptions = KeyboardOptions(
+ imeAction = ImeAction.Done,
+ keyboardType = KeyboardType.Text
+ ),
+ keyboardActions = KeyboardActions(
+ onDone = {
+ keyboardController?.hide()
+ focusManager.clearFocus()
+ }
+ ),
+ placeholder = stringResource(R.string.upload_place_enter_menu_placeholder),
+ modifier = Modifier
+ .fillMaxWidth()
+ .then(
+ if (!hasAnimated) Modifier.slideUpAnimation(
+ order = 4,
+ onAnimationEnded = { onAnimationEnded("5") }
+ ) else Modifier
+ )
+ )
+ }
}
}
@@ -111,7 +130,8 @@ private fun UploadPlaceEnterMenuScreenPreview() {
UploadPlaceEnterMenuScreen(
state = UploadPlaceUiState(),
onSearchQueryChanged = {},
- onUpdateNextPageBtnEnabled = {}
+ onUpdateNextPageBtnEnabled = {},
+ onAnimationEnded = {}
)
}
}
\ No newline at end of file
diff --git a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/type/TypeExtensions.kt b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/type/TypeExtensions.kt
index 84c7f6ee4..d6a118c69 100644
--- a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/type/TypeExtensions.kt
+++ b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/composable/type/TypeExtensions.kt
@@ -38,7 +38,7 @@ internal fun RestaurantFeatureType.RestaurantType.getNameResId(): Int {
internal fun CafeFeatureType.CafeType.getNameResId(): Int {
return when(this) {
- CafeFeatureType.CafeType.GOOD_FOR_WORK -> R.string.upload_place_select_cafe_option1
+ CafeFeatureType.CafeType.WORK_FRIENDLY -> R.string.upload_place_select_cafe_option1
CafeFeatureType.CafeType.NOT_GOOD_FOR_WORK -> R.string.upload_place_select_cafe_option2
}
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c4c3df544..dc3f63fb3 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,8 +4,8 @@ projectApplicationId = "com.acon.acon"
projectCompileSdk = "35"
projectTargetSdk = "35"
projectMinSdk = "28"
-projectVersionCode = "20000003"
-projectVersionName = "2.0.0"
+projectVersionCode = "20010001"
+projectVersionName = "2.1.0"
#######################################
composeCompilerVersion = "1.5.1"