diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml
index 82e6fb3cb..6f7fc81c7 100644
--- a/.github/workflows/android_cd.yml
+++ b/.github/workflows/android_cd.yml
@@ -38,6 +38,8 @@ jobs:
echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> local.properties
echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> local.properties
echo "AMPLITUDE_API_KEY=${{ secrets.AMPLITUDE_API_KEY }}" >> local.properties
+ echo "KAKAO_APP_KEY=${{ secrets.KAKAO_APP_KEY }}" >> local.properties
+ echo "APPSFLYER_API_KEY=${{ secrets.APPSFLYER_API_KEY }}" >> local.properties
- name: Set up keystore.properties
run: |
diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml
index 6142e91b4..a5826aa16 100644
--- a/.github/workflows/android_ci.yml
+++ b/.github/workflows/android_ci.yml
@@ -41,6 +41,8 @@ jobs:
echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> local.properties
echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> local.properties
echo "AMPLITUDE_API_KEY=${{ secrets.AMPLITUDE_API_KEY }}" >> local.properties
+ echo "KAKAO_APP_KEY=${{ secrets.KAKAO_APP_KEY }}" >> local.properties
+ echo "APPSFLYER_API_KEY=${{ secrets.APPSFLYER_API_KEY }}" >> local.properties
- name: Set up keystore.properties
run: |
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9578c4df6..a29b45c56 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -11,8 +11,8 @@ android {
namespace = "com.idle.care"
defaultConfig {
- versionCode = 14
- versionName = "1.1.5"
+ versionCode = 15
+ versionName = "1.2.0"
targetSdk = 34
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -20,18 +20,20 @@ android {
val localProperties = Properties()
localProperties.load(project.rootProject.file("local.properties").bufferedReader())
manifestPlaceholders["NAVER_CLIENT_ID"] = localProperties["NAVER_CLIENT_ID"] as String
+ manifestPlaceholders["KAKAO_APP_KEY"] = localProperties["KAKAO_APP_KEY"] as String
buildConfigField(
- "String",
- "AMPLITUDE_API_KEY",
- "\"${localProperties["AMPLITUDE_API_KEY"]}\"",
+ "String", "AMPLITUDE_API_KEY", "\"${localProperties["AMPLITUDE_API_KEY"]}\"",
)
+ buildConfigField("String", "KAKAO_APP_KEY", "\"${localProperties["KAKAO_APP_KEY"]}\"")
+ buildConfigField("String", "APPSFLYER_API_KEY", "\"${localProperties["APPSFLYER_API_KEY"]}\"")
}
signingConfigs {
create("release") {
val keystoreProperties = Properties()
- keystoreProperties.load(project.rootProject.file("keystore.properties").bufferedReader()
+ keystoreProperties.load(
+ project.rootProject.file("keystore.properties").bufferedReader()
)
storeFile = file(keystoreProperties["STORE_FILE_PATH"] as String)
@@ -61,4 +63,7 @@ dependencies {
implementation(projects.core.analytics)
implementation(libs.firebase.messaging)
+ implementation(libs.kakao.common)
+ implementation(libs.appsFlyer)
+ implementation(libs.installerReferrer)
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0c4132867..b83e5b636 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,7 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.idle.care">
@@ -16,6 +17,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Care"
+ tools:replace="android:dataExtractionRules, android:fullBackupContent"
tools:targetApi="34">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/idle/care/CareApplication.kt b/app/src/main/java/com/idle/care/CareApplication.kt
index 45e535a07..5f5f8f4ca 100644
--- a/app/src/main/java/com/idle/care/CareApplication.kt
+++ b/app/src/main/java/com/idle/care/CareApplication.kt
@@ -3,15 +3,31 @@ package com.idle.care
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
+import com.appsflyer.AppsFlyerLib
+import com.appsflyer.attribution.AppsFlyerRequestListener
+import com.idle.analytics.error.ErrorLoggingHelper
import com.idle.care.notification.NotificationHandler.Companion.BACKGROUND_CHANNEL
import com.idle.care.notification.NotificationHandler.Companion.BACKGROUND_DESCRIPTION
+import com.kakao.sdk.common.KakaoSdk
import dagger.hilt.android.HiltAndroidApp
+import javax.inject.Inject
+
@HiltAndroidApp
class CareApplication : Application() {
+
+ @Inject
+ lateinit var errorLoggingHelper: ErrorLoggingHelper
+
override fun onCreate() {
super.onCreate()
+ initNotification()
+ initKakao()
+ initAppsFlyer()
+ }
+
+ private fun initNotification() {
val channel =
NotificationChannel(
BACKGROUND_CHANNEL,
@@ -23,4 +39,21 @@ class CareApplication : Application() {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
+
+ private fun initKakao() {
+ KakaoSdk.init(this, BuildConfig.KAKAO_APP_KEY)
+ }
+
+ private fun initAppsFlyer() {
+ AppsFlyerLib.getInstance().apply {
+ init(BuildConfig.APPSFLYER_API_KEY, null, this@CareApplication)
+ setDebugLog(true)
+ start(this@CareApplication, "", object : AppsFlyerRequestListener {
+ override fun onSuccess() {}
+ override fun onError(p0: Int, p1: String) {
+ errorLoggingHelper.logError(Exception("AppsFlyer 연동 실패 $p0 $p1"))
+ }
+ })
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/idle/care/notification/NotificationService.kt b/app/src/main/java/com/idle/care/notification/NotificationService.kt
index 78e8007f5..973b25441 100644
--- a/app/src/main/java/com/idle/care/notification/NotificationService.kt
+++ b/app/src/main/java/com/idle/care/notification/NotificationService.kt
@@ -1,9 +1,8 @@
package com.idle.care.notification
-import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
-import com.idle.domain.model.auth.UserType
+import com.idle.analytics.error.ErrorLoggingHelper
import com.idle.domain.usecase.auth.GetUserTypeUseCase
import com.idle.domain.usecase.notification.PostDeviceTokenUseCase
import dagger.hilt.android.AndroidEntryPoint
@@ -26,9 +25,12 @@ class NotificationService : FirebaseMessagingService() {
@Inject
lateinit var notificationHandler: NotificationHandler
+ @Inject
+ lateinit var errorLoggingHelper: ErrorLoggingHelper
+
private val job = SupervisorJob()
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
- Log.e("test", throwable.stackTraceToString())
+ errorLoggingHelper.logError(throwable)
}
private val scope = CoroutineScope(Dispatchers.IO + job + coroutineExceptionHandler)
@@ -38,12 +40,10 @@ class NotificationService : FirebaseMessagingService() {
scope.launch {
val userType = getUserTypeUseCase()
- when (userType) {
- UserType.CENTER.apiValue, UserType.WORKER.apiValue -> postDeviceTokenUseCase(
- deviceToken = token,
- userType = userType,
- )
- }
+ postDeviceTokenUseCase(
+ deviceToken = token,
+ userType = userType,
+ )
}
}
diff --git a/core/common-ui/binding/src/main/java/com/idle/binding/EventHandlerHelper.kt b/core/common-ui/binding/src/main/java/com/idle/binding/EventHandlerHelper.kt
index a378b3ad7..16e3512d6 100644
--- a/core/common-ui/binding/src/main/java/com/idle/binding/EventHandlerHelper.kt
+++ b/core/common-ui/binding/src/main/java/com/idle/binding/EventHandlerHelper.kt
@@ -21,6 +21,7 @@ sealed class MainEvent {
MainEvent()
data object DismissToast : MainEvent()
+ data class ShareJobPosting(val shareJobPostingInfo: ShareJobPostingInfo) : MainEvent()
}
enum class ToastType {
@@ -32,3 +33,16 @@ enum class ToastType {
}
}
}
+
+data class ShareJobPostingInfo(
+ val id: String,
+ val title: String,
+ val weekdays: String,
+ val workTime: String,
+ val payAmount: String,
+ val roadNameAddress: String,
+ val gender: String,
+ val centerName: String,
+ val centerOfficeNumber: String,
+ val type: String,
+)
diff --git a/core/data/src/main/java/com/idle/data/repository/jobposting/JobPostingRepositoryImpl.kt b/core/data/src/main/java/com/idle/data/repository/jobposting/JobPostingRepositoryImpl.kt
index 0be27ce9e..09f606a3b 100644
--- a/core/data/src/main/java/com/idle/data/repository/jobposting/JobPostingRepositoryImpl.kt
+++ b/core/data/src/main/java/com/idle/data/repository/jobposting/JobPostingRepositoryImpl.kt
@@ -14,6 +14,7 @@ import com.idle.domain.model.jobposting.JobPostingType
import com.idle.domain.model.jobposting.LifeAssistance
import com.idle.domain.model.jobposting.MentalStatus
import com.idle.domain.model.jobposting.PayType
+import com.idle.domain.model.jobposting.SharedJobPostingInfo
import com.idle.domain.model.jobposting.WorkerJobPosting
import com.idle.domain.model.jobposting.WorkerJobPostingDetail
import com.idle.domain.repositorry.jobposting.JobPostingRepository
@@ -26,6 +27,13 @@ import javax.inject.Inject
class JobPostingRepositoryImpl @Inject constructor(
private val jobPostingDataSource: JobPostingDataSource
) : JobPostingRepository {
+ override var sharedJobPostingInfo: SharedJobPostingInfo? = null
+ get() {
+ val currentValue = field
+ field = null
+ return currentValue
+ }
+
override suspend fun postJobPosting(
weekdays: List,
startTime: String,
diff --git a/core/designresource/src/main/res/drawable/ic_share.xml b/core/designresource/src/main/res/drawable/ic_share.xml
new file mode 100644
index 000000000..9e94a389f
--- /dev/null
+++ b/core/designresource/src/main/res/drawable/ic_share.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/core/designresource/src/main/res/values/strings.xml b/core/designresource/src/main/res/values/strings.xml
index bb865bd36..9818d7bbb 100644
--- a/core/designresource/src/main/res/values/strings.xml
+++ b/core/designresource/src/main/res/values/strings.xml
@@ -246,6 +246,7 @@
개인정보 처리 방침
자주 묻는 질문
문의하기
+ 공유하기
통화하기
전화로 문의하기
채팅으로 문의하기
diff --git a/core/domain/src/main/kotlin/com/idle/domain/model/jobposting/SharedJobPostingInfo.kt b/core/domain/src/main/kotlin/com/idle/domain/model/jobposting/SharedJobPostingInfo.kt
new file mode 100644
index 000000000..0afa7c0a6
--- /dev/null
+++ b/core/domain/src/main/kotlin/com/idle/domain/model/jobposting/SharedJobPostingInfo.kt
@@ -0,0 +1,6 @@
+package com.idle.domain.model.jobposting
+
+data class SharedJobPostingInfo(
+ val jobPostingId: String,
+ val jobPostingType: JobPostingType,
+)
diff --git a/core/domain/src/main/kotlin/com/idle/domain/model/notification/Notification.kt b/core/domain/src/main/kotlin/com/idle/domain/model/notification/Notification.kt
index 04495e025..bdde75602 100644
--- a/core/domain/src/main/kotlin/com/idle/domain/model/notification/Notification.kt
+++ b/core/domain/src/main/kotlin/com/idle/domain/model/notification/Notification.kt
@@ -16,6 +16,7 @@ data class Notification(
enum class NotificationType(private val notificationTypeClass: Type) {
APPLICANT(NotificationContent.ApplicantNotification::class.java),
+ NEW_JOB_POSTING(NotificationContent.NewJobPostingNotification::class.java),
UNKNOWN(NotificationContent.UnKnownNotification::class.java);
companion object {
@@ -27,5 +28,6 @@ enum class NotificationType(private val notificationTypeClass: Type) {
sealed class NotificationContent {
data class ApplicantNotification(val jobPostingId: String) : NotificationContent()
+ data class NewJobPostingNotification(val jobPostingId: String) : NotificationContent()
data object UnKnownNotification : NotificationContent()
}
\ No newline at end of file
diff --git a/core/domain/src/main/kotlin/com/idle/domain/model/profile/WorkerProfile.kt b/core/domain/src/main/kotlin/com/idle/domain/model/profile/WorkerProfile.kt
index 0d3372a8f..9d94bf3e4 100644
--- a/core/domain/src/main/kotlin/com/idle/domain/model/profile/WorkerProfile.kt
+++ b/core/domain/src/main/kotlin/com/idle/domain/model/profile/WorkerProfile.kt
@@ -17,4 +17,4 @@ data class WorkerProfile(
val introduce: String?,
val speciality: String?,
val profileImageUrl: String?,
-)
\ No newline at end of file
+)
diff --git a/core/domain/src/main/kotlin/com/idle/domain/repositorry/jobposting/JobPostingRepository.kt b/core/domain/src/main/kotlin/com/idle/domain/repositorry/jobposting/JobPostingRepository.kt
index 9f1922b81..c600187f7 100644
--- a/core/domain/src/main/kotlin/com/idle/domain/repositorry/jobposting/JobPostingRepository.kt
+++ b/core/domain/src/main/kotlin/com/idle/domain/repositorry/jobposting/JobPostingRepository.kt
@@ -14,10 +14,13 @@ import com.idle.domain.model.jobposting.JobPostingType
import com.idle.domain.model.jobposting.LifeAssistance
import com.idle.domain.model.jobposting.MentalStatus
import com.idle.domain.model.jobposting.PayType
+import com.idle.domain.model.jobposting.SharedJobPostingInfo
import com.idle.domain.model.jobposting.WorkerJobPosting
import com.idle.domain.model.jobposting.WorkerJobPostingDetail
interface JobPostingRepository {
+ var sharedJobPostingInfo: SharedJobPostingInfo?
+
suspend fun postJobPosting(
weekdays: List,
startTime: String,
diff --git a/core/navigation/src/main/java/com/idle/navigation/NavigationHelper.kt b/core/navigation/src/main/java/com/idle/navigation/NavigationHelper.kt
index 5c2fbe6be..53dfc071d 100644
--- a/core/navigation/src/main/java/com/idle/navigation/NavigationHelper.kt
+++ b/core/navigation/src/main/java/com/idle/navigation/NavigationHelper.kt
@@ -1,14 +1,14 @@
package com.idle.navigation
import android.os.Bundle
-import com.idle.navigation.DeepLinkDestination.CenterApplicantInquiry
-import com.idle.navigation.DeepLinkDestination.CenterHome
-import com.idle.navigation.DeepLinkDestination.CenterJobDetail
-import com.idle.navigation.DeepLinkDestination.WorkerJobDetail
import com.idle.domain.model.jobposting.JobPostingType
import com.idle.domain.model.notification.Notification
import com.idle.domain.model.notification.NotificationContent
import com.idle.domain.model.notification.NotificationType.APPLICANT
+import com.idle.domain.model.notification.NotificationType.NEW_JOB_POSTING
+import com.idle.navigation.DeepLinkDestination.CenterHome
+import com.idle.navigation.DeepLinkDestination.CenterJobDetail
+import com.idle.navigation.DeepLinkDestination.WorkerJobDetail
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
import kotlinx.coroutines.flow.receiveAsFlow
@@ -27,16 +27,18 @@ class NavigationHelper @Inject constructor() {
fun handleFCMNavigate(
isColdStart: Boolean,
extras: Bundle?,
- onInit: () -> Unit
+ onInit: () -> Unit,
+ readNotification: (String) -> Unit,
) {
+ val notificationId = extras?.getString(NotificationKeys.NOTIFICATION_ID) ?: run {
+ if (isColdStart) onInit()
+ return
+ }
+
+ readNotification(notificationId)
+
val notificationType = NotificationType.create(
- extras?.getString(NotificationKeys.NOTIFICATION_TYPE) ?: run {
- if (isColdStart) {
- onInit()
- return
- }
- return
- }
+ extras.getString(NotificationKeys.NOTIFICATION_TYPE) ?: return
)
when (notificationType) {
@@ -63,7 +65,7 @@ class NavigationHelper @Inject constructor() {
destinations.forEach { destination -> _navigationFlow.trySend(destination) }
}
- NotificationType.JOB_POSTING_DETAIL -> {
+ NotificationType.NEW_JOB_POSTING -> {
val jobPostingId = extras.getString(NotificationKeys.JOB_POSTING_ID) ?: run {
if (isColdStart) {
onInit()
@@ -92,20 +94,27 @@ class NavigationHelper @Inject constructor() {
fun handleNotificationNavigate(notification: Notification) {
val destinations = when (notification.notificationType) {
APPLICANT -> {
- val notificationContent =
- notification.notificationDetails as? NotificationContent.ApplicantNotification
+ (notification.notificationDetails as? NotificationContent.ApplicantNotification)?.let { content ->
+ listOf(NavigationEvent.NavigateTo(CenterJobDetail(content.jobPostingId)))
+ } ?: listOf()
+ }
- notificationContent?.let { content ->
+ NEW_JOB_POSTING -> {
+ (notification.notificationDetails as? NotificationContent.NewJobPostingNotification)?.let { content ->
listOf(
- NavigationEvent.NavigateTo(CenterJobDetail(content.jobPostingId)),
+ NavigationEvent.NavigateTo(
+ WorkerJobDetail(
+ content.jobPostingId,
+ JobPostingType.CAREMEET.name
+ )
+ )
)
} ?: listOf()
}
else -> listOf()
}
-
- destinations.onEach { destination -> _navigationFlow.trySend(destination) }
+ destinations.forEach { _navigationFlow.trySend(it) }
}
}
@@ -121,7 +130,7 @@ sealed class NavigationEvent {
enum class NotificationType {
APPLICANT,
- JOB_POSTING_DETAIL,
+ NEW_JOB_POSTING,
UNKNOWN;
companion object {
@@ -132,6 +141,7 @@ enum class NotificationType {
}
private object NotificationKeys {
+ const val NOTIFICATION_ID = "notificationId"
const val NOTIFICATION_TYPE = "notificationType"
const val JOB_POSTING_ID = "jobPostingId"
}
diff --git a/core/network/src/main/java/com/idle/network/serializer/NotificationSerializer.kt b/core/network/src/main/java/com/idle/network/serializer/NotificationSerializer.kt
index 3b4dd05ad..ac9fc06b5 100644
--- a/core/network/src/main/java/com/idle/network/serializer/NotificationSerializer.kt
+++ b/core/network/src/main/java/com/idle/network/serializer/NotificationSerializer.kt
@@ -49,6 +49,10 @@ class NotificationSerializer @Inject constructor() : KSerializer {
jobPostingId = details["jobPostingId"]?.jsonPrimitive?.content ?: ""
)
+ NotificationType.NEW_JOB_POSTING -> NotificationContent.NewJobPostingNotification(
+ jobPostingId = details["jobPostingId"]?.jsonPrimitive?.content ?: ""
+ )
+
NotificationType.UNKNOWN -> NotificationContent.UnKnownNotification
}
} ?: NotificationContent.UnKnownNotification
diff --git a/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/WorkerJobPostingDetailFragment.kt b/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/WorkerJobPostingDetailFragment.kt
index abbb626ab..09354d8a4 100644
--- a/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/WorkerJobPostingDetailFragment.kt
+++ b/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/WorkerJobPostingDetailFragment.kt
@@ -10,6 +10,8 @@ import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.navArgs
+import com.idle.binding.MainEvent
+import com.idle.binding.ShareJobPostingInfo
import com.idle.compose.base.BaseComposeFragment
import com.idle.designsystem.compose.component.CareStateAnimator
import com.idle.domain.model.jobposting.CrawlingJobPostingDetail
@@ -84,8 +86,33 @@ internal class WorkerJobPostingDetailFragment : BaseComposeFragment() {
navigationHelper.navigateTo(
com.idle.navigation.NavigationEvent.NavigateTo(it)
)
- }
-
+ },
+ shareJobPosting = {
+ eventHandlerHelper.sendEvent(
+ MainEvent.ShareJobPosting(
+ ShareJobPostingInfo(
+ id = jobPosting.id,
+ title = try {
+ jobPosting.lotNumberAddress.split(" ")
+ .subList(0, 3)
+ .joinToString(" ")
+ } catch (e: IndexOutOfBoundsException) {
+ ""
+ },
+ weekdays = jobPosting.weekdays.toList()
+ .sortedBy { it.ordinal }
+ .joinToString(",") { it.displayName },
+ workTime = "${jobPosting.startTime} - ${jobPosting.endTime}",
+ payAmount = "${jobPosting.payType.displayName} ${jobPosting.payAmount}원",
+ roadNameAddress = jobPosting.roadNameAddress,
+ gender = jobPosting.gender.displayName,
+ centerName = jobPosting.centerName,
+ centerOfficeNumber = jobPosting.centerOfficeNumber,
+ type = jobPosting.jobPostingType.name,
+ )
+ )
+ )
+ },
)
} else {
CrawlingJobPostingDetailScreen(
@@ -94,6 +121,24 @@ internal class WorkerJobPostingDetailFragment : BaseComposeFragment() {
showPlaceDetail = setShowPlaceDetail,
addFavoriteJobPosting = ::addFavoriteJobPosting,
removeFavoriteJobPosting = ::removeFavoriteJobPosting,
+ shareJobPosting = {
+ eventHandlerHelper.sendEvent(
+ MainEvent.ShareJobPosting(
+ ShareJobPostingInfo(
+ id = jobPosting.id,
+ title = jobPosting.title,
+ weekdays = jobPosting.workingSchedule,
+ workTime = jobPosting.workingTime,
+ payAmount = jobPosting.payInfo,
+ roadNameAddress = jobPosting.clientAddress,
+ gender = "-",
+ centerName = jobPosting.centerName,
+ centerOfficeNumber = jobPosting.centerAddress,
+ type = jobPosting.jobPostingType.name,
+ )
+ )
+ )
+ },
)
}
}
diff --git a/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/screen/CrawlingJobPostingDetailScreen.kt b/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/screen/CrawlingJobPostingDetailScreen.kt
index 0cf5351b5..0919acda2 100644
--- a/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/screen/CrawlingJobPostingDetailScreen.kt
+++ b/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/screen/CrawlingJobPostingDetailScreen.kt
@@ -14,6 +14,7 @@ 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.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -58,6 +59,7 @@ internal fun CrawlingJobPostingDetailScreen(
showPlaceDetail: (Boolean) -> Unit,
addFavoriteJobPosting: (String, JobPostingType) -> Unit,
removeFavoriteJobPosting: (String, JobPostingType) -> Unit,
+ shareJobPosting: () -> Unit,
) {
val onBackPressedDispatcher =
LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
@@ -76,6 +78,17 @@ internal fun CrawlingJobPostingDetailScreen(
end = 20.dp,
bottom = 12.dp
),
+ leftComponent = {
+ Image(
+ painter = painterResource(R.drawable.ic_share),
+ contentDescription = null,
+ modifier = Modifier
+ .size(32.dp)
+ .clickable(throttleTime = 2000L) {
+ shareJobPosting()
+ },
+ )
+ },
onNavigationClick = { onBackPressedDispatcher?.onBackPressed() },
)
},
diff --git a/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/screen/WorkerJobPostingDetailScreen.kt b/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/screen/WorkerJobPostingDetailScreen.kt
index 451319537..9a5c80f47 100644
--- a/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/screen/WorkerJobPostingDetailScreen.kt
+++ b/feature/job-posting-detail/src/main/java/com/idle/worker/job/posting/detail/worker/screen/WorkerJobPostingDetailScreen.kt
@@ -17,6 +17,7 @@ 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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -70,6 +71,7 @@ internal fun WorkerJobPostingDetailScreen(
removeFavoriteJobPosting: (String, JobPostingType) -> Unit,
applyJobPosting: (String, ApplyMethod) -> Unit,
navigateTo: (com.idle.navigation.DeepLinkDestination) -> Unit,
+ shareJobPosting: () -> Unit,
) {
val onBackPressedDispatcher =
LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
@@ -160,6 +162,17 @@ internal fun WorkerJobPostingDetailScreen(
end = 20.dp,
bottom = 12.dp
),
+ leftComponent = {
+ Image(
+ painter = painterResource(R.drawable.ic_share),
+ contentDescription = null,
+ modifier = Modifier
+ .size(32.dp)
+ .clickable(throttleTime = 2000L) {
+ shareJobPosting()
+ },
+ )
+ },
onNavigationClick = { onBackPressedDispatcher?.onBackPressed() },
)
},
diff --git a/feature/notification/src/main/java/com/idle/notification/NotificationFragment.kt b/feature/notification/src/main/java/com/idle/notification/NotificationFragment.kt
index 61228ec76..acd9a55dc 100644
--- a/feature/notification/src/main/java/com/idle/notification/NotificationFragment.kt
+++ b/feature/notification/src/main/java/com/idle/notification/NotificationFragment.kt
@@ -26,6 +26,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
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 androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -196,6 +197,8 @@ private fun NotificationItem(
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = notification.createdAt.formatRelativeTimeDescription(),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
style = CareTheme.typography.caption1,
color = CareTheme.colors.gray500,
)
@@ -203,12 +206,16 @@ private fun NotificationItem(
Text(
text = notification.title,
style = CareTheme.typography.subtitle3,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
color = CareTheme.colors.black,
)
Text(
text = notification.body,
style = CareTheme.typography.body3,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
color = CareTheme.colors.gray300,
)
}
diff --git a/feature/worker/home/src/main/java/com/idle/worker/home/WorkerHomeViewModel.kt b/feature/worker/home/src/main/java/com/idle/worker/home/WorkerHomeViewModel.kt
index a47c69a72..59c061ccf 100644
--- a/feature/worker/home/src/main/java/com/idle/worker/home/WorkerHomeViewModel.kt
+++ b/feature/worker/home/src/main/java/com/idle/worker/home/WorkerHomeViewModel.kt
@@ -12,6 +12,7 @@ import com.idle.domain.model.jobposting.JobPosting
import com.idle.domain.model.jobposting.JobPostingType
import com.idle.domain.model.jobposting.WorkerJobPosting
import com.idle.domain.model.profile.WorkerProfile
+import com.idle.domain.repositorry.jobposting.JobPostingRepository
import com.idle.domain.usecase.config.ShowNotificationCenterUseCase
import com.idle.domain.usecase.jobposting.AddFavoriteJobPostingUseCase
import com.idle.domain.usecase.jobposting.ApplyJobPostingUseCase
@@ -20,6 +21,8 @@ import com.idle.domain.usecase.jobposting.GetJobPostingsUseCase
import com.idle.domain.usecase.jobposting.RemoveFavoriteJobPostingUseCase
import com.idle.domain.usecase.notification.GetUnreadNotificationCountUseCase
import com.idle.domain.usecase.profile.GetLocalMyWorkerProfileUseCase
+import com.idle.navigation.DeepLinkDestination
+import com.idle.navigation.NavigationEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -37,6 +40,7 @@ class WorkerHomeViewModel @Inject constructor(
private val removeFavoriteJobPostingUseCase: RemoveFavoriteJobPostingUseCase,
private val showNotificationCenterUseCase: ShowNotificationCenterUseCase,
private val getUnreadNotificationCountUseCase: GetUnreadNotificationCountUseCase,
+ private val jobPostingRepository: JobPostingRepository,
private val errorHandlerHelper: ErrorHandler,
private val eventHandlerHelper: EventHandlerHelper,
val navigationHelper: com.idle.navigation.NavigationHelper,
@@ -60,6 +64,7 @@ class WorkerHomeViewModel @Inject constructor(
init {
getJobPostings()
+ navigateToSharedJobPosting()
}
internal fun getJobPostings() = viewModelScope.launch {
@@ -156,6 +161,19 @@ class WorkerHomeViewModel @Inject constructor(
}
}
+ private fun navigateToSharedJobPosting() {
+ val sharedJobPostingInfo = jobPostingRepository.sharedJobPostingInfo ?: return
+
+ navigationHelper.navigateTo(
+ NavigationEvent.NavigateTo(
+ DeepLinkDestination.WorkerJobDetail(
+ jobPostingId = sharedJobPostingInfo.jobPostingId,
+ jobPostingType = sharedJobPostingInfo.jobPostingType.name,
+ )
+ )
+ )
+ }
+
private suspend fun fetchInAppJobPostings() {
getJobPostingsUseCase(next = next.value).onSuccess { (nextId, postings) ->
next.value = nextId
diff --git a/feature/worker/profile/src/main/java/com/idle/worker/profile/WorkerProfileFragment.kt b/feature/worker/profile/src/main/java/com/idle/worker/profile/WorkerProfileFragment.kt
index a82ff425a..7ede28f65 100644
--- a/feature/worker/profile/src/main/java/com/idle/worker/profile/WorkerProfileFragment.kt
+++ b/feature/worker/profile/src/main/java/com/idle/worker/profile/WorkerProfileFragment.kt
@@ -706,4 +706,4 @@ internal fun WorkerProfileScreen(
}
TrackScreenViewEvent(screenName = "carer_profile_screen")
-}
\ No newline at end of file
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c72a6f879..ca35a7e41 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -85,7 +85,7 @@ ktlint = "12.1.1"
## firebase
googleServices = "4.4.2"
-firebaseBom = "33.5.0"
+firebaseBom = "33.5.1"
crashlytics = "3.0.2"
messaging = "24.0.3"
@@ -98,6 +98,15 @@ lottie-compose = "6.5.2"
# https://navermaps.github.io/android-map-sdk/guide-ko/1.html
naverMap = "3.19.1"
+# https://developers.kakao.com/docs/latest/ko/android/getting-started#apply-sdk
+kakao = "2.20.6"
+
+# https://mvnrepository.com/artifact/com.appsflyer/af-android-sdk
+appsFlyer = "6.15.2"
+
+# https://developer.android.com/google/play/installreferrer
+installReferrer = "2.2"
+
[libraries]
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
@@ -170,6 +179,14 @@ lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "
naver-map = { module = "com.naver.maps:map-sdk", version.ref = "naverMap" }
+kakao-common = { module = "com.kakao.sdk:v2-common", version.ref = "kakao" }
+kakao-talk = { module = "com.kakao.sdk:v2-talk", version.ref = "kakao" }
+kakao-share = { module = "com.kakao.sdk:v2-share", version.ref = "kakao" }
+
+appsFlyer = { module = "com.appsflyer:af-android-sdk", version.ref = "appsFlyer" }
+
+installerReferrer = { module = "com.android.installreferrer:installreferrer", version.ref = "installReferrer" }
+
[bundles]
[plugins]
diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts
index bda5cb6bb..afc6298f6 100644
--- a/presentation/build.gradle.kts
+++ b/presentation/build.gradle.kts
@@ -35,4 +35,7 @@ dependencies {
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.navigation.ui)
-}
\ No newline at end of file
+ implementation(libs.kakao.talk)
+ implementation(libs.kakao.share)
+ implementation(libs.appsFlyer)
+}
diff --git a/presentation/src/main/java/com/idle/presentation/MainActivity.kt b/presentation/src/main/java/com/idle/presentation/MainActivity.kt
index b16b9d742..661452784 100644
--- a/presentation/src/main/java/com/idle/presentation/MainActivity.kt
+++ b/presentation/src/main/java/com/idle/presentation/MainActivity.kt
@@ -3,8 +3,10 @@ package com.idle.presentation
import android.Manifest
import android.animation.Animator
import android.animation.ValueAnimator
+import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings.ACTION_WIFI_SETTINGS
@@ -19,24 +21,46 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
+import com.appsflyer.AppsFlyerLib
+import com.appsflyer.deeplink.DeepLink
+import com.appsflyer.deeplink.DeepLinkResult
+import com.idle.analytics.AnalyticsEvent
+import com.idle.analytics.businessmetric.AnalyticsHelper
import com.idle.auth.AuthFragmentDirections
import com.idle.binding.MainEvent
+import com.idle.binding.ShareJobPostingInfo
import com.idle.binding.repeatOnStarted
import com.idle.designsystem.binding.component.dismissToast
import com.idle.designsystem.binding.component.showToast
+import com.idle.domain.model.config.ForceUpdate
+import com.idle.domain.model.jobposting.JobPostingType
+import com.idle.domain.model.jobposting.SharedJobPostingInfo
+import com.idle.navigation.NavigationEvent.NavigateTo
+import com.idle.navigation.NavigationEvent.NavigateToAuthWithClearBackStack
import com.idle.navigation.deepLinkNavigateTo
import com.idle.presentation.databinding.ActivityMainBinding
import com.idle.presentation.forceupdate.ForceUpdateFragment
import com.idle.presentation.network.NetworkObserver
import com.idle.presentation.network.NetworkState
+import com.kakao.sdk.common.util.KakaoCustomTabsClient
+import com.kakao.sdk.share.ShareClient
+import com.kakao.sdk.share.WebSharerClient
+import com.kakao.sdk.template.model.Button
+import com.kakao.sdk.template.model.Content
+import com.kakao.sdk.template.model.FeedTemplate
+import com.kakao.sdk.template.model.ItemContent
+import com.kakao.sdk.template.model.ItemInfo
+import com.kakao.sdk.template.model.Link
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.delay
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var networkObserver: NetworkObserver
+
+ @Inject
+ lateinit var analyticsHelper: AnalyticsHelper
private lateinit var forceUpdateFragment: ForceUpdateFragment
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
@@ -74,25 +98,6 @@ class MainActivity : AppCompatActivity() {
) { isGranted: Boolean ->
if (isGranted) {
// FCM SDK (and your app) can post notifications.
- } else {
- // TODO: Inform user that that your app will not show notifications.
- }
- }
-
- private fun askNotificationPermission() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) ==
- PackageManager.PERMISSION_GRANTED
- ) {
- // FCM SDK (and your app) can post notifications.
- } else if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
- // TODO: display an educational UI explaining to the user the features that will be enabled
- // by them granting the POST_NOTIFICATION permission. This UI should provide the user
- // "OK" and "No thanks" buttons. If the user selects "OK," directly request the permission.
- // If the user selects "No thanks," allow the user to continue without notifications.
- } else {
- requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
- }
}
}
@@ -102,94 +107,13 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
- viewModel.apply {
- repeatOnStarted {
- networkObserver.networkState.collect { state ->
- if (state == NetworkState.NOT_CONNECTED) {
- showNetworkDialog()
- } else {
- dismissNetworkDialog()
- getForceUpdateInfo()
- }
- }
- }
-
- repeatOnStarted {
- forceUpdate.collect {
- it?.let { info ->
- val nowVersion = packageManager.getPackageInfo(packageName, 0).versionName
- val minAppVersion = info.minVersion
- val shouldUpdate = checkShouldUpdate(nowVersion, minAppVersion)
-
- if (shouldUpdate) {
- forceUpdateFragment = ForceUpdateFragment(info).apply {
- isCancelable = false
- }
- forceUpdateFragment.show(
- supportFragmentManager,
- forceUpdateFragment.tag
- )
- }
- }
- }
- }
-
- askNotificationPermission()
-
- repeatOnStarted {
- navigationMenuType.collect { menuType ->
- this@MainActivity.setNavigationMenuType(menuType)
- delay(310L)
- }
- }
-
- repeatOnStarted {
- eventFlow.collect {
- when (it) {
- is MainEvent.ShowToast -> showToast(
- context = this@MainActivity,
- msg = it.msg,
- toastType = it.toastType,
- paddingBottom = calculateSnackBarBottomPadding(),
- )
-
- is MainEvent.DismissToast -> dismissToast()
- }
- }
- }
-
- repeatOnStarted {
- navigationHelper.navigationFlow.collect { navigationEvent ->
- when (navigationEvent) {
- is com.idle.navigation.NavigationEvent.NavigateTo -> navController.deepLinkNavigateTo(
- context = this@MainActivity,
- deepLinkDestination = navigationEvent.destination,
- popUpTo = navigationEvent.popUpTo,
- )
-
- is com.idle.navigation.NavigationEvent.NavigateToAuthWithClearBackStack -> navController.navigate(
- AuthFragmentDirections.actionGlobalNavAuth(
- navigationEvent.toastMsg,
- navigationEvent.toastType
- )
- )
- }
-
- dismissToast()
- }
- }
-
- binding.apply {
- val navHostFragment =
- supportFragmentManager.findFragmentById(R.id.main_FCV) as NavHostFragment
- navController = navHostFragment.navController
-
- mainBNVCenter.itemIconTintList = null
- mainBNVWorker.itemIconTintList = null
- }
-
- setDestinationListener()
+ setupNavigationController()
+ askNotificationPermission()
+ setDestinationListener()
+ observeViewModel()
+ handleDeepLinking()
+ viewModel.apply {
navigationHelper.handleFCMNavigate(
isColdStart = true,
extras = intent?.extras ?: run {
@@ -197,6 +121,7 @@ class MainActivity : AppCompatActivity() {
return
},
onInit = ::initializeUserSession,
+ readNotification = ::readNotification,
)
}
}
@@ -211,10 +136,12 @@ class MainActivity : AppCompatActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
+
viewModel.navigationHelper.handleFCMNavigate(
isColdStart = false,
extras = intent?.extras ?: return,
onInit = viewModel::initializeUserSession,
+ readNotification = viewModel::readNotification,
)
}
@@ -226,20 +153,131 @@ class MainActivity : AppCompatActivity() {
}
}
+ private fun askNotificationPermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ }
+ }
+
+ private fun setupNavigationController() {
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.main_FCV) as NavHostFragment
+ navController = navHostFragment.navController
+ binding.mainBNVCenter.itemIconTintList = null
+ binding.mainBNVWorker.itemIconTintList = null
+ setDestinationListener()
+ }
+
+ private fun observeViewModel() {
+ viewModel.apply {
+ repeatOnStarted {
+ networkObserver.networkState.collect { handleNetworkState(it) }
+ }
+ repeatOnStarted {
+ forceUpdate.collect { it?.let { showForceUpdateDialog(it) } }
+ }
+ repeatOnStarted {
+ navigationMenuType.collect { this@MainActivity.setNavigationMenuType(it) }
+ }
+ repeatOnStarted {
+ eventFlow.collect { handleMainEvent(it) }
+ }
+ repeatOnStarted {
+ navigationHelper.navigationFlow.collect { handleNavigationEvent(it) }
+ }
+ }
+ }
+
+ private fun handleNetworkState(state: NetworkState) {
+ if (state == NetworkState.NOT_CONNECTED) {
+ showNetworkDialog()
+ } else {
+ dismissNetworkDialog()
+ viewModel.getForceUpdateInfo()
+ }
+ }
+
+ private fun showForceUpdateDialog(info: ForceUpdate) {
+ val currentVersion = packageManager.getPackageInfo(packageName, 0).versionName
+ if (checkShouldUpdate(currentVersion, info.minVersion)) {
+ forceUpdateFragment = ForceUpdateFragment(info).apply { isCancelable = false }
+ forceUpdateFragment.show(supportFragmentManager, forceUpdateFragment.tag)
+ }
+ }
+
+ private fun handleMainEvent(event: MainEvent) {
+ when (event) {
+ is MainEvent.ShareJobPosting -> shareJobPosting(event.shareJobPostingInfo)
+ is MainEvent.DismissToast -> dismissToast()
+ is MainEvent.ShowToast -> showToast(
+ context = this,
+ msg = event.msg,
+ toastType = event.toastType,
+ paddingBottom = calculateSnackBarBottomPadding()
+ )
+ }
+ }
+
+ private fun handleNavigationEvent(navigationEvent: com.idle.navigation.NavigationEvent) {
+ when (navigationEvent) {
+ is NavigateTo -> navController.deepLinkNavigateTo(
+ context = this,
+ deepLinkDestination = navigationEvent.destination,
+ popUpTo = navigationEvent.popUpTo
+ )
+
+ is NavigateToAuthWithClearBackStack -> navController.navigate(
+ AuthFragmentDirections.actionGlobalNavAuth(
+ toastMsg = navigationEvent.toastMsg,
+ toastType = navigationEvent.toastType
+ )
+ )
+ }
+ dismissToast()
+ }
+
+ private fun handleDeepLinking() {
+ AppsFlyerLib.getInstance().subscribeForDeepLink { deepLinkResult ->
+ when (deepLinkResult.status) {
+ DeepLinkResult.Status.FOUND -> handleDeepLink(deepLinkResult.deepLink)
+ DeepLinkResult.Status.NOT_FOUND -> viewModel.errorLoggingHelper.logError(Exception("AppsFlyer User Not Found"))
+ else -> viewModel.errorLoggingHelper.logError(Exception(deepLinkResult.error.toString()))
+ }
+ }
+ }
+
+ private fun handleDeepLink(deepLink: DeepLink) {
+ try {
+ val sharedJobPostingId = deepLink.getStringValue("sharedJobPostingId")
+ val sharedJobPostingType = deepLink.getStringValue("sharedJobPostingType")
+ viewModel.setSharedJobPostingInfo(
+ SharedJobPostingInfo(
+ jobPostingId = sharedJobPostingId ?: return,
+ jobPostingType = JobPostingType.create(sharedJobPostingType ?: return)
+ )
+ )
+ } catch (e: Exception) {
+ viewModel.errorLoggingHelper.logError(e)
+ }
+ }
+
private fun showNetworkDialog() {
- networkDialog?.show() ?: run {
- networkDialog = AlertDialog.Builder(this@MainActivity).apply {
+ if (networkDialog == null) {
+ networkDialog = AlertDialog.Builder(this).apply {
setTitle("인터넷이 연결되어 있지 않아요")
setMessage("Wi-Fi 또는 데이터 연결을 확인한 후 다시 시도해 주세요.")
- setPositiveButton("설정") { _, _ ->
- startActivity(Intent(ACTION_WIFI_SETTINGS))
- }
- setNegativeButton("종료") { _, _ ->
- finish()
- }
+ setPositiveButton("설정") { _, _ -> startActivity(Intent(ACTION_WIFI_SETTINGS)) }
+ setNegativeButton("종료") { _, _ -> finish() }
setCancelable(false)
- }.show()
+ }.create()
}
+ networkDialog?.show()
}
private fun dismissNetworkDialog() {
@@ -249,46 +287,33 @@ class MainActivity : AppCompatActivity() {
private fun setDestinationListener() {
navController.addOnDestinationChangedListener { _, destination, _ ->
- binding.apply {
- val navMenuType = when (destination.id) {
- in centerBottomNavDestinationIds -> NavigationMenuType.CENTER
- in workerBottomNavDestinationIds -> NavigationMenuType.WORKER
- else -> NavigationMenuType.HIDE
- }
-
- viewModel.setNavigationMenuType(navMenuType)
+ val navMenuType = when (destination.id) {
+ in centerBottomNavDestinationIds -> NavigationMenuType.CENTER
+ in workerBottomNavDestinationIds -> NavigationMenuType.WORKER
+ else -> NavigationMenuType.HIDE
}
+ viewModel.setNavigationMenuType(navMenuType)
}
}
private fun setNavigationMenuType(menuType: NavigationMenuType) {
- when (menuType) {
- NavigationMenuType.CENTER -> binding.apply {
- if (mainBNVWorker.visibility == View.VISIBLE) {
- slideDown(mainBNVWorker)
- }
- if (mainBNVCenter.visibility != View.VISIBLE) {
- slideUp(mainBNVCenter)
+ binding.apply {
+ when (menuType) {
+ NavigationMenuType.CENTER -> {
+ if (mainBNVWorker.visibility == View.VISIBLE) slideDown(mainBNVWorker)
+ if (mainBNVCenter.visibility != View.VISIBLE) slideUp(mainBNVCenter)
+ mainBNVCenter.setupWithNavController(navController)
}
- mainBNVCenter.setupWithNavController(navController)
- }
- NavigationMenuType.WORKER -> binding.apply {
- if (mainBNVCenter.visibility == View.VISIBLE) {
- slideDown(mainBNVCenter)
+ NavigationMenuType.WORKER -> {
+ if (mainBNVCenter.visibility == View.VISIBLE) slideDown(mainBNVCenter)
+ if (mainBNVWorker.visibility != View.VISIBLE) slideUp(mainBNVWorker)
+ mainBNVWorker.setupWithNavController(navController)
}
- if (mainBNVWorker.visibility != View.VISIBLE) {
- slideUp(mainBNVWorker)
- }
- mainBNVWorker.setupWithNavController(navController)
- }
- NavigationMenuType.HIDE -> binding.apply {
- if (mainBNVCenter.visibility == View.VISIBLE) {
- slideDown(mainBNVCenter)
- }
- if (mainBNVWorker.visibility == View.VISIBLE) {
- slideDown(mainBNVWorker)
+ NavigationMenuType.HIDE -> {
+ if (mainBNVCenter.visibility == View.VISIBLE) slideDown(mainBNVCenter)
+ if (mainBNVWorker.visibility == View.VISIBLE) slideDown(mainBNVWorker)
}
}
}
@@ -332,78 +357,110 @@ class MainActivity : AppCompatActivity() {
private fun checkShouldUpdate(currentVersion: String, minVersion: String): Boolean {
val current = normalizeVersion(currentVersion)
val min = normalizeVersion(minVersion)
-
- // 버전 비교 (메이저, 마이너, 패치 순으로 비교)
- for (i in 0..2) {
- if (current[i] < min[i]) return true
- if (current[i] > min[i]) return false
- }
- return false
+ return (0..2).any { current[it] < min[it] }
}
private fun normalizeVersion(version: String): List =
- version.split('.')
- .map { it.toIntOrNull() ?: 0 }
- .let {
- when (it.size) {
- 2 -> it + listOf(0) // 1.0 -> 1.0.0 형태로 변환
- else -> it
- }
- }
+ version.split('.').map { it.toIntOrNull() ?: 0 }.let {
+ if (it.size == 2) it + 0 else it
+ }
+
private fun slideUp(view: View) {
- view.measure(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- val targetHeight = view.measuredHeight
- view.layoutParams.height = 0
-
- val slide = ValueAnimator.ofInt(0, targetHeight)
- slide.addUpdateListener { valueAnimator ->
- val animatedValue = valueAnimator.animatedValue as Int
- val layoutParams = view.layoutParams
- layoutParams.height = animatedValue
- view.layoutParams = layoutParams
+ view.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ animateViewHeight(view, 0, view.measuredHeight)
+ }
+
+ private fun slideDown(view: View) {
+ animateViewHeight(view, view.measuredHeight, 0) {
+ view.visibility = View.GONE
+ view.isClickable = false
}
- slide.duration = 300
- slide.addListener(object : Animator.AnimatorListener {
- override fun onAnimationStart(animation: Animator) {
- view.visibility = View.VISIBLE
- }
+ }
- override fun onAnimationEnd(animation: Animator) {
- view.isClickable = true
+ private fun animateViewHeight(
+ view: View,
+ startHeight: Int,
+ endHeight: Int,
+ onEnd: (() -> Unit)? = null
+ ) {
+ view.layoutParams.height = startHeight
+ ValueAnimator.ofInt(startHeight, endHeight).apply {
+ addUpdateListener {
+ view.layoutParams.height = it.animatedValue as Int
+ view.requestLayout()
}
+ duration = 300
+ addListener(object : Animator.AnimatorListener {
+ override fun onAnimationStart(animation: Animator) {
+ if (endHeight > startHeight) view.visibility = View.VISIBLE
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ onEnd?.invoke()
+ }
- override fun onAnimationCancel(animation: Animator) {}
- override fun onAnimationRepeat(animation: Animator) {}
- })
- slide.start()
+ override fun onAnimationCancel(animation: Animator) {}
+ override fun onAnimationRepeat(animation: Animator) {}
+ })
+ }.start()
}
- private fun slideDown(view: View) {
- val initialHeight = view.measuredHeight
-
- val slide = ValueAnimator.ofInt(initialHeight, 0)
- slide.addUpdateListener { valueAnimator ->
- val animatedValue = valueAnimator.animatedValue as Int
- val layoutParams = view.layoutParams
- layoutParams.height = animatedValue
- view.layoutParams = layoutParams
- }
- slide.duration = 300
- slide.addListener(object : Animator.AnimatorListener {
- override fun onAnimationStart(animation: Animator) {}
+ private fun shareJobPosting(sharedJobPostingInfo: ShareJobPostingInfo) {
+ val oneLinkUrl =
+ "https://caremeet.onelink.me/dXPO/edg5vvwt?sharedJobPostingId=${sharedJobPostingInfo.id}&sharedJobPostingType=${sharedJobPostingInfo.type}"
+ val jobPostingFeed = FeedTemplate(
+ content = Content(
+ title = sharedJobPostingInfo.centerName,
+ description = sharedJobPostingInfo.centerOfficeNumber,
+ imageUrl = "https://idle-prod-bucket.s3.ap-northeast-2.amazonaws.com/assets/caremeet-share.png",
+ link = Link(webUrl = oneLinkUrl, mobileWebUrl = oneLinkUrl)
+ ),
+ itemContent = ItemContent(
+ profileText = "케어밋에서 아래의 일자리에 지원해요!",
+ titleImageText = sharedJobPostingInfo.title,
+ titleImageCategory = "요양 일자리",
+ items = listOf(
+ ItemInfo(item = "근무 요일", itemOp = sharedJobPostingInfo.weekdays),
+ ItemInfo(item = "근무 시간", itemOp = sharedJobPostingInfo.workTime),
+ ItemInfo(item = "급여", itemOp = sharedJobPostingInfo.payAmount),
+ ItemInfo(item = "근무 주소", itemOp = sharedJobPostingInfo.roadNameAddress)
+ )
+ ),
+ buttons = listOf(
+ Button(
+ title = "앱에서 확인하기",
+ link = Link(webUrl = oneLinkUrl, mobileWebUrl = oneLinkUrl)
+ )
+ )
+ )
- override fun onAnimationEnd(animation: Animator) {
- view.visibility = View.GONE
- view.isClickable = false
+ analyticsHelper.logEvent(
+ AnalyticsEvent(
+ type = AnalyticsEvent.Types.ACTION,
+ properties = mutableMapOf("JobPostingType" to sharedJobPostingInfo.type)
+ )
+ )
+
+ if (ShareClient.instance.isKakaoTalkSharingAvailable(this)) {
+ ShareClient.instance.shareDefault(this, jobPostingFeed) { result, error ->
+ if (error != null) viewModel.errorLoggingHelper.logError(Exception(error))
+ else result?.let { startActivity(it.intent) }
}
+ } else {
+ openWebSharer(WebSharerClient.instance.makeDefaultUrl(jobPostingFeed))
+ }
+ }
- override fun onAnimationCancel(animation: Animator) {}
- override fun onAnimationRepeat(animation: Animator) {}
- })
- slide.start()
+ private fun openWebSharer(url: Uri) {
+ try {
+ KakaoCustomTabsClient.openWithDefault(this, url)
+ } catch (e: UnsupportedOperationException) {
+ try {
+ KakaoCustomTabsClient.open(this, url)
+ } catch (e: ActivityNotFoundException) {
+ viewModel.errorLoggingHelper.logError(Exception(e))
+ }
+ }
}
}
diff --git a/presentation/src/main/java/com/idle/presentation/MainViewModel.kt b/presentation/src/main/java/com/idle/presentation/MainViewModel.kt
index fc25c6aa9..0b3506b67 100644
--- a/presentation/src/main/java/com/idle/presentation/MainViewModel.kt
+++ b/presentation/src/main/java/com/idle/presentation/MainViewModel.kt
@@ -11,10 +11,13 @@ import com.idle.domain.model.config.ForceUpdate
import com.idle.domain.model.error.ApiErrorCode
import com.idle.domain.model.error.ErrorHandler
import com.idle.domain.model.error.HttpResponseException
+import com.idle.domain.model.jobposting.SharedJobPostingInfo
import com.idle.domain.model.profile.CenterManagerAccountStatus
+import com.idle.domain.repositorry.jobposting.JobPostingRepository
import com.idle.domain.usecase.auth.GetAccessTokenUseCase
import com.idle.domain.usecase.auth.GetUserTypeUseCase
import com.idle.domain.usecase.config.GetForceUpdateInfoUseCase
+import com.idle.domain.usecase.notification.ReadNotificationUseCase
import com.idle.domain.usecase.profile.GetCenterStatusUseCase
import com.idle.domain.usecase.profile.GetMyCenterProfileUseCase
import com.idle.domain.usecase.profile.GetMyWorkerProfileUseCase
@@ -42,11 +45,13 @@ class MainViewModel @Inject constructor(
private val getMyCenterProfileUseCase: GetMyCenterProfileUseCase,
private val getMyWorkerProfileUseCase: GetMyWorkerProfileUseCase,
private val getCenterStatusUseCase: GetCenterStatusUseCase,
+ private val readNotificationUseCase: ReadNotificationUseCase,
+ private val jobPostingRepository: JobPostingRepository,
// private val connectWebSocketUseCase: ConnectWebSocketUseCase,
// private val disconnectWebSocketUseCase: DisconnectWebSocketUseCase,
private val errorHandlerHelper: ErrorHandler,
private val eventHandlerHelper: EventHandlerHelper,
- private val errorLoggingHelper: ErrorLoggingHelper,
+ val errorLoggingHelper: ErrorLoggingHelper,
val navigationHelper: com.idle.navigation.NavigationHelper,
) : ViewModel() {
private val _navigationMenuType = MutableStateFlow(NavigationMenuType.HIDE)
@@ -103,6 +108,14 @@ class MainViewModel @Inject constructor(
navigateToDestination(userRole)
}
+ internal fun setSharedJobPostingInfo(sharedJobPostingInfo: SharedJobPostingInfo) {
+ jobPostingRepository.sharedJobPostingInfo = sharedJobPostingInfo
+ }
+
+ internal fun readNotification(notificationId: String) = viewModelScope.launch {
+ readNotificationUseCase(notificationId).onFailure { errorHandlerHelper.sendError(it) }
+ }
+
private suspend fun getAccessTokenAndUserRole(): Pair = coroutineScope {
val accessTokenDeferred = async { getAccessTokenUseCase() }
val userRoleDeferred = async { getMyUserRoleUseCase() }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index f99c776f2..84695aca0 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,6 +19,7 @@ dependencyResolutionManagement {
google()
mavenCentral()
maven("https://repository.map.naver.com/archive/maven")
+ maven("https://devrepo.kakao.com/nexus/content/groups/public/")
}
}