diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 00000000..cbd3cfd1
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,7 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(xargs grep:*)"
+ ]
+ }
+}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b6d66af0..ee4ccc7a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -47,7 +47,7 @@ android {
minSdk = 26
targetSdk = 35
versionCode = 11015
- versionName = "1.4.1"
+ versionName = "1.5.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b6bf3517..c949cd13 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,8 @@
+
+
diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/KakaoLocalApi.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/KakaoLocalApi.kt
new file mode 100644
index 00000000..8caa911d
--- /dev/null
+++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/KakaoLocalApi.kt
@@ -0,0 +1,20 @@
+package com.no5ing.bbibbi.data.datasource.network
+
+import com.no5ing.bbibbi.data.datasource.network.response.KakaoSearchResponse
+import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.Query
+
+interface KakaoLocalApi {
+
+ @GET("v2/local/search/keyword.json")
+ suspend fun searchKeyword(
+ @Header("Authorization") authorization: String,
+ @Query("query") query: String,
+ @Query("x") x: String? = null,
+ @Query("y") y: String? = null,
+ @Query("radius") radius: Int? = null,
+ @Query("page") page: Int? = null,
+ @Query("size") size: Int? = null,
+ ): KakaoSearchResponse
+}
diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt
index 77d68b9f..09074a35 100644
--- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/RestAPI.kt
@@ -36,6 +36,7 @@ import com.no5ing.bbibbi.data.model.notification.NotificationModel
import com.no5ing.bbibbi.data.model.post.AIImageCount
import com.no5ing.bbibbi.data.model.post.AIImageResponse
import com.no5ing.bbibbi.data.model.post.AIPost
+import com.no5ing.bbibbi.data.model.post.AIPostType
import com.no5ing.bbibbi.data.model.post.CalendarBanner
import com.no5ing.bbibbi.data.model.post.CalendarElement
import com.no5ing.bbibbi.data.model.post.DailyCalendarElement
@@ -204,6 +205,7 @@ interface RestAPI {
suspend fun createAiPost(
@Body body: CreatePostRequest,
@Query("type") type: String? = null,
+ @Query("aiPostType") aiPostType: String? = null,
): ApiResponse
@POST("v1/posts/image-upload-request")
@@ -334,10 +336,16 @@ interface RestAPI {
@Query("size") size: Int?,
@Query("memberId") memberId: String?,
@Query("sort") sort: String? = "DESC",
+ @Query("aiPostType") aiPostType: String? = null,
): ApiResponse>
@GET("v1/posts/ai-images/count")
- suspend fun getAiImagePostCount(): ApiResponse
+ suspend fun getAiImagePostCount(
+ @Query("aiPostType") aiPostType: String? = null,
+ ): ApiResponse
+
+ @GET("v1/posts/ai-images/types")
+ suspend fun getAiImageTypes(): ApiResponse>
}
/**
diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/request/post/CreatePostRequest.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/request/post/CreatePostRequest.kt
index b4b157df..10d7687d 100644
--- a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/request/post/CreatePostRequest.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/request/post/CreatePostRequest.kt
@@ -9,4 +9,7 @@ data class CreatePostRequest(
val imageUrl: String,
val content: String,
val uploadTime: String,
+ val latitude: Double? = null,
+ val longitude: Double? = null,
+ val address: String? = null,
) : Parcelable, BaseModel()
diff --git a/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/response/KakaoSearchResponse.kt b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/response/KakaoSearchResponse.kt
new file mode 100644
index 00000000..6e443dbe
--- /dev/null
+++ b/app/src/main/java/com/no5ing/bbibbi/data/datasource/network/response/KakaoSearchResponse.kt
@@ -0,0 +1,30 @@
+package com.no5ing.bbibbi.data.datasource.network.response
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+import com.fasterxml.jackson.annotation.JsonProperty
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class KakaoSearchResponse(
+ val meta: KakaoSearchMeta,
+ val documents: List,
+)
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class KakaoSearchMeta(
+ @JsonProperty("total_count") val totalCount: Int,
+ @JsonProperty("pageable_count") val pageableCount: Int,
+ @JsonProperty("is_end") val isEnd: Boolean,
+)
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class KakaoSearchDocument(
+ val id: String = "",
+ @JsonProperty("place_name") val placeName: String = "",
+ @JsonProperty("address_name") val addressName: String = "",
+ @JsonProperty("road_address_name") val roadAddressName: String = "",
+ val x: String = "",
+ val y: String = "",
+ val phone: String = "",
+ val distance: String = "",
+ @JsonProperty("category_name") val categoryName: String = "",
+)
diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/post/AIPostType.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/post/AIPostType.kt
new file mode 100644
index 00000000..f89ad100
--- /dev/null
+++ b/app/src/main/java/com/no5ing/bbibbi/data/model/post/AIPostType.kt
@@ -0,0 +1,16 @@
+package com.no5ing.bbibbi.data.model.post
+
+import com.no5ing.bbibbi.data.model.BaseModel
+
+data class AIPostType(
+ val aiPostType: String,
+ val imageUrl: String,
+ val startDate: String,
+ val aiPostTheme: String,
+ val endDate: String,
+ val postCount: Int,
+) : BaseModel() {
+ fun getTypeName(): String {
+ return aiPostType
+ }
+}
diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt
index 5c193f52..f0564a72 100644
--- a/app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/data/model/post/DailyCalendarElement.kt
@@ -19,6 +19,7 @@ data class DailyCalendarElement(
val emojiCount: Int,
val allFamilyMembersUploaded: Boolean,
val createdAt: ZonedDateTime,
+ val address: String?,
) : Parcelable, BaseModel() {
fun toPost() = Post(
postId = postId,
@@ -30,5 +31,6 @@ data class DailyCalendarElement(
imageUrl = postImgUrl,
content = postContent,
createdAt = createdAt,
+ address = address,
)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/no5ing/bbibbi/data/model/post/Post.kt b/app/src/main/java/com/no5ing/bbibbi/data/model/post/Post.kt
index 927a5488..216ee38f 100644
--- a/app/src/main/java/com/no5ing/bbibbi/data/model/post/Post.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/data/model/post/Post.kt
@@ -15,6 +15,7 @@ data class Post(
val emojiCount: Int,
val imageUrl: String,
val content: String,
+ val address: String?,
val createdAt: ZonedDateTime,
) : Parcelable, BaseModel()
diff --git a/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetAIPostsRepository.kt b/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetAIPostsRepository.kt
index 50e78c06..c322a874 100644
--- a/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetAIPostsRepository.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/data/repository/post/GetAIPostsRepository.kt
@@ -63,6 +63,7 @@ class GetAIPostPagingSource @Inject constructor(
memberId = null,
page = loadParams.key ?: 1,
size = loadParams.loadSize,
+ aiPostType = arguments.get("aiPostType"),
).mapSuccess {
Pagination(
currentPage = currentPage,
diff --git a/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt b/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt
index ad7e8ddb..adbc122c 100644
--- a/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/di/NetworkModule.kt
@@ -7,6 +7,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule
import com.google.gson.Gson
import com.no5ing.bbibbi.BuildConfig
+import com.no5ing.bbibbi.data.datasource.network.KakaoLocalApi
import com.no5ing.bbibbi.data.datasource.network.RestAPI
import com.no5ing.bbibbi.data.model.auth.AuthResult
import com.skydoves.sandwich.SandwichInitializer
@@ -215,6 +216,22 @@ object NetworkModule {
.build()
}
+ @Provides
+ @Singleton
+ fun provideKakaoLocalApi(): KakaoLocalApi {
+ return Retrofit.Builder()
+ .baseUrl("https://dapi.kakao.com/")
+ .addConverterFactory(
+ JacksonConverterFactory.create(
+ jacksonObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .registerModule(kotlinModule())
+ )
+ )
+ .build()
+ .create(KakaoLocalApi::class.java)
+ }
+
@Provides
@Singleton
fun provideRestFamilyApi(retrofit: Retrofit): RestAPI.FamilyApi {
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/main/home/HomePageState.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/main/home/HomePageState.kt
index bcb80400..4b367dd0 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/main/home/HomePageState.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/state/main/home/HomePageState.kt
@@ -30,6 +30,7 @@ fun rememberHomePageState(
createdAt = ZonedDateTime.now(),
missionId = null,
type = PostType.SURVIVAL,
+ address = null,
)
)
},
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/family_studio/FamilyStudioPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/family_studio/FamilyStudioPage.kt
index 2f56bd60..aef147c6 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/family_studio/FamilyStudioPage.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/family_studio/FamilyStudioPage.kt
@@ -35,6 +35,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.no5ing.bbibbi.R
+import coil.compose.AsyncImage
import com.no5ing.bbibbi.data.model.post.AIPost
import com.no5ing.bbibbi.data.repository.Arguments
import com.no5ing.bbibbi.presentation.component.AIPhotoInfoBaloon
@@ -45,39 +46,46 @@ import com.no5ing.bbibbi.presentation.component.button.CustomCTAButton
import com.no5ing.bbibbi.presentation.feature.view.common.CustomAlertDialog
import com.no5ing.bbibbi.presentation.feature.view_model.post.GetAIPostsViewModel
import com.no5ing.bbibbi.presentation.feature.view_model.post.GetAiImageCountViewModel
+import com.no5ing.bbibbi.presentation.feature.view_model.post.GetAiImageTypesViewModel
import com.no5ing.bbibbi.presentation.theme.bbibbiScheme
import com.no5ing.bbibbi.presentation.theme.bbibbiTypo
import com.no5ing.bbibbi.util.LocalSessionState
+import com.no5ing.bbibbi.util.asyncImagePainter
import java.time.LocalDate
@Composable
fun FamilyStudioPage(
+ aiPostType: String = "",
onDispose: () -> Unit = {},
onTapCreateImage: () -> Unit = {},
onTapAiPost: (AIPost) -> Unit = {},
postsViewModel: GetAIPostsViewModel = hiltViewModel(),
aiImageCountViewModel: GetAiImageCountViewModel = hiltViewModel(),
+ aiImageTypesViewModel: GetAiImageTypesViewModel = hiltViewModel(),
isTermDialogEnabled: State = mutableStateOf(true),
onDisagreeTerm: () -> Unit = {},
onAgreeTerm: () -> Unit = {},
onClickTerm: () -> Unit = {},
) {
val aiImageState = aiImageCountViewModel.uiState.collectAsState()
+ val typesState = aiImageTypesViewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
+ val postArgs = Arguments(
+ arguments = if (aiPostType.isNotEmpty()) mapOf("aiPostType" to aiPostType.uppercase()) else emptyMap()
+ )
if (postsViewModel.isInitialize()) {
- postsViewModel.invoke(Arguments())
+ postsViewModel.invoke(postArgs)
} else {
postsViewModel.refresh()
}
- aiImageCountViewModel.invoke(Arguments())
- }
- val photoCount = if(aiImageState.value.isReady()) {
- aiImageState.value.data.familyAiImageCount
- } else {
- 0
+ aiImageCountViewModel.invoke(postArgs)
+ aiImageTypesViewModel.invoke(Arguments())
}
+ val matchedType = if (typesState.value.isReady()) {
+ typesState.value.data.results.find { it.aiPostType == aiPostType }
+ } else null
CustomAlertDialog(
title = "이미지사용약관",
description = "AI 이미지 기능을 사용하려면\n약관에 대한 동의가 필요해요",
@@ -101,11 +109,8 @@ fun FamilyStudioPage(
onDispose = onDispose,
title = "가족 사진관"
)
- val scrollState = rememberScrollState()
- Column(
- modifier = Modifier
- .verticalScroll(state = scrollState)
- ) {
+ if (matchedType != null) {
+ val dateRange = formatDateRange(matchedType.startDate, matchedType.endDate)
Column(
modifier = Modifier.padding(vertical = 20.dp, horizontal = 20.dp)
) {
@@ -122,14 +127,14 @@ fun FamilyStudioPage(
.padding(vertical = 2.dp, horizontal = 6.dp)
) {
Text(
- text = "추석",
+ text = matchedType.getTypeName(),
color = MaterialTheme.bbibbiScheme.backgroundPrimary,
style = MaterialTheme.bbibbiTypo.bodyTwoBold,
)
}
Box(modifier = Modifier.width(6.dp))
Text(
- text = "9/29~10/27",
+ text = dateRange,
color = MaterialTheme.bbibbiScheme.textPrimary,
style = MaterialTheme.bbibbiTypo.headTwoBold,
)
@@ -137,63 +142,65 @@ fun FamilyStudioPage(
AIPhotoInfoBaloon()
}
Text(
- text = "${photoCount}개의 추억",
+ text = "${matchedType.postCount}개의 추억",
color = MaterialTheme.bbibbiScheme.textPrimary,
style = MaterialTheme.bbibbiTypo.bodyOneRegular,
)
-
}
Box(modifier = Modifier.height(16.dp))
- Image(
- painter = painterResource(id = R.drawable.family_studio_banner),
+ AsyncImage(
+ model = asyncImagePainter(source = matchedType.imageUrl),
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
- contentScale = ContentScale.FillWidth
+ contentScale = ContentScale.FillWidth,
)
}
-
}
FamilyStudioPageFeed(
postItemsState = postsViewModel.uiState,
onTapContent = onTapAiPost,
onPullToRefresh = {
- aiImageCountViewModel.invoke(Arguments())
+ val refreshArgs = Arguments(
+ arguments = if (aiPostType.isNotEmpty()) mapOf("aiPostType" to aiPostType.uppercase()) else emptyMap()
+ )
+ aiImageCountViewModel.invoke(refreshArgs)
}
)
}
- Box(
- modifier = Modifier
- .padding(horizontal = 12.dp, vertical = 15.dp)
- .navigationBarsPadding()
- .align(Alignment.BottomCenter)
- ) {
-
- CustomCTAButton(
- modifier = Modifier.fillMaxWidth(),
- contentPadding = PaddingValues(vertical = 18.dp),
- onClick = onTapCreateImage,
- isActive = aiImageState.value.isReady() && aiImageState.value.data.hasAvailableImage()
+ if (isWithinDateRange(matchedType?.startDate, matchedType?.endDate)) {
+ Box(
+ modifier = Modifier
+ .padding(horizontal = 12.dp, vertical = 15.dp)
+ .navigationBarsPadding()
+ .align(Alignment.BottomCenter)
) {
- Text(
- text = "이미지 만들기",
- color = MaterialTheme.bbibbiScheme.backgroundPrimary,
- style = MaterialTheme.bbibbiTypo.bodyOneBold,
- )
- Image(
- painter = painterResource(id = R.drawable.ai),
- contentDescription = null,
- modifier = Modifier.size(22.dp),
- contentScale = ContentScale.FillWidth
- )
- if (aiImageState.value.isReady()) {
- val count = aiImageState.value.data
- Spacer(modifier = Modifier.width(3.dp))
+ CustomCTAButton(
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = PaddingValues(vertical = 18.dp),
+ onClick = onTapCreateImage,
+ isActive = aiImageState.value.isReady() && aiImageState.value.data.hasAvailableImage()
+ ) {
Text(
- text = "(${count.availableAiImageCount}/3)",
+ text = "이미지 만들기",
color = MaterialTheme.bbibbiScheme.backgroundPrimary,
- style = MaterialTheme.bbibbiTypo.bodyTwoRegular,
+ style = MaterialTheme.bbibbiTypo.bodyOneBold,
)
+ Image(
+ painter = painterResource(id = R.drawable.ai),
+ contentDescription = null,
+ modifier = Modifier.size(22.dp),
+ contentScale = ContentScale.FillWidth
+ )
+ if (aiImageState.value.isReady()) {
+ val count = aiImageState.value.data
+ Spacer(modifier = Modifier.width(3.dp))
+ Text(
+ text = "(${count.availableAiImageCount}/3)",
+ color = MaterialTheme.bbibbiScheme.backgroundPrimary,
+ style = MaterialTheme.bbibbiTypo.bodyTwoRegular,
+ )
+ }
}
}
}
@@ -204,6 +211,28 @@ fun FamilyStudioPage(
}
}
+private fun isWithinDateRange(startDate: String?, endDate: String?): Boolean {
+ if (startDate == null || endDate == null) return false
+ return try {
+ val today = LocalDate.now()
+ val start = LocalDate.parse(startDate)
+ val end = LocalDate.parse(endDate)
+ !today.isBefore(start) && !today.isAfter(end)
+ } catch (e: Exception) {
+ false
+ }
+}
+
+private fun formatDateRange(startDate: String, endDate: String): String {
+ return try {
+ val start = LocalDate.parse(startDate)
+ val end = LocalDate.parse(endDate)
+ "${start.monthValue}/${start.dayOfMonth}~${end.monthValue}/${end.dayOfMonth}"
+ } catch (e: Exception) {
+ "$startDate~$endDate"
+ }
+}
+
@Preview(
showBackground = true,
name = "FamilyStudioPage",
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/family_studio_upload/FamilyStudioUploadPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/family_studio_upload/FamilyStudioUploadPage.kt
index 1abb0b55..7a446cfb 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/family_studio_upload/FamilyStudioUploadPage.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/family_studio_upload/FamilyStudioUploadPage.kt
@@ -65,6 +65,7 @@ import java.util.UUID
@Composable
fun FamilyStudioUploadPage(
+ aiPostType: String = "",
onDispose: () -> Unit,
imageUrl: State,
convertAIImageViewModel: ConvertAIImageViewModel = hiltViewModel(),
@@ -141,14 +142,14 @@ fun FamilyStudioUploadPage(
isSaveIdle = convertResult.value.isReady(),
onClickUpload = {
mixPanel.track("Click_UploadPhoto")
- createPostViewModel.invoke(
- Arguments(
- arguments = mapOf(
- "imageUrl" to convertResult.value.data.imageUrl,
- "type" to "AI_IMAGE"
- )
- )
+ val args = mutableMapOf(
+ "imageUrl" to convertResult.value.data.imageUrl,
+ "type" to "AI_IMAGE",
)
+ if (aiPostType.isNotEmpty()) {
+ args["aiPostType"] = aiPostType
+ }
+ createPostViewModel.invoke(Arguments(arguments = args))
},
onClickSave = {
coroutineScope.launch {
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt
index c36ff26b..3190f31d 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePage.kt
@@ -53,7 +53,7 @@ fun HomePage(
onTapViewPost: (LocalDate) -> Unit = {},
onTapPick: (MainPageTopBarModel) -> Unit = {},
onTapNight: () -> Unit = {},
- onTapFamilyStudio: () -> Unit = {},
+ onTapFamilyStudio: (String) -> Unit = {},
) {
val postViewType by postViewTypeState
val mainPageState = mainPageViewModel.uiState.collectAsState()
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt
index 15e46118..adde22c8 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/home/HomePageContent.kt
@@ -2,6 +2,7 @@ package com.no5ing.bbibbi.presentation.feature.view.main.home
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
+import coil.compose.AsyncImage
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -61,7 +62,8 @@ import com.no5ing.bbibbi.presentation.component.AIPhotoInfoBaloon
import com.no5ing.bbibbi.presentation.component.BannerAd
import com.no5ing.bbibbi.presentation.component.VerticalGrid
import com.no5ing.bbibbi.presentation.feature.view.common.PostTypeSwitchButton
-import com.no5ing.bbibbi.presentation.feature.view_model.post.GetAiImageCountViewModel
+import com.no5ing.bbibbi.presentation.feature.view_model.post.GetAiImageTypesViewModel
+import com.no5ing.bbibbi.util.asyncImagePainter
import com.no5ing.bbibbi.presentation.theme.bbibbiScheme
import com.no5ing.bbibbi.presentation.theme.bbibbiTypo
import com.no5ing.bbibbi.util.gapBetweenNow
@@ -79,7 +81,7 @@ fun HomePageContent(
onTapProfile: (String) -> Unit = {},
onTapPick: (MainPageTopBarModel) -> Unit = {},
onTapInvite: () -> Unit = {},
- onTapAi: () -> Unit = {},
+ onTapAi: (String) -> Unit = {},
onRefresh: () -> Unit = {},
) {
val warningState = remember {
@@ -305,64 +307,77 @@ fun MissionFeedTab(
@Composable
fun AIImageTab(
- onTap: () -> Unit,
- aiImageCountViewModel: GetAiImageCountViewModel = hiltViewModel(),
+ onTap: (String) -> Unit,
+ aiImageTypesViewModel: GetAiImageTypesViewModel = hiltViewModel(),
) {
- val aiImageState = aiImageCountViewModel.uiState.collectAsState()
+ val typesState = aiImageTypesViewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
- aiImageCountViewModel.invoke(Arguments())
- }
- val photoCount = if(aiImageState.value.isReady()) {
- aiImageState.value.data.familyAiImageCount
- } else {
- 0
+ aiImageTypesViewModel.invoke(Arguments())
}
+ if (!typesState.value.isReady()) return
+ val types = typesState.value.data.results
+ if (types.isEmpty()) return
+
Column(
- modifier = Modifier.padding(vertical = 20.dp, horizontal = 20.dp)
+ modifier = Modifier
+ .padding(vertical = 20.dp, horizontal = 20.dp)
) {
- Row(
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth()
- ) {
+ types.forEach { aiType ->
+ val dateRange = formatDateRange(aiType.startDate, aiType.endDate)
Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
) {
- Box(modifier = Modifier
- .background(MaterialTheme.bbibbiScheme.icon, RoundedCornerShape(100.dp))
- .padding(vertical = 2.dp, horizontal = 6.dp)
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
) {
+ Box(modifier = Modifier
+ .background(MaterialTheme.bbibbiScheme.icon, RoundedCornerShape(100.dp))
+ .padding(vertical = 2.dp, horizontal = 6.dp)
+ ) {
+ Text(
+ text = aiType.getTypeName(),
+ color = MaterialTheme.bbibbiScheme.backgroundPrimary,
+ style = MaterialTheme.bbibbiTypo.bodyTwoBold,
+ )
+ }
+ Box(modifier = Modifier.width(6.dp))
Text(
- text = "추석",
- color = MaterialTheme.bbibbiScheme.backgroundPrimary,
- style = MaterialTheme.bbibbiTypo.bodyTwoBold,
+ text = dateRange,
+ color = MaterialTheme.bbibbiScheme.textPrimary,
+ style = MaterialTheme.bbibbiTypo.headTwoBold,
)
+ Spacer(modifier = Modifier.width(4.dp))
+ AIPhotoInfoBaloon()
}
- Box(modifier = Modifier.width(6.dp))
Text(
- text = "9/29~10/27",
+ text = "${aiType.postCount}개의 추억",
color = MaterialTheme.bbibbiScheme.textPrimary,
- style = MaterialTheme.bbibbiTypo.headTwoBold,
+ style = MaterialTheme.bbibbiTypo.bodyOneRegular,
)
- Spacer(modifier = Modifier.width(4.dp))
- AIPhotoInfoBaloon()
}
- Text(
- text = "${photoCount}개의 추억",
- color = MaterialTheme.bbibbiScheme.textPrimary,
- style = MaterialTheme.bbibbiTypo.bodyOneRegular,
+ Box(modifier = Modifier.height(16.dp))
+ AsyncImage(
+ model = asyncImagePainter(source = aiType.imageUrl),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onTap(aiType.aiPostType) },
+ contentScale = ContentScale.FillWidth,
)
-
+ Box(modifier = Modifier.height(24.dp))
}
- Box(modifier = Modifier.height(16.dp))
- Image(
- painter = painterResource(id = R.drawable.family_studio_banner),
- contentDescription = null,
- modifier = Modifier.fillMaxWidth().clickable {
- onTap()
- },
- contentScale = ContentScale.FillWidth
- )
+ }
+}
+
+private fun formatDateRange(startDate: String, endDate: String): String {
+ return try {
+ val start = LocalDate.parse(startDate)
+ val end = LocalDate.parse(endDate)
+ "${start.monthValue}/${start.dayOfMonth}~${end.monthValue}/${end.dayOfMonth}"
+ } catch (e: Exception) {
+ "$startDate~$endDate"
}
}
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/location_picker/LocationPickerPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/location_picker/LocationPickerPage.kt
new file mode 100644
index 00000000..7f6adcfa
--- /dev/null
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/location_picker/LocationPickerPage.kt
@@ -0,0 +1,218 @@
+package com.no5ing.bbibbi.presentation.feature.view.main.location_picker
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.no5ing.bbibbi.R
+import com.no5ing.bbibbi.presentation.component.BBiBBiSurface
+import com.no5ing.bbibbi.presentation.component.DisposableTopBar
+import com.no5ing.bbibbi.presentation.feature.view_model.location.LocationSearchViewModel
+import com.no5ing.bbibbi.presentation.theme.bbibbiScheme
+import com.no5ing.bbibbi.presentation.theme.bbibbiTypo
+
+@Composable
+fun LocationPickerPage(
+ onDispose: () -> Unit = {},
+ onConfirmLocation: (latitude: Double, longitude: Double, address: String) -> Unit = { _, _, _ -> },
+ currentLatitude: Double? = null,
+ currentLongitude: Double? = null,
+ locationSearchViewModel: LocationSearchViewModel = hiltViewModel(),
+) {
+ var query by remember { mutableStateOf("") }
+ val searchResults by locationSearchViewModel.searchResults.collectAsState()
+ val isLoading by locationSearchViewModel.isLoading.collectAsState()
+ val keyboardController = LocalSoftwareKeyboardController.current
+
+ LaunchedEffect(Unit) {
+ if (currentLatitude != null && currentLongitude != null) {
+ locationSearchViewModel.search("", currentLatitude, currentLongitude)
+ }
+ }
+
+ BBiBBiSurface(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ DisposableTopBar(
+ onDispose = onDispose,
+ title = stringResource(id = R.string.location_picker_title),
+ )
+
+ // Search bar
+ TextField(
+ value = query,
+ onValueChange = { query = it },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ placeholder = {
+ Text(
+ text = stringResource(id = R.string.location_search_hint),
+ color = MaterialTheme.bbibbiScheme.icon,
+ )
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(id = R.drawable.search_icon),
+ contentDescription = null,
+ tint = MaterialTheme.bbibbiScheme.icon,
+ modifier = Modifier.size(20.dp),
+ )
+ },
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ keyboardActions = KeyboardActions(
+ onSearch = {
+ keyboardController?.hide()
+ locationSearchViewModel.search(query, currentLatitude, currentLongitude)
+ }
+ ),
+ shape = RoundedCornerShape(12.dp),
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = MaterialTheme.bbibbiScheme.backgroundHover,
+ unfocusedContainerColor = MaterialTheme.bbibbiScheme.backgroundHover,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedTextColor = MaterialTheme.bbibbiScheme.textPrimary,
+ unfocusedTextColor = MaterialTheme.bbibbiScheme.textPrimary,
+ cursorColor = MaterialTheme.bbibbiScheme.mainYellow,
+ ),
+ )
+
+ // Results
+ if (isLoading) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(32.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator(
+ color = MaterialTheme.bbibbiScheme.mainYellow,
+ modifier = Modifier.size(24.dp),
+ strokeWidth = 2.dp,
+ )
+ }
+ } else if (searchResults.isEmpty()) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = "아직 위치가 없어요.\n검색으로 위치를 추가할 수 있어요.",
+ color = MaterialTheme.bbibbiScheme.gray500,
+ style = MaterialTheme.bbibbiTypo.bodyOneRegular,
+ textAlign = TextAlign.Center,
+ )
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ items(searchResults) { document ->
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ val lat = document.y.toDoubleOrNull() ?: return@clickable
+ val lng = document.x.toDoubleOrNull() ?: return@clickable
+ val address = document.placeName
+ onConfirmLocation(lat, lng, address)
+ }
+ .padding(horizontal = 20.dp, vertical = 14.dp),
+ ) {
+ Text(
+ text = document.placeName,
+ color = MaterialTheme.bbibbiScheme.textPrimary,
+ style = MaterialTheme.bbibbiTypo.bodyOneBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ val distanceText = when {
+ document.distance.isNotEmpty() -> formatDistance(document.distance)
+ currentLatitude != null && currentLongitude != null -> {
+ val docLat = document.y.toDoubleOrNull()
+ val docLng = document.x.toDoubleOrNull()
+ if (docLat != null && docLng != null) {
+ formatDistance(calculateDistance(currentLatitude, currentLongitude, docLat, docLng).toString())
+ } else null
+ }
+ else -> null
+ }
+ Text(
+ text = distanceText ?: document.roadAddressName.ifEmpty { document.addressName },
+ color = MaterialTheme.bbibbiScheme.textSecondary,
+ style = MaterialTheme.bbibbiTypo.bodyTwoRegular,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun calculateDistance(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Int {
+ val r = 6371000.0 // Earth radius in meters
+ val dLat = Math.toRadians(lat2 - lat1)
+ val dLng = Math.toRadians(lng2 - lng1)
+ val a = kotlin.math.sin(dLat / 2) * kotlin.math.sin(dLat / 2) +
+ kotlin.math.cos(Math.toRadians(lat1)) * kotlin.math.cos(Math.toRadians(lat2)) *
+ kotlin.math.sin(dLng / 2) * kotlin.math.sin(dLng / 2)
+ val c = 2 * kotlin.math.atan2(kotlin.math.sqrt(a), kotlin.math.sqrt(1 - a))
+ return (r * c).toInt()
+}
+
+private fun formatDistance(distanceStr: String): String {
+ val meters = distanceStr.toIntOrNull() ?: return distanceStr
+ return if (meters >= 1000) {
+ String.format("%.1fkm", meters / 1000.0)
+ } else {
+ "${meters}m"
+ }
+}
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt
index a1635b93..d44d4879 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/mission_upload/MissionUploadPage.kt
@@ -45,6 +45,9 @@ import com.no5ing.bbibbi.presentation.feature.view.main.post_upload.PostUploadPa
import com.no5ing.bbibbi.presentation.feature.view.main.post_upload.PostUploadPageUploadBar
import com.no5ing.bbibbi.presentation.feature.view_model.mission.GetTodayMissionViewModel
import com.no5ing.bbibbi.presentation.feature.view_model.post.CreatePostViewModel
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.isGranted
+import com.google.accompanist.permissions.rememberPermissionState
import com.no5ing.bbibbi.util.LocalMixpanelProvider
import com.no5ing.bbibbi.util.LocalSnackbarHostState
import com.no5ing.bbibbi.util.codePointLength
@@ -52,11 +55,16 @@ import com.no5ing.bbibbi.util.getErrorMessage
import kotlinx.coroutines.launch
+@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MissionUploadPage(
onDispose: () -> Unit,
+ onNavigateToLocationPicker: () -> Unit = {},
todayMissionViewModel: GetTodayMissionViewModel = hiltViewModel(),
imageUrl: State,
+ locationLatitude: State = remember { mutableStateOf(null) },
+ locationLongitude: State = remember { mutableStateOf(null) },
+ locationAddress: State = remember { mutableStateOf(null) },
imageText: MutableState = remember {
mutableStateOf("")
},
@@ -65,6 +73,25 @@ fun MissionUploadPage(
},
createPostViewModel: CreatePostViewModel = hiltViewModel(),
) {
+ var pendingLocationNavigation by remember { mutableStateOf(false) }
+ val locationPermissionState = rememberPermissionState(
+ permission = android.Manifest.permission.ACCESS_FINE_LOCATION
+ ) { isGranted ->
+ if (isGranted) {
+ onNavigateToLocationPicker()
+ }
+ }
+ LaunchedEffect(pendingLocationNavigation) {
+ if (pendingLocationNavigation) {
+ pendingLocationNavigation = false
+ if (locationPermissionState.status.isGranted) {
+ onNavigateToLocationPicker()
+ } else {
+ locationPermissionState.launchPermissionRequest()
+ }
+ }
+ }
+
LaunchedEffect(Unit) {
if (todayMissionViewModel.uiState.value.isIdle()) {
todayMissionViewModel.invoke(Arguments())
@@ -120,6 +147,7 @@ fun MissionUploadPage(
PostUploadPageImagePreview(
previewImgUrl = imageUrl.value,
imageTextState = imageText,
+ addressState = locationAddress,
onTapImageTextButton = {
mixPanel.track("Click_PhotoText")
textOverlayShown.value = true
@@ -128,16 +156,22 @@ fun MissionUploadPage(
Spacer(modifier = Modifier.height(48.dp))
PostUploadPageUploadBar(
isIdle = uploadResult.value.isIdle(),
+ showLocationButton = true,
+ onClickLocation = {
+ pendingLocationNavigation = true
+ },
onClickUpload = {
mixPanel.track("Click_UploadPhoto")
+ val args = mutableMapOf(
+ "imageUri" to imageUrl.value.toString(),
+ "content" to imageText.value,
+ "type" to "MISSION"
+ )
+ locationLatitude.value?.let { args["latitude"] = it.toString() }
+ locationLongitude.value?.let { args["longitude"] = it.toString() }
+ locationAddress.value?.let { args["address"] = it }
createPostViewModel.invoke(
- Arguments(
- arguments = mapOf(
- "imageUri" to imageUrl.value.toString(),
- "content" to imageText.value,
- "type" to "MISSION"
- )
- )
+ Arguments(arguments = args)
)
},
onClickSave = {
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPage.kt
index 3154bf88..0fb9b778 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPage.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPage.kt
@@ -44,6 +44,9 @@ import com.no5ing.bbibbi.presentation.component.showSnackBarWithDismiss
import com.no5ing.bbibbi.presentation.component.snackBarCamera
import com.no5ing.bbibbi.presentation.component.snackBarWarning
import com.no5ing.bbibbi.presentation.feature.view_model.post.CreatePostViewModel
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.isGranted
+import com.google.accompanist.permissions.rememberPermissionState
import com.no5ing.bbibbi.util.LocalMixpanelProvider
import com.no5ing.bbibbi.util.LocalSnackbarHostState
import com.no5ing.bbibbi.util.codePointLength
@@ -52,11 +55,16 @@ import com.no5ing.bbibbi.util.getErrorMessage
import kotlinx.coroutines.launch
+@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PostUploadPage(
onDispose: () -> Unit,
+ onNavigateToLocationPicker: () -> Unit = {},
isUnsaveMode: Boolean = false,
imageUrl: State,
+ locationLatitude: State = remember { mutableStateOf(null) },
+ locationLongitude: State = remember { mutableStateOf(null) },
+ locationAddress: State = remember { mutableStateOf(null) },
imageText: MutableState = remember {
mutableStateOf("")
},
@@ -70,6 +78,25 @@ fun PostUploadPage(
onDispose()
}
+ var pendingLocationNavigation by remember { mutableStateOf(false) }
+ val locationPermissionState = rememberPermissionState(
+ permission = android.Manifest.permission.ACCESS_FINE_LOCATION
+ ) { isGranted ->
+ if (isGranted) {
+ onNavigateToLocationPicker()
+ }
+ }
+ LaunchedEffect(pendingLocationNavigation) {
+ if (pendingLocationNavigation) {
+ pendingLocationNavigation = false
+ if (locationPermissionState.status.isGranted) {
+ onNavigateToLocationPicker()
+ } else {
+ locationPermissionState.launchPermissionRequest()
+ }
+ }
+ }
+
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val snackBarHost = LocalSnackbarHostState.current
@@ -121,6 +148,7 @@ fun PostUploadPage(
PostUploadPageImagePreview(
previewImgUrl = imageUrl.value,
imageTextState = imageText,
+ addressState = locationAddress,
onTapImageTextButton = {
mixPanel.track("Click_PhotoText")
textOverlayShown.value = true
@@ -129,15 +157,21 @@ fun PostUploadPage(
Spacer(modifier = Modifier.height(48.dp))
PostUploadPageUploadBar(
isIdle = uploadResult.value.isIdle(),
+ onClickLocation = {
+ pendingLocationNavigation = true
+ },
+ showLocationButton = true,
onClickUpload = {
mixPanel.track("Click_UploadPhoto")
+ val args = mutableMapOf(
+ "imageUri" to imageUrl.value.toString(),
+ "content" to imageText.value
+ )
+ locationLatitude.value?.let { args["latitude"] = it.toString() }
+ locationLongitude.value?.let { args["longitude"] = it.toString() }
+ locationAddress.value?.let { args["address"] = it }
createPostViewModel.invoke(
- Arguments(
- arguments = mapOf(
- "imageUri" to imageUrl.value.toString(),
- "content" to imageText.value
- )
- )
+ Arguments(arguments = args)
)
},
onClickSave = {
@@ -249,7 +283,8 @@ fun PostUploadPagePreview() {
)
Spacer(modifier = Modifier.height(48.dp))
PostUploadPageUploadBar(
- isIdle = true
+ isIdle = true,
+ showLocationButton = true,
)
}
AnimatedVisibility(
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPageImagePreview.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPageImagePreview.kt
index 4cbb06dd..01982cd7 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPageImagePreview.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPageImagePreview.kt
@@ -26,6 +26,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import com.no5ing.bbibbi.R
@@ -37,10 +38,12 @@ import com.no5ing.bbibbi.util.toCodePointList
fun PostUploadPageImagePreview(
previewImgUrl: Uri?,
imageTextState: State = mutableStateOf(""),
+ addressState: State = mutableStateOf(null),
supportText: Boolean = true,
onTapImageTextButton: () -> Unit = {},
) {
val imageText by imageTextState
+ val address by addressState
Box(
modifier = Modifier.fillMaxWidth()
) {
@@ -62,6 +65,38 @@ fun PostUploadPageImagePreview(
}
}
+ if (!address.isNullOrEmpty()) {
+ Box(
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .padding(start = 20.dp, top = 20.dp)
+ .background(
+ color = Color.Black.copy(alpha = 0.3f),
+ RoundedCornerShape(100.dp)
+ )
+ .padding(horizontal = 10.dp, vertical = 8.dp)
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.location_icon),
+ contentDescription = null,
+ tint = MaterialTheme.bbibbiScheme.mainYellow,
+ modifier = Modifier.size(16.dp)
+ )
+ Text(
+ text = address!!,
+ color = MaterialTheme.bbibbiScheme.mainYellow,
+ style = MaterialTheme.bbibbiTypo.bodyTwoBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+ }
+
if (supportText) {
Box(
modifier = Modifier
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPageUploadBar.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPageUploadBar.kt
index f6b30655..f1607e5a 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPageUploadBar.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_upload/PostUploadPageUploadBar.kt
@@ -21,8 +21,10 @@ import com.no5ing.bbibbi.presentation.component.button.CTAButton
fun PostUploadPageUploadBar(
isIdle: Boolean,
isSaveIdle: Boolean = true,
+ showLocationButton: Boolean = false,
onClickUpload: () -> Unit = {},
onClickSave: () -> Unit = {},
+ onClickLocation: () -> Unit = {},
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -31,7 +33,20 @@ fun PostUploadPageUploadBar(
Alignment.CenterHorizontally
),
) {
- Box(modifier = Modifier.size(48.dp))
+ if (showLocationButton) {
+ Image(
+ painter = painterResource(R.drawable.location_button),
+ contentDescription = null, // 필수 param
+ modifier = Modifier
+ .size(48.dp)
+ .clickable {
+ onClickLocation()
+ }
+ )
+ } else {
+ Box(modifier = Modifier.size(48.dp))
+ }
+
CTAButton(
text = stringResource(id = R.string.upload_image),
contentPadding = PaddingValues(horizontal = 60.dp, vertical = 15.dp),
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewContent.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewContent.kt
index bfc33b74..7681cc57 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewContent.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewContent.kt
@@ -10,8 +10,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
+import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -25,6 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -128,6 +131,37 @@ fun PostViewContent(
contentScale = ContentScale.Crop
)
}
+ if (post.address != null) {
+ Box(
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .padding(start = 20.dp, top = 20.dp)
+ .background(
+ color = Color.Black.copy(alpha = 0.3f),
+ RoundedCornerShape(100.dp)
+ )
+ .padding(horizontal = 10.dp, vertical = 8.dp)
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.location_icon),
+ contentDescription = null,
+ tint = MaterialTheme.bbibbiScheme.mainYellow,
+ modifier = Modifier.size(16.dp)
+ )
+ Text(
+ text = post.address,
+ color = MaterialTheme.bbibbiScheme.mainYellow,
+ style = MaterialTheme.bbibbiTypo.bodyTwoBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+ }
if (missionText != null) {
Box(
modifier = Modifier
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt
index 3f7a7c1b..080fec7f 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view/main/post_view/PostViewPage.kt
@@ -28,10 +28,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.blur
@@ -85,20 +85,20 @@ fun PostViewPage(
val memberId = LocalSessionState.current.memberId
var isPagerReady by remember { mutableStateOf(false) }
+ var siblingPostsLoaded by remember { mutableStateOf(false) }
val postState by postViewPageState.uiState.collectAsState()
val siblingPostState by familyPostsViewModel.uiState.collectAsState()
- val pagerState = key(siblingPostState) {
- rememberPagerState(
- initialPage = if (siblingPostState.isReady()) siblingPostState.data
- .indexOfFirst { it.post.postId == postId } else 0,
- pageCount = {
- if (siblingPostState.isReady()) siblingPostState.data.size else 1
- }
- )
- }
+ val pagerState = rememberPagerState(
+ initialPage = 0,
+ pageCount = {
+ if (siblingPostState.isReady()) siblingPostState.data.size else 1
+ }
+ )
val adView = getAdView()
+ // Load sibling posts once when postState becomes ready
LaunchedEffect(postState) {
- if (postState.isReady()) {
+ if (!siblingPostsLoaded && postState.isReady()) {
+ siblingPostsLoaded = true
val currentPost = postState.data.post
familyPostsViewModel.invoke(
Arguments(
@@ -109,8 +109,10 @@ fun PostViewPage(
)
}
}
- LaunchedEffect(postState, postCommentDialogState.value) {
- if (!postCommentDialogState.value && postState.isReady()) {
+ // Refresh sibling posts only when comment dialog actually closes
+ var wasCommentDialogOpen by remember { mutableStateOf(false) }
+ LaunchedEffect(postCommentDialogState.value) {
+ if (wasCommentDialogOpen && !postCommentDialogState.value && postState.isReady()) {
val currentPost = postState.data.post
familyPostsViewModel.invoke(
Arguments(
@@ -120,28 +122,50 @@ fun PostViewPage(
)
)
}
+ wasCommentDialogOpen = postCommentDialogState.value
}
+ // When sibling posts load, scroll pager to the correct post
LaunchedEffect(siblingPostState) {
if (siblingPostState.isReady()) {
+ val targetPage = siblingPostState.data
+ .indexOfFirst { it.post.postId == postId }
+ .coerceAtLeast(0)
+ pagerState.scrollToPage(targetPage)
isPagerReady = true
}
}
- LaunchedEffect(postState, pagerState.currentPage) {
+ // Load reactions for initial post
+ LaunchedEffect(postState) {
if (postState.isReady()) {
- val currentPostId =
- (if (siblingPostState.isReady()) siblingPostState.data.getOrNull(pagerState.currentPage)?.post?.postId
- else postState.data.post.postId)
- ?: return@LaunchedEffect
familyPostReactionBarViewModel.invoke(
Arguments(
arguments = mapOf(
- "postId" to currentPostId,
+ "postId" to postState.data.post.postId,
"memberId" to memberId
)
)
)
}
}
+ // Reload reactions when user swipes to a different page
+ LaunchedEffect(isPagerReady) {
+ if (isPagerReady) {
+ snapshotFlow { pagerState.currentPage }
+ .collect { page ->
+ val currentPostId =
+ siblingPostState.data.getOrNull(page)?.post?.postId
+ ?: return@collect
+ familyPostReactionBarViewModel.invoke(
+ Arguments(
+ arguments = mapOf(
+ "postId" to currentPostId,
+ "memberId" to memberId
+ )
+ )
+ )
+ }
+ }
+ }
BBiBBiSurface(modifier = Modifier.fillMaxSize()) {
Box {
AnimatedVisibility(
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt
index 739aa095..e63edc8c 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/NavigationDestination.kt
@@ -73,6 +73,7 @@ abstract class NavigationDestination(
internal const val cameraViewRoute = "common/camera"
internal const val uploadMissionPageRoute = "main/upload-mission"
internal const val uploadMissionPreviewPageRoute = "main/upload-mission-preview"
+ internal const val locationPickerRoute = "post/location-picker"
@OptIn(ExperimentalComposeUiApi::class)
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/FamilyStudioPageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/FamilyStudioPageController.kt
index fd13afbb..c26cba03 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/FamilyStudioPageController.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/FamilyStudioPageController.kt
@@ -6,6 +6,7 @@ import androidx.compose.runtime.remember
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
+import androidx.navigation.navArgument
import com.no5ing.bbibbi.BuildConfig
import com.no5ing.bbibbi.presentation.feature.view.main.family_studio.FamilyStudioPage
import com.no5ing.bbibbi.presentation.feature.view_controller.NavigationDestination
@@ -17,23 +18,26 @@ import com.no5ing.bbibbi.presentation.feature.view_controller.main.WebViewPageCo
import com.no5ing.bbibbi.presentation.feature.view_model.post.GetAiImageCountViewModel
object FamilyStudioPageController: NavigationDestination(
- route = mainFamilyStudioPageRoute
+ route = mainFamilyStudioPageRoute,
+ arguments = listOf(navArgument("aiPostType") {}),
) {
@Composable
override fun Render(navController: NavHostController, backStackEntry: NavBackStackEntry) {
+ val aiPostType = backStackEntry.arguments?.getString("aiPostType") ?: ""
val aiImageCountViewModel = hiltViewModel()
val dialogState = remember { mutableStateOf(false) }
if (aiImageCountViewModel.shouldShowTermDialog()) {
dialogState.value = true
}
FamilyStudioPage(
+ aiPostType = aiPostType,
isTermDialogEnabled = dialogState,
aiImageCountViewModel = aiImageCountViewModel,
onDispose = {
navController.popBackStack()
},
onTapCreateImage = {
- navController.goFamilyStudioUploadPage()
+ navController.goFamilyStudioUploadPage(aiPostType)
navController.goFamilyStudioCameraPage()
},
onTapAiPost = {
@@ -52,7 +56,7 @@ object FamilyStudioPageController: NavigationDestination(
)
}
- fun NavHostController.goFamilyStudioPage() {
- navigate(FamilyStudioPageController)
+ fun NavHostController.goFamilyStudioPage(aiPostType: String) {
+ navigate(FamilyStudioPageController, params = listOf("aiPostType" to aiPostType))
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/FamilyStudioUploadPageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/FamilyStudioUploadPageController.kt
index f12bc335..894b092a 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/FamilyStudioUploadPageController.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/FamilyStudioUploadPageController.kt
@@ -5,18 +5,22 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
+import androidx.navigation.navArgument
import com.no5ing.bbibbi.presentation.feature.view.main.family_studio_upload.FamilyStudioUploadPage
import com.no5ing.bbibbi.presentation.feature.view_controller.NavigationDestination
object FamilyStudioUploadPageController : NavigationDestination(
route = mainFamilyStudioUploadPageRoute,
+ arguments = listOf(navArgument("aiPostType") {}),
) {
@Composable
override fun Render(navController: NavHostController, backStackEntry: NavBackStackEntry) {
+ val aiPostType = backStackEntry.arguments?.getString("aiPostType") ?: ""
val imageCaptureState = backStackEntry.savedStateHandle
.getLiveData("imageUrl")
.observeAsState()
FamilyStudioUploadPage(
+ aiPostType = aiPostType,
imageUrl = imageCaptureState,
onDispose = {
navController.popBackStack()
@@ -24,7 +28,7 @@ object FamilyStudioUploadPageController : NavigationDestination(
)
}
- fun NavHostController.goFamilyStudioUploadPage() {
- navigate(FamilyStudioUploadPageController)
+ fun NavHostController.goFamilyStudioUploadPage(aiPostType: String) {
+ navigate(FamilyStudioUploadPageController, params = listOf("aiPostType" to aiPostType))
}
}
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt
index caf0327a..859471a6 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/HomePageController.kt
@@ -215,8 +215,8 @@ object HomePageController : NavigationDestination(
onTapNight = {
isNightTimeDialogVisible = true
},
- onTapFamilyStudio = {
- navController.goFamilyStudioPage()
+ onTapFamilyStudio = { aiPostType ->
+ navController.goFamilyStudioPage(aiPostType)
}
)
}
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/LocationPickerPageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/LocationPickerPageController.kt
new file mode 100644
index 00000000..66413337
--- /dev/null
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/LocationPickerPageController.kt
@@ -0,0 +1,36 @@
+package com.no5ing.bbibbi.presentation.feature.view_controller.main
+
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavHostController
+import com.no5ing.bbibbi.presentation.feature.view.main.location_picker.LocationPickerPage
+import com.no5ing.bbibbi.presentation.feature.view_controller.NavigationDestination
+
+object LocationPickerPageController : NavigationDestination(
+ route = locationPickerRoute,
+) {
+ const val LOCATION_LAT_KEY = "locationLat"
+ const val LOCATION_LNG_KEY = "locationLng"
+ const val LOCATION_ADDRESS_KEY = "locationAddress"
+
+ @Composable
+ override fun Render(navController: NavHostController, backStackEntry: NavBackStackEntry) {
+ LocationPickerPage(
+ onDispose = {
+ navController.popBackStack()
+ },
+ onConfirmLocation = { lat, lng, address ->
+ navController.previousBackStackEntry?.savedStateHandle?.apply {
+ set(LOCATION_LAT_KEY, lat)
+ set(LOCATION_LNG_KEY, lng)
+ set(LOCATION_ADDRESS_KEY, address)
+ }
+ navController.popBackStack()
+ },
+ )
+ }
+
+ fun NavHostController.goLocationPickerPage() {
+ navigate(LocationPickerPageController)
+ }
+}
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/MissionUploadPageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/MissionUploadPageController.kt
index d33d02b0..4d3fe477 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/MissionUploadPageController.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/MissionUploadPageController.kt
@@ -7,6 +7,10 @@ import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
import com.no5ing.bbibbi.presentation.feature.view.main.mission_upload.MissionUploadPage
import com.no5ing.bbibbi.presentation.feature.view_controller.NavigationDestination
+import com.no5ing.bbibbi.presentation.feature.view_controller.main.LocationPickerPageController.LOCATION_ADDRESS_KEY
+import com.no5ing.bbibbi.presentation.feature.view_controller.main.LocationPickerPageController.LOCATION_LAT_KEY
+import com.no5ing.bbibbi.presentation.feature.view_controller.main.LocationPickerPageController.LOCATION_LNG_KEY
+import com.no5ing.bbibbi.presentation.feature.view_controller.main.LocationPickerPageController.goLocationPickerPage
object MissionUploadPageController : NavigationDestination(
route = uploadMissionPreviewPageRoute,
@@ -16,11 +20,26 @@ object MissionUploadPageController : NavigationDestination(
val imageCaptureState = backStackEntry.savedStateHandle
.getLiveData("imageUrl")
.observeAsState()
+ val locationLat = backStackEntry.savedStateHandle
+ .getLiveData(LOCATION_LAT_KEY)
+ .observeAsState()
+ val locationLng = backStackEntry.savedStateHandle
+ .getLiveData(LOCATION_LNG_KEY)
+ .observeAsState()
+ val locationAddress = backStackEntry.savedStateHandle
+ .getLiveData(LOCATION_ADDRESS_KEY)
+ .observeAsState()
MissionUploadPage(
imageUrl = imageCaptureState,
+ locationLatitude = locationLat,
+ locationLongitude = locationLng,
+ locationAddress = locationAddress,
onDispose = {
navController.popBackStack()
},
+ onNavigateToLocationPicker = {
+ navController.goLocationPickerPage()
+ },
)
}
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/PostUploadPageController.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/PostUploadPageController.kt
index dbe4f1eb..097e232c 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/PostUploadPageController.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_controller/main/PostUploadPageController.kt
@@ -10,6 +10,10 @@ import androidx.navigation.NavHostController
import androidx.navigation.navArgument
import com.no5ing.bbibbi.presentation.feature.view.main.post_upload.PostUploadPage
import com.no5ing.bbibbi.presentation.feature.view_controller.NavigationDestination
+import com.no5ing.bbibbi.presentation.feature.view_controller.main.LocationPickerPageController.LOCATION_ADDRESS_KEY
+import com.no5ing.bbibbi.presentation.feature.view_controller.main.LocationPickerPageController.LOCATION_LAT_KEY
+import com.no5ing.bbibbi.presentation.feature.view_controller.main.LocationPickerPageController.LOCATION_LNG_KEY
+import com.no5ing.bbibbi.presentation.feature.view_controller.main.LocationPickerPageController.goLocationPickerPage
object PostUploadPageController : NavigationDestination(
route = postUploadRoute,
@@ -19,11 +23,26 @@ object PostUploadPageController : NavigationDestination(
val imageCaptureState = backStackEntry.savedStateHandle
.getLiveData("imageUrl")
.observeAsState()
+ val locationLat = backStackEntry.savedStateHandle
+ .getLiveData(LOCATION_LAT_KEY)
+ .observeAsState()
+ val locationLng = backStackEntry.savedStateHandle
+ .getLiveData(LOCATION_LNG_KEY)
+ .observeAsState()
+ val locationAddress = backStackEntry.savedStateHandle
+ .getLiveData(LOCATION_ADDRESS_KEY)
+ .observeAsState()
PostUploadPage(
imageUrl = imageCaptureState,
+ locationLatitude = locationLat,
+ locationLongitude = locationLng,
+ locationAddress = locationAddress,
onDispose = {
navController.popBackStack()
},
+ onNavigateToLocationPicker = {
+ navController.goLocationPickerPage()
+ },
)
}
@@ -40,12 +59,27 @@ object PostReUploadPageController : NavigationDestination(
override fun Render(navController: NavHostController, backStackEntry: NavBackStackEntry) {
val imageCaptureState = backStackEntry.arguments?.getString("imageUrl")
val uriState = remember { mutableStateOf(Uri.parse(imageCaptureState)) }
+ val locationLat = backStackEntry.savedStateHandle
+ .getLiveData(LOCATION_LAT_KEY)
+ .observeAsState()
+ val locationLng = backStackEntry.savedStateHandle
+ .getLiveData(LOCATION_LNG_KEY)
+ .observeAsState()
+ val locationAddress = backStackEntry.savedStateHandle
+ .getLiveData(LOCATION_ADDRESS_KEY)
+ .observeAsState()
PostUploadPage(
imageUrl = uriState,
isUnsaveMode = true,
+ locationLatitude = locationLat,
+ locationLongitude = locationLng,
+ locationAddress = locationAddress,
onDispose = {
navController.popBackStack()
},
+ onNavigateToLocationPicker = {
+ navController.goLocationPickerPage()
+ },
)
}
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/location/LocationSearchViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/location/LocationSearchViewModel.kt
new file mode 100644
index 00000000..44811810
--- /dev/null
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/location/LocationSearchViewModel.kt
@@ -0,0 +1,48 @@
+package com.no5ing.bbibbi.presentation.feature.view_model.location
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.no5ing.bbibbi.BuildConfig
+import com.no5ing.bbibbi.data.datasource.network.KakaoLocalApi
+import com.no5ing.bbibbi.data.datasource.network.response.KakaoSearchDocument
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import javax.inject.Inject
+
+@HiltViewModel
+class LocationSearchViewModel @Inject constructor(
+ private val kakaoLocalApi: KakaoLocalApi,
+) : ViewModel() {
+
+ private val _searchResults = MutableStateFlow>(emptyList())
+ val searchResults: StateFlow> = _searchResults
+
+ private val _isLoading = MutableStateFlow(false)
+ val isLoading: StateFlow = _isLoading
+
+ fun search(query: String, latitude: Double?, longitude: Double?) {
+ viewModelScope.launch(Dispatchers.IO) {
+ _isLoading.value = true
+ try {
+ val response = kakaoLocalApi.searchKeyword(
+ authorization = "KakaoAK ${BuildConfig.kakaoRestApiKey}",
+ query = query,
+ x = longitude?.toString(),
+ y = latitude?.toString(),
+ radius = if (latitude != null && longitude != null) 20000 else null,
+ size = 15,
+ )
+ _searchResults.value = response.documents
+ } catch (e: Exception) {
+ Timber.e(e, "Kakao location search failed")
+ _searchResults.value = emptyList()
+ } finally {
+ _isLoading.value = false
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreateFamilyStudioPostViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreateFamilyStudioPostViewModel.kt
index 9b80525f..429f3003 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreateFamilyStudioPostViewModel.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreateFamilyStudioPostViewModel.kt
@@ -35,7 +35,8 @@ class CreateFamilyStudioPostViewModel @Inject constructor(
content = "",
uploadTime = getZonedDateTimeString(),
),
- type = arguments.get("type")
+ type = arguments.get("type"),
+ aiPostType = arguments.get("aiPostType")?.uppercase(),
).wrapToAPIResponse()
setState(postResult)
}
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreatePostViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreatePostViewModel.kt
index 54d26bac..98405697 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreatePostViewModel.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/CreatePostViewModel.kt
@@ -67,6 +67,9 @@ class CreatePostViewModel @Inject constructor(
imageUrl = imageUploadResult,
content = content,
uploadTime = getZonedDateTimeString(),
+ latitude = arguments.get("latitude")?.toDoubleOrNull(),
+ longitude = arguments.get("longitude")?.toDoubleOrNull(),
+ address = arguments.get("address"),
),
type = arguments.get("type")
).wrapToAPIResponse()
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/GetAiImageCountViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/GetAiImageCountViewModel.kt
index b785dede..6061b290 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/GetAiImageCountViewModel.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/GetAiImageCountViewModel.kt
@@ -29,7 +29,9 @@ class GetAiImageCountViewModel @Inject constructor(
override fun invoke(arguments: Arguments) {
withMutexScope(Dispatchers.IO) {
- val result = restAPI.getPostApi().getAiImagePostCount()
+ val result = restAPI.getPostApi().getAiImagePostCount(
+ aiPostType = arguments.get("aiPostType"),
+ )
setState(result.wrapToAPIResponse())
}
}
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/GetAiImageTypesViewModel.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/GetAiImageTypesViewModel.kt
new file mode 100644
index 00000000..614868d4
--- /dev/null
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/feature/view_model/post/GetAiImageTypesViewModel.kt
@@ -0,0 +1,29 @@
+package com.no5ing.bbibbi.presentation.feature.view_model.post
+
+import com.no5ing.bbibbi.data.datasource.network.RestAPI
+import com.no5ing.bbibbi.data.datasource.network.response.ArrayResponse
+import com.no5ing.bbibbi.data.model.APIResponse
+import com.no5ing.bbibbi.data.model.APIResponse.Companion.wrapToAPIResponse
+import com.no5ing.bbibbi.data.model.post.AIPostType
+import com.no5ing.bbibbi.data.repository.Arguments
+import com.no5ing.bbibbi.presentation.feature.view_model.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import javax.inject.Inject
+
+@HiltViewModel
+class GetAiImageTypesViewModel @Inject constructor(
+ private val restAPI: RestAPI,
+) : BaseViewModel>>() {
+
+ override fun initState(): APIResponse> {
+ return APIResponse.idle()
+ }
+
+ override fun invoke(arguments: Arguments) {
+ withMutexScope(Dispatchers.IO) {
+ val result = restAPI.getPostApi().getAiImageTypes()
+ setState(result.wrapToAPIResponse())
+ }
+ }
+}
diff --git a/app/src/main/java/com/no5ing/bbibbi/presentation/navigation/graph/MainNavGraph.kt b/app/src/main/java/com/no5ing/bbibbi/presentation/navigation/graph/MainNavGraph.kt
index e81eb43a..de3a461e 100644
--- a/app/src/main/java/com/no5ing/bbibbi/presentation/navigation/graph/MainNavGraph.kt
+++ b/app/src/main/java/com/no5ing/bbibbi/presentation/navigation/graph/MainNavGraph.kt
@@ -20,6 +20,7 @@ import com.no5ing.bbibbi.presentation.feature.view_controller.main.FamilyStudioP
import com.no5ing.bbibbi.presentation.feature.view_controller.main.FamilyStudioUploadPageController
import com.no5ing.bbibbi.presentation.feature.view_controller.main.HomePageController
import com.no5ing.bbibbi.presentation.feature.view_controller.main.ImagePreviewPageController
+import com.no5ing.bbibbi.presentation.feature.view_controller.main.LocationPickerPageController
import com.no5ing.bbibbi.presentation.feature.view_controller.main.MissionUploadPageController
import com.no5ing.bbibbi.presentation.feature.view_controller.main.NotificationPageController
import com.no5ing.bbibbi.presentation.feature.view_controller.main.PostReUploadPageController
@@ -205,5 +206,15 @@ fun NavGraphBuilder.mainGraph(
controller = navController,
destination = QuitPageController,
)
+ composable(
+ controller = navController,
+ destination = LocationPickerPageController,
+ enterTransition = {
+ fullHorizontalSlideInToLeft()
+ },
+ popExitTransition = {
+ fullHorizontalSlideOutToRight()
+ }
+ )
}
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/location_button.xml b/app/src/main/res/drawable/location_button.xml
new file mode 100644
index 00000000..dae6d109
--- /dev/null
+++ b/app/src/main/res/drawable/location_button.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/location_icon.xml b/app/src/main/res/drawable/location_icon.xml
new file mode 100644
index 00000000..b096c57b
--- /dev/null
+++ b/app/src/main/res/drawable/location_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/search_icon.xml b/app/src/main/res/drawable/search_icon.xml
new file mode 100644
index 00000000..5ff45745
--- /dev/null
+++ b/app/src/main/res/drawable/search_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
index 4c9db3f4..1b99fe47 100644
--- a/app/src/main/res/values-en/strings.xml
+++ b/app/src/main/res/values-en/strings.xml
@@ -226,4 +226,7 @@
Survival
Mission
+
+ Search Location
+ Search
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index e67c5953..15bc19e3 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -226,4 +226,7 @@
生存
ミッション
+
+ 場所検索
+ 検索
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 78a38637..5e0d7840 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -230,4 +230,7 @@
생존
미션
+
+ 위치 검색
+ 검색
\ No newline at end of file