Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#839 행사 카카오 공유 기능 #888

Merged
merged 22 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8cdebdd
feat: 행사 공유 아이콘 리소스 추가
tmdgh1592 Dec 19, 2023
82dc30c
feat: 행사 상세 화면 툴바에 공유하기 아이콘 버튼 추가
tmdgh1592 Dec 19, 2023
e503eb2
chore: 카카오 공유 모듈 의존성 추가
tmdgh1592 Dec 19, 2023
d299cac
chore: 카카오 공유하기 난독화 설정
tmdgh1592 Dec 19, 2023
f98ae52
chore: 카카오 앱키, 스키마, 호스트 리소스화
tmdgh1592 Dec 19, 2023
d2501fc
feat: 카카오 scheme, host를 manifest에 등록
tmdgh1592 Dec 19, 2023
958d765
feat: 카카오 Sdk 초기화하는 기능 구현
tmdgh1592 Dec 19, 2023
ce9e337
feat: 행사 공유 템플릿을 생성하는 클래스 구현
tmdgh1592 Dec 19, 2023
debf877
feat: 행사 공유를 공유하는 클래스 구현
tmdgh1592 Dec 19, 2023
d826e56
refactor: 행사 공유 로직을 ViewModel에서 처리하도록 변경
tmdgh1592 Dec 19, 2023
48f7798
feat: 행사 정보 카카오톡 공유하기 기능 구현
tmdgh1592 Dec 19, 2023
60ee764
chore: 카카오톡 공유하기 모듈 버전 업그레이드 (v2.12.0)
tmdgh1592 Dec 19, 2023
1dfbb56
chore: 카카오톡 공유하기 난독화 추가
tmdgh1592 Dec 19, 2023
982cb75
refactor: 코드 포맷팅
tmdgh1592 Dec 19, 2023
c2d40c1
Merge remote-tracking branch 'upstream/android-main' into Feature/#83…
tmdgh1592 Dec 19, 2023
5d573ca
feat: 공유하기 아이콘 색상에 투명도를 없애고 #D9D9D9로 변경
tmdgh1592 Dec 19, 2023
ce91571
fix: 카카오톡으로 공유된 행사 메시지 전체 부분을 클릭하면 크래시 발생하는 버그 수정
tmdgh1592 Dec 19, 2023
1cc4141
refactor: 라인 포맷팅
tmdgh1592 Dec 19, 2023
37b0cad
refactor: 라인 포맷팅
tmdgh1592 Dec 19, 2023
9d40008
Merge branch 'android-main' into Feature/#839-행사_카카오_공유_기능
ki960213 Jan 28, 2024
d67fc94
fix(RefreshableViewModel): 새로고침 시 네트워크 에러에 의해 실패하면 네트워크 에러 이벤트가 발생되도록 변경
ki960213 Jan 28, 2024
4671335
feat(EventDetailActivity): 행사 상세 화면에서 탭이 좌우 스크롤 되도록 변경
ki960213 Jan 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions android/2023-emmsale/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ android {

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

buildConfigField(
"String",
"GITHUB_CLIENT_ID",
getApiKey("GH_CLIENT_ID"),
)
buildConfigField("String", "GITHUB_CLIENT_ID", getApiKey("GH_CLIENT_ID"))
resValue("string", "kakao_app_key", getApiKey("KAKAO_APP_KEY"))
resValue("string", "kakao_scheme", getApiKey("KAKAO_SCHEME"))
resValue("string", "kakao_host", getApiKey("KAKAO_HOST"))
}

buildFeatures {
buildConfig = true
}

buildTypes {
debug {
buildConfigField("String", "BASE_URL", "\"https://dev.kerdy.kro.kr\"")
Expand All @@ -59,16 +60,20 @@ android {
signingConfig = signingConfigs.getByName("debug")
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
}

dataBinding {
enable = true
}

tasks.withType(Test::class) {
testLogging {
events.addAll(
Expand All @@ -80,6 +85,7 @@ android {
)
}
}

testOptions {
unitTests.isReturnDefaultValues = true
unitTests.isIncludeAndroidResources = true
Expand Down Expand Up @@ -171,6 +177,9 @@ dependencies {
implementation("androidx.room:room-runtime:$roomVersion")
kapt("androidx.room:room-compiler:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")

// Kakao Share
implementation("com.kakao.sdk:v2-share:2.12.0")
}

kapt {
Expand Down
7 changes: 7 additions & 0 deletions android/2023-emmsale/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@

# ApiResponse 클래스의 타입 매개변수를 유지하기 위해 추가. 안하면 CallAdapter에서 retrofit2.Call<ApiResponse>를 반환하는 CallAdapter 만들 수 없음.
-keepnames, allowobfuscation class com.emmsale.data.common.retrofit.callAdapter.ApiResponse

# 카카오 공유하기
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
-keep interface com.kakao.sdk.**.*Api
-keep class com.kakao.sdk.**.model.* { <fields>; }
-keep class * extends com.google.gson.TypeAdapter
15 changes: 14 additions & 1 deletion android/2023-emmsale/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,20 @@
android:name=".presentation.ui.main.MainActivity"
android:launchMode="singleTask" />
<activity android:name=".presentation.ui.onboarding.OnboardingActivity" />
<activity android:name=".presentation.ui.eventDetail.EventDetailActivity" />
<activity
android:name=".presentation.ui.eventDetail.EventDetailActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="@string/kakao_host"
android:scheme="@string/kakao_scheme" />
</intent-filter>
</activity>
<activity
android:name=".presentation.ui.login.LoginActivity"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.emmsale.presentation

import android.app.Application
import com.emmsale.R
import com.emmsale.presentation.common.KerdyNotificationChannel
import com.google.firebase.analytics.FirebaseAnalytics
import com.kakao.sdk.common.KakaoSdk
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
Expand All @@ -11,6 +13,7 @@ class KerdyApplication : Application() {
super.onCreate()
initFirebaseAnalytics()
initNotificationChannels()
KakaoSdk.init(this, getString(R.string.kakao_app_key))
}

private fun initFirebaseAnalytics() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ abstract class RefreshableViewModel : NetworkViewModel() {
onSuccess: suspend (T) -> Unit = {},
onFailure: suspend (code: Int, message: String?) -> Unit = { _, _ -> dispatchFetchFailEvent() },
onLoading: suspend () -> Unit = {},
onNetworkError: suspend () -> Unit = {},
onNetworkError: suspend () -> Unit = { dispatchNetworkErrorEvent() },
onStart: suspend () -> Unit = {},
onFinish: suspend () -> Unit = {},
): Job = requestNetwork(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class EventDetailActivity :

registerScreen(this)

fetchEvent()
setupDataBinding()
setupFragmentStateAdapter()
setupBackPressedDispatcher()
Expand All @@ -44,6 +45,14 @@ class EventDetailActivity :
observeUiEvent()
}

private fun fetchEvent() {
val eventId = intent.data?.getQueryParameter(EVENT_ID_KEY)?.toLong()
if (eventId != null) viewModel.eventId = eventId

viewModel.fetchEvent()
viewModel.fetchIsScrapped()
}

private fun setupDataBinding() {
binding.vm = viewModel
binding.navigateToUrl = ::navigateToUrl
Expand Down Expand Up @@ -79,7 +88,6 @@ class EventDetailActivity :
TabLayoutMediator(binding.tablayoutEventdetail, binding.vpEventdetail) { tab, position ->
tab.text = tabNames[position]
}.attach()
binding.vpEventdetail.isUserInputEnabled = false
}

private fun setupBackPressedDispatcher() {
Expand All @@ -96,6 +104,10 @@ class EventDetailActivity :

private fun setupToolbar() {
binding.tbEventdetail.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
binding.tbEventdetail.setOnMenuItemClickListener { item ->
if (item.itemId == R.id.share_event) viewModel.shareEvent()
true
}
}

private fun setupOnTabSelectedListener() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import com.emmsale.presentation.base.RefreshableViewModel
import com.emmsale.presentation.common.livedata.NotNullLiveData
import com.emmsale.presentation.common.livedata.NotNullMutableLiveData
import com.emmsale.presentation.common.livedata.SingleLiveEvent
import com.emmsale.presentation.ui.eventDetail.eventSharer.EventSharer
import com.emmsale.presentation.ui.eventDetail.eventSharer.EventTemplateMaker
import com.emmsale.presentation.ui.eventDetail.uiState.EventDetailScreenUiState
import com.emmsale.presentation.ui.eventDetail.uiState.EventDetailUiEvent
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -24,8 +26,10 @@ class EventDetailViewModel @Inject constructor(
stateHandle: SavedStateHandle,
private val eventRepository: EventRepository,
private val recruitmentRepository: RecruitmentRepository,
private val eventTemplateMaker: EventTemplateMaker,
private val eventSharer: EventSharer,
) : RefreshableViewModel() {
val eventId = stateHandle[EVENT_ID_KEY] ?: DEFAULT_EVENT_ID
var eventId = stateHandle[EVENT_ID_KEY] ?: DEFAULT_EVENT_ID
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뷰모델이 eventId를 가지고 있어서 그럴까요? 점점 side effect가 늘어나는 구현이 되고 있네요ㅠㅠ
fetchEvent(eventId: Long), fetchIsScrapped(eventId: Long) 이렇게 하고 Activity에서 eventId를 가지고 있는 게 나을까요? 궁금해서 질문드립니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

side effect가 늘어나도 상태 관리에 대한 문제가 발생하는 상황이 아니라고 생각이 드는데 혹시 어떤 것이 우려되시나요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예를 들어 fetchEvent() 호출 후 eventId 값을 변경하고 fetchIsScrapped()를 호출하면 일관성이 깨지게 됩니다.
사실 저희는 이렇게 코드를 작성하지 않을 거라 예상되지만 side effect 발생 가능성을 열어둔 구조라고 생각합니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 부분에 대해서 공감합니다.
다만, 토마스 말씀처럼 그렇게 코드를 작성하지도 않을 것이고 모든 코드를 순수 함수로 만들기에는 어려움이 있다고 생각합니다.

예를 들어서, 제안해주신 fetchEvent(eventId: Long), fetchIsScrapped(eventId: Long) 도 마찬가지로 함수 외부의 상태를 변경하기 때문에 결국 side effect가 발생하는 것은 동일합니다.
그만큼 함수형으로 모든 코드를 작성하는 것이 어렵다고 생각하며 이후에 문제를 느낀다면 그 때 개선해봐도 좋을 것 같습니다 👍

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 이전 구조와 차이점은 뷰모델 객체가 생성된 이후에 eventId가 변경될 수 있다는 것입니다.
eventId의 setter가 public 이지만 "카카오 공유 메세지 클릭해서 들어올 때 처리하는 것 빼고는 eventId를 수정하면 안돼"라는 추가 지식을 알아야만 의도치 않은 결과를 막을 수 있다는 점 때문에 약간 아쉬운 구조라고 생각했는데, 그래도 매번 eventId를 인자로 넘겨야 하는 것보다는 낫다고 생각이 들어서 지금도 괜찮은 것 같네요 :)


private val _event = NotNullMutableLiveData(Event())
val event: NotNullLiveData<Event> = _event
Expand All @@ -51,17 +55,12 @@ class EventDetailViewModel @Inject constructor(
private val _uiEvent = SingleLiveEvent<EventDetailUiEvent>()
val uiEvent: LiveData<EventDetailUiEvent> = _uiEvent

init {
fetchEvent()
fetchIsScrapped()
}

private fun fetchEvent(): Job = fetchData(
fun fetchEvent(): Job = fetchData(
fetchData = { eventRepository.getEventDetail(eventId) },
onSuccess = { _event.value = it },
)

private fun fetchIsScrapped(): Job = viewModelScope.launch {
fun fetchIsScrapped(): Job = viewModelScope.launch {
when (val result = eventRepository.isScraped(eventId)) {
is Success -> _isScraped.value = result.data
else -> {}
Expand Down Expand Up @@ -117,6 +116,15 @@ class EventDetailViewModel @Inject constructor(
onFinish = { _canStartToWriteRecruitment.value = true },
)

fun shareEvent() {
val eventShareTemplate = eventTemplateMaker.create(
eventId = eventId,
eventName = event.value.name,
posterUrl = event.value.posterImageUrl,
)
eventSharer.shareEvent(eventShareTemplate)
}

Comment on lines +119 to +127
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구성변경에 대응할 필요가 없고 비즈니스 로직이 포함되지 않은 로직을 viewModel에 정의할 필요가 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구성변경과 상관없이 카카오톡 공유하기를 비즈니스 로직이라고 생각했습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

카카오톡 앱이 있어야만 수행할 수 있는 로직이 비즈니스 로직인가요? 궁금해서 질문드립니다..!
뷰모델이 카카오톡 앱에 공유할 데이터를 가지고 있고 UI 컨트롤러가 카카오톡 앱에 그 데이터를 공유하는 게 올바른 책임 할당이라고 생각합니다. 😊

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떻게 생각하느냐에 따라 다르겠지만 저는 비즈니스 로직이라고 생각합니다.
흔히 안드로이드에서 네트워크, 저장소 등을 사용하는 로직들도 비즈니스 로직으로 분류하듯이,
카카오 api를 사용해서 네트워크 연결을 하는 로직 또한 비즈니스 로직으로 분류된다고 생각합니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇다면 갤러리 앱에서 사진을 불러오는 기능도 뷰모델에서 하는 게 좋을까요? 아니면 지도 앱에 현재 화면에 보여지는 행사의 장소를 검색하는 기능도 뷰모델에서 하는 게 좋을까요? 😕
위 기능 모두 Context를 @ApplicationContext로 주입받는 클래스를 만들면 ViewModel은 Context에 의존하지는 않게 됩니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

갤러리도 저장소 역할을 하기 때문에 관점에 따라 Activity에서 처리할 수도 있고 ViewModel에서 처리할 수도 있을 것 같습니다.
다만, 카카오톡 공유하기와 다르게 갤러리는 확실하게 Activity의 콜백을 필요로하기 때문에 뷰모델에서 처리하기 까다로울 듯 하네요 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토마스 말씀처럼 Activity는 UiController이지 BusinessController가 아닙니다.
따라서 Ui와 관련된 로직 위주로 컨트롤하는 역할을 수행하죠.
행사 정보를 공유한다 라는 기능은 UI보다는 비즈니스 로직에 가깝다고 느껴지네요.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희 앱에는 갤러리에 사진을 추가하는 기능이 없지만 만약 생긴다면 사진을 저장한다라는 기능도 비즈니스 로직이므로 뷰모델에서 처리하는 게 나을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사진을 저장한다 라는 로직도 비즈니스 로직이기 때문에, 어떠한 개발자들은 토마스가 말씀하신 것처럼 하기도 합니다.
위에 댓글에도 언급했지만 행사 정보를 공유한다.사진을 저장한다 라는 코드를 비즈니스 로직으로 분리했을 때 증가하는 코드와 복잡성은 후자일 때 더 커지죠..
행사 정보 공유 로직은 그런 단점이 따로 없기 때문에 비즈니스 로직으로 ViewModel에 위치시켜도 문제가 되지 않는다는 것입니다.
또한, ActivityController가 반드시 필요한 로직인가 아닌가에 대해서도 따져봐야 하구요. (갤러리)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇군요. 토스트 메세지 띄우기, 스낵바 보여주기, 다른 화면 이동 같은 UI 관련 기능만 Activity에서 처리하고 다른 앱에 데이터 공유 기능은 UI 관련 기능으로 볼 수 없으니 Activity에서 처리하지 않는 게 좋겠군요.
덕분에 Activity와 ViewModel의 역할에 대해 배웠네요. :)

companion object {
const val EVENT_ID_KEY = "EVENT_ID_KEY"
private const val DEFAULT_EVENT_ID = -1L
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.emmsale.presentation.ui.eventDetail.eventSharer

import android.content.Context
import com.emmsale.R
import com.emmsale.presentation.common.extension.showToast
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.FeedTemplate
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

class EventSharer @Inject constructor(
@ApplicationContext private val context: Context,
) {
Comment on lines +13 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context가 필요한 객체를 viewModel에서 의존하고 있는 것 같아요...!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 클래스 객체는 Activity의 context가 아닌, ApplicationContext를 의존하기 때문에 구성변경시 메모리릭이 발생하지는 않습니다.
그 외에 토마스가 생각하시기에 context가 필요한 객체를 ViewModel에서 의존하면 어떠한 문제가 발생하나요??

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context를 사용한다는 것은 안드로이드 관련 기능과 강하게 결합된다고 생각합니다. 물론 AAC ViewModel을 사용하고 있긴 하지만 ViewModel은 안드로이드 의존성을 최대한 줄이는 게 적절한 책임할당이라고 생각합니다. 메모리릭이 발생하지 않는다는 이유로 ApplicationContext를 사용해도 괜찮다면 뷰모델의 주 생성자 매개변수로 @ApplicationContext val context: Context를 선언하는 것은 괜찮을까용?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 토마스 말씀처럼 context를 주입받아서 ViewModel을 사용하는 방법도 있을 겁니다.
하지만 위 코드에서는 그럴 필요와 이유가 전혀 없기 때문에 객체를 전달받는 구조로 작성하였습니다.

저는 테스트하기 좋은 구조가 자연스럽게 유지보수하기에도 좋은 구조로 이어진다고 생각합니다.
context 자체를 주입받고 내부에서 Sharer과 TemplateMaker를 생성한다면 테스트하기 어려워질 듯 합니다.
우리가 DI를 사용하는 이유도 마찬가지죠.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

또한, context를 직접적으로 사용하지 않기 때문에 안드로이드 의존성도 직접적으로는 없다고 볼 수 있습니다.
Repository에서 Room을 사용한다고 해서, Repository를 사용하는 ViewModel이 (AAC를 제외하고) 안드로이드 의존성에 강하게 결합되어 있다고 하지는 않는 것과 비슷하다고 생각해주셔도 좋을 것 같습니다~

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 저장소가 "데이터를 가져올 수 있고 영속화 할 수 있는 객체"라고 추상화해서 생각했습니다. Repository도 context에 의존하진 않고 LocalDataSource, RemoteDataSource를 추상화한 인터페이스에 의존하고 있습니다.
사실 지금 Repository에서 ApiResponse 객체를 받고 있다는 것이 데이터를 어떻게 가져올 지에 대한 구현을 노출하고 있다는 것을 의미하긴 하지만 저는 이건 타협할만한 가치가 있다고 생각합니다. ApiResponse를 사용하면 뷰모델에서 Repository 메서드 호출마다 직접 네트워크 에러를 캐치하지 않아도 되기 때문입니다. 하지만 카카오톡 공유 기능은 UI 컨트롤러에서 직접 처리하는 게 더 코드가 줄기 때문에 타협하지 않는 게 낫다고 생각합니다.
정리하자면 뷰모델과 Repository는 도메인 레이어이기 때문에 최대한 안드로이드 의존성을 줄이되, 구현 시간, 코드량, 실수 확률을 크게 줄일 수 있다면 타협하는 것이 좋다고 생각합니다.

Copy link
Member Author

@tmdgh1592 tmdgh1592 Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음.. 뷰모델은 UI 레이어입니다.
그리고 코드가 말도 안 되게 늘어나는게 아니라면 역할에 따라 위치하는게 옳다고 생각합니다.

레이어에 따라 안드로이드 의존성을 줄여야 하는 것이라면 UI 레이어이기 때문에 크게 문제가 되지 않는다고 이해해도 될까요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 죄송합니다. 뷰모델과 Repository는 도메인 로직을 처리하기 때문에 라고 정정하겠습니다 😓

private val isNotKakaoTalkInstalled
get() = !ShareClient.instance.isKakaoTalkSharingAvailable(context)

fun shareEvent(eventTemplate: FeedTemplate) {
if (isNotKakaoTalkInstalled) {
handleKakaoNotInstalledError(eventTemplate)
return
}

share(eventTemplate)
}

private fun handleKakaoNotInstalledError(eventTemplate: FeedTemplate) {
val sharerUrl = WebSharerClient.instance.makeDefaultUrl(eventTemplate)

runCatching {
KakaoCustomTabsClient.openWithDefault(context, sharerUrl)
}.onFailure {
context.showToast(R.string.eventdetail_no_exist_browser)
}

runCatching {
KakaoCustomTabsClient.open(context, sharerUrl)
}.onFailure {
context.showToast(R.string.eventdetail_no_exist_browser)
}
}

private fun share(eventTemplate: FeedTemplate) {
ShareClient.instance.shareDefault(context, eventTemplate) { sharingResult, error ->
if (error != null) {
context.showToast(R.string.eventdetail_kakao_share_fail)
} else if (sharingResult != null) {
context.startActivity(sharingResult.intent)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.emmsale.presentation.ui.eventDetail.eventSharer

import android.content.Context
import com.emmsale.R
import com.emmsale.presentation.ui.eventDetail.EventDetailViewModel
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.Link
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

class EventTemplateMaker @Inject constructor(
@ApplicationContext private val context: Context,
) {
fun create(
eventId: Long,
eventName: String,
posterUrl: String?,
): FeedTemplate = FeedTemplate(
content = Content(
title = eventName,
description = context.getString(R.string.eventdetail_share_template_description),
imageUrl = posterUrl ?: "",
link = Link(androidExecutionParams = mapOf(EventDetailViewModel.EVENT_ID_KEY to eventId.toString())),
),
buttons = listOf(
Button(
context.getString(R.string.eventdetail_share_button_title),
Link(androidExecutionParams = mapOf(EventDetailViewModel.EVENT_ID_KEY to eventId.toString())),
),
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ class FeedDetailViewHolder(
parent: ViewGroup,
onAuthorImageClick: (authorId: Long) -> Unit,
) : FeedOrCommentViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_feeddetail_feed_detail, parent, false),
LayoutInflater.from(parent.context)
.inflate(R.layout.item_feeddetail_feed_detail, parent, false),
) {
private val binding = ItemFeeddetailFeedDetailBinding.bind(itemView)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ class NotificationTagConfigViewModel @Inject constructor(
_networkUiEvent.value = NetworkUiEvent.Unexpected(eventTagsResult.error.toString())

interestTagsResult is Unexpected ->
_networkUiEvent.value = NetworkUiEvent.Unexpected(interestTagsResult.error.toString())
_networkUiEvent.value =
NetworkUiEvent.Unexpected(interestTagsResult.error.toString())

eventTagsResult is Failure || interestTagsResult is Failure -> dispatchFetchFailEvent()
eventTagsResult is NetworkError || interestTagsResult is NetworkError -> {
Expand Down Expand Up @@ -93,7 +94,8 @@ class NotificationTagConfigViewModel @Inject constructor(
_networkUiEvent.value = NetworkUiEvent.Unexpected(eventTagsResult.error.toString())

interestTagsResult is Unexpected ->
_networkUiEvent.value = NetworkUiEvent.Unexpected(interestTagsResult.error.toString())
_networkUiEvent.value =
NetworkUiEvent.Unexpected(interestTagsResult.error.toString())

eventTagsResult is Failure || interestTagsResult is Failure -> dispatchFetchFailEvent()
eventTagsResult is NetworkError || interestTagsResult is NetworkError -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ class SettingFragment :
{ MyCommentsActivity.startActivity(requireContext()) }
binding.onNotificationSettingButtonClick =
{ NotificationConfigActivity.startActivity(requireContext()) }
binding.onBlockMembersButtonClick = { BlockedMembersActivity.startActivity(requireContext()) }
binding.onBlockMembersButtonClick =
{ BlockedMembersActivity.startActivity(requireContext()) }
binding.onUseTermButtonClick = { UseTermWebViewActivity.startActivity(requireContext()) }
binding.onLogoutButtonClick = ::showLogoutConfirmDialog
binding.onInquirePageButtonClick = ::navigateToInquirePage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M11.354,8.354C11.549,8.158 11.549,7.842 11.354,7.646L8.172,4.464C7.976,4.269 7.66,4.269 7.464,4.464C7.269,4.66 7.269,4.976 7.464,5.172L10.293,8L7.464,10.828C7.269,11.024 7.269,11.34 7.464,11.535C7.66,11.731 7.976,11.731 8.172,11.535L11.354,8.354ZM0.5,0V6H1.5V0H0.5ZM3,8.5H11V7.5H3V8.5ZM0.5,6C0.5,7.381 1.619,8.5 3,8.5V7.5C2.172,7.5 1.5,6.828 1.5,6H0.5Z"
android:fillColor="#D9D9D9" />
android:fillColor="#D9D9D9"
android:pathData="M11.354,8.354C11.549,8.158 11.549,7.842 11.354,7.646L8.172,4.464C7.976,4.269 7.66,4.269 7.464,4.464C7.269,4.66 7.269,4.976 7.464,5.172L10.293,8L7.464,10.828C7.269,11.024 7.269,11.34 7.464,11.535C7.66,11.731 7.976,11.731 8.172,11.535L11.354,8.354ZM0.5,0V6H1.5V0H0.5ZM3,8.5H11V7.5H3V8.5ZM0.5,6C0.5,7.381 1.619,8.5 3,8.5V7.5C2.172,7.5 1.5,6.828 1.5,6H0.5Z" />
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
android:viewportWidth="18"
android:viewportHeight="16">
<path
android:pathData="M5.312,1.613C5.54,1.271 5.849,0.99 6.212,0.796C6.575,0.602 6.98,0.5 7.392,0.5H10.608C11.02,0.5 11.425,0.602 11.788,0.796C12.151,0.99 12.46,1.271 12.688,1.613L13.365,2.629C13.441,2.743 13.544,2.837 13.665,2.901C13.786,2.966 13.921,3 14.058,3H14.833C15.497,3 16.132,3.263 16.601,3.732C17.07,4.201 17.333,4.837 17.333,5.5V13C17.333,13.663 17.07,14.299 16.601,14.768C16.132,15.237 15.497,15.5 14.833,15.5H3.167C2.504,15.5 1.868,15.237 1.399,14.768C0.93,14.299 0.667,13.663 0.667,13V5.5C0.667,4.837 0.93,4.201 1.399,3.732C1.868,3.263 2.504,3 3.167,3H3.942C4.079,3 4.214,2.966 4.335,2.901C4.456,2.837 4.559,2.743 4.635,2.629L5.312,1.613ZM7.333,8.833C7.333,8.391 7.509,7.967 7.822,7.655C8.134,7.342 8.558,7.167 9,7.167C9.442,7.167 9.866,7.342 10.179,7.655C10.491,7.967 10.667,8.391 10.667,8.833C10.667,9.275 10.491,9.699 10.179,10.012C9.866,10.324 9.442,10.5 9,10.5C8.558,10.5 8.134,10.324 7.822,10.012C7.509,9.699 7.333,9.275 7.333,8.833ZM9,5.5C8.116,5.5 7.268,5.851 6.643,6.476C6.018,7.101 5.667,7.949 5.667,8.833C5.667,9.717 6.018,10.565 6.643,11.19C7.268,11.816 8.116,12.167 9,12.167C9.884,12.167 10.732,11.816 11.357,11.19C11.982,10.565 12.333,9.717 12.333,8.833C12.333,7.949 11.982,7.101 11.357,6.476C10.732,5.851 9.884,5.5 9,5.5Z"
android:fillColor="#545454"
android:fillType="evenOdd" />
android:fillType="evenOdd"
android:pathData="M5.312,1.613C5.54,1.271 5.849,0.99 6.212,0.796C6.575,0.602 6.98,0.5 7.392,0.5H10.608C11.02,0.5 11.425,0.602 11.788,0.796C12.151,0.99 12.46,1.271 12.688,1.613L13.365,2.629C13.441,2.743 13.544,2.837 13.665,2.901C13.786,2.966 13.921,3 14.058,3H14.833C15.497,3 16.132,3.263 16.601,3.732C17.07,4.201 17.333,4.837 17.333,5.5V13C17.333,13.663 17.07,14.299 16.601,14.768C16.132,15.237 15.497,15.5 14.833,15.5H3.167C2.504,15.5 1.868,15.237 1.399,14.768C0.93,14.299 0.667,13.663 0.667,13V5.5C0.667,4.837 0.93,4.201 1.399,3.732C1.868,3.263 2.504,3 3.167,3H3.942C4.079,3 4.214,2.966 4.335,2.901C4.456,2.837 4.559,2.743 4.635,2.629L5.312,1.613ZM7.333,8.833C7.333,8.391 7.509,7.967 7.822,7.655C8.134,7.342 8.558,7.167 9,7.167C9.442,7.167 9.866,7.342 10.179,7.655C10.491,7.967 10.667,8.391 10.667,8.833C10.667,9.275 10.491,9.699 10.179,10.012C9.866,10.324 9.442,10.5 9,10.5C8.558,10.5 8.134,10.324 7.822,10.012C7.509,9.699 7.333,9.275 7.333,8.833ZM9,5.5C8.116,5.5 7.268,5.851 6.643,6.476C6.018,7.101 5.667,7.949 5.667,8.833C5.667,9.717 6.018,10.565 6.643,11.19C7.268,11.816 8.116,12.167 9,12.167C9.884,12.167 10.732,11.816 11.357,11.19C11.982,10.565 12.333,9.717 12.333,8.833C12.333,7.949 11.982,7.101 11.357,6.476C10.732,5.851 9.884,5.5 9,5.5Z" />
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="20dp"
android:viewportWidth="16"
android:viewportHeight="20">
<path
android:fillColor="#D9D9D9"
android:pathData="M7,13H9V4H11L8,0L5,4H7V13ZM15,7H12V9H14V18H2V9H4V7H1C0.735,7 0.48,7.105 0.293,7.293C0.105,7.48 0,7.735 0,8V19C0,19.265 0.105,19.52 0.293,19.707C0.48,19.895 0.735,20 1,20H15C15.265,20 15.52,19.895 15.707,19.707C15.895,19.52 16,19.265 16,19V8C16,7.735 15.895,7.48 15.707,7.293C15.52,7.105 15.265,7 15,7Z" />
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M11.807,2.693C12.067,2.433 12.067,2 11.807,1.753L10.247,0.193C10,-0.067 9.567,-0.067 9.307,0.193L8.08,1.413L10.58,3.913M0,9.5V12H2.5L9.873,4.62L7.373,2.12L0,9.5Z"
android:fillColor="#ffffff" />
android:fillColor="#ffffff"
android:pathData="M11.807,2.693C12.067,2.433 12.067,2 11.807,1.753L10.247,0.193C10,-0.067 9.567,-0.067 9.307,0.193L8.08,1.413L10.58,3.913M0,9.5V12H2.5L9.873,4.62L7.373,2.12L0,9.5Z" />
</vector>
Loading
Loading