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/") } }