Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6ed1b28
feat: 컬렉션 도메인 레이어 구현 및 관련 모델 정의
devfeijoa Sep 7, 2026
5764f20
feat: 컬렉션 데이터 레이어 구현
devfeijoa Sep 7, 2026
7730dcc
feat: 컬렉션 관련 리소스(아이콘, 이미지, 문자열) 추가
devfeijoa Sep 7, 2026
01cc59f
feat: 컬렉션 리스트 및 확인 다이얼로그 컴포넌트 구현
devfeijoa Sep 7, 2026
3cc14fb
feat: 컬렉션 생성 및 수정 기능 구현 및 관련 로직 강화
devfeijoa Sep 7, 2026
d006816
feat: 컬렉션 상세 화면과 정렬 및 관리 메뉴 구현
devfeijoa Sep 7, 2026
f8765ef
feat: 컬렉션 미리보기(CollectionPreview) 컴포넌트 및 뷰모델 구현
devfeijoa Sep 7, 2026
84af4fc
feat: data-collection 모듈 생성 및 의존성 설정
devfeijoa Sep 7, 2026
68ab96a
feat: 컬렉션 상세 조회, 편집 및 페이징 연동 구현
devfeijoa Sep 7, 2026
c7b718b
feat: 컬렉션 상세 화면 ViewModel 구현
devfeijoa Sep 7, 2026
de1ca58
feat: 타인 서재 내 컬렉션 미리보기 연동 및 UI 구조 개선
devfeijoa Sep 7, 2026
44bfc0a
refactor: 코드 포맷팅 및 가독성 개선
devfeijoa Sep 7, 2026
b8eade6
feat: 타인 서재 내 컬렉션 프리뷰 UI 및 빈 화면 처리 구현
devfeijoa Sep 7, 2026
d4d2a5f
feat: 컬렉션 작품 검색 화면 뒤로가기 시 확인 다이얼로그 추가
devfeijoa Sep 8, 2026
3578694
feat: 카카오 공유 링크를 통한 컬렉션 상세 딥링크 기능 구현
devfeijoa Sep 8, 2026
a479b07
feat: 컬렉션 카카오톡 공유 기능 구현
devfeijoa Sep 8, 2026
af9fffb
refactor: ktlint 코드 포맷팅 수정
devfeijoa Sep 8, 2026
aa41e82
fix: 컬렉션 데이터 로드 시 사용자 ID 조회 타임아웃 처리 추가
devfeijoa Sep 9, 2026
3046e20
refactor: 컬렉션 상세 화면 내 정렬 기능 고도화
devfeijoa Sep 9, 2026
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
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ dependencies {
implementation(projects.core.datastore)

implementation(projects.data.account)
implementation(projects.data.collection)
implementation(projects.data.library)
implementation(projects.data.feed)

Expand Down Expand Up @@ -133,6 +134,7 @@ dependencies {

// Third-party SDK
implementation(libs.kakao) // 카카오 로그인 API
implementation(libs.kakao.share) // 카카오톡 공유 API
implementation(libs.amplitude) // Amplitude

// Firebase
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<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="kakaolink"
android:scheme="kakao${kakaoAppKey}" />
</intent-filter>
</activity>
<activity
android:name=".ui.login.LoginActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,107 @@ package com.into.websoso.ui.collection
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.google.firebase.Firebase
import com.google.firebase.analytics.analytics
import com.google.firebase.analytics.logEvent
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.core.resource.R
import com.into.websoso.feature.collection.CollectionNavHost
import com.into.websoso.feature.collection.model.CollectionShareContent
import com.into.websoso.ui.novelDetail.NovelDetailActivity
import com.kakao.sdk.share.ShareClient
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class CollectionActivity : ComponentActivity() {
private var isSharing by mutableStateOf(false)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
WebsosoTheme {
CollectionNavHost(onNavigateBack = ::finish)
CollectionNavHost(
onNavigateBack = ::finish,
onNovelClick = { startActivity(NovelDetailActivity.getIntent(this, it)) },
onShare = ::shareCollection,
onShareBlocked = ::logShareBlocked,
isSharing = isSharing,
initialCollectionId = intent.getLongExtra(COLLECTION_ID, 0L).takeIf { it > 0L },
userId = intent.getLongExtra(USER_ID, 0L).takeIf { it > 0L },
)
}
}
}

override fun onResume() {
super.onResume()
isSharing = false
}

private fun shareCollection(content: CollectionShareContent) {
if (isSharing) return
if (!ShareClient.instance.isKakaoTalkSharingAvailable(this)) {
Toast.makeText(this, R.string.collection_share_kakao_required, Toast.LENGTH_SHORT).show()
return
}
isSharing = true
runCatching {
ShareClient.instance.shareCustom(
context = this,
templateId = CollectionKakaoShare.templateId(content),
templateArgs = CollectionKakaoShare.templateArgs(content),
) { result, error ->
isSharing = false
if (isFinishing || isDestroyed) return@shareCustom
if (error != null || result == null) {
showShareError()
} else {
runCatching { startActivity(result.intent) }.onFailure { showShareError() }
}
}
}.onFailure {
isSharing = false
showShareError()
}
}

private fun showShareError() {
Toast.makeText(this, R.string.collection_share_failed, Toast.LENGTH_SHORT).show()
}

private fun logShareBlocked(
collectionId: Long,
novelCount: Int,
novelsSize: Int,
isPublic: Boolean,
) {
Firebase.analytics.logEvent("collection_share_blocked") {
param("collection_id", collectionId)
param("novel_count", novelCount.toLong())
param("novels_size", novelsSize.toLong())
param("is_public", if (isPublic) 1L else 0L)
}
}

companion object {
fun getIntent(context: Context): Intent = Intent(context, CollectionActivity::class.java)
private const val COLLECTION_ID = "COLLECTION_ID"
private const val USER_ID = "USER_ID"

fun getIntent(
context: Context,
collectionId: Long? = null,
userId: Long? = null,
): Intent =
Intent(context, CollectionActivity::class.java).apply {
collectionId?.let { putExtra(COLLECTION_ID, it) }
userId?.let { putExtra(USER_ID, it) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.into.websoso.ui.collection

import android.content.Intent
import java.net.URI
import java.net.URLDecoder

internal object CollectionDeepLink {
const val PENDING_COLLECTION_ID = "pendingCollectionId"

fun parseCollectionId(
link: String?,
expectedScheme: String,
): Long? =
runCatching {
val uri = URI(link ?: return null)
if (uri.scheme != expectedScheme || uri.rawAuthority != "kakaolink") return null
if (!uri.rawPath.isNullOrEmpty() && uri.rawPath != "/") return null
if (uri.rawFragment != null) return null

val values = uri.rawQuery
?.split('&')
?.map { it.split('=', limit = 2) }
?.filter { URLDecoder.decode(it[0], "UTF-8") == "collectionId" }
?: return null
val value = values.singleOrNull()?.getOrNull(1) ?: return null
val decoded = URLDecoder.decode(value, "UTF-8")
if (decoded.isEmpty() || decoded.any { it !in '0'..'9' }) return null
decoded.toLongOrNull()?.takeIf { it > 0L }
}.getOrNull()

fun forward(
source: Intent,
destination: Intent,
): Intent =
destination.apply {
source.getLongExtra(PENDING_COLLECTION_ID, 0L).takeIf { it > 0L }?.let {
putExtra(PENDING_COLLECTION_ID, it)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.into.websoso.ui.collection

import com.into.websoso.feature.collection.model.CollectionShareContent

internal object CollectionKakaoShare {
// 카카오 디벨로퍼스 콘솔의 메시지 템플릿(카카오링크)과 1:1로 연결된 ID.
// 콘솔에서 템플릿을 재생성하면 값이 바뀌므로 함께 갱신해야 한다.
private const val TEMPLATE_ID_ONE_COVER = 136782L
private const val TEMPLATE_ID_TWO_COVERS = 136783L
private const val TEMPLATE_ID_THREE_COVERS = 136784L

fun templateId(content: CollectionShareContent): Long =
when (content.imageUrls.size) {
1 -> TEMPLATE_ID_ONE_COVER
2 -> TEMPLATE_ID_TWO_COVERS
3 -> TEMPLATE_ID_THREE_COVERS
else -> error("Collection sharing requires 1 to 3 covers")
}

fun templateArgs(content: CollectionShareContent): Map<String, String> =
buildMap {
put("collectionId", content.collectionId.toString())
put("TITLE", content.title)
put("NICKNAME", content.nickname)
content.imageUrls.forEachIndexed { index, url -> put("IMAGE${index + 1}", url) }
}
}
8 changes: 7 additions & 1 deletion app/src/main/java/com/into/websoso/ui/login/LoginActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.into.websoso.core.common.navigator.NavigatorProvider
import com.into.websoso.core.common.util.setupSystemBarIconColor
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.feature.signin.SignInScreen
import com.into.websoso.ui.collection.CollectionDeepLink
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

Expand All @@ -39,12 +40,17 @@ class LoginActivity : ComponentActivity() {
WebsosoTheme {
SignInScreen(
authClient = { platform -> authClient[platform] },
websosoNavigator = websosoNavigator,
onNavigateToHome = { websosoNavigator.navigateToMainActivity(::startDestination) },
onNavigateToOnboarding = { websosoNavigator.navigateToOnboardingActivity(::startDestination) },
)
}
}
}

private fun startDestination(destination: Intent) {
startActivity(CollectionDeepLink.forward(intent, destination))
}

companion object {
fun getIntent(context: Context): Intent =
Intent(context, LoginActivity::class.java).apply {
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/com/into/websoso/ui/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import com.into.websoso.core.common.util.showWebsosoSnackBar
import com.into.websoso.core.resource.R.drawable.ic_blocked_user_snack_bar
import com.into.websoso.core.resource.R.string.main_back_press
import com.into.websoso.databinding.ActivityMainBinding
import com.into.websoso.ui.collection.CollectionActivity
import com.into.websoso.ui.collection.CollectionDeepLink
import com.into.websoso.ui.common.dialog.LoginRequestDialogFragment
import com.into.websoso.ui.main.MainActivity.FragmentType.FEED
import com.into.websoso.ui.main.MainActivity.FragmentType.HOME
Expand Down Expand Up @@ -62,6 +64,15 @@ class MainActivity : BaseActivity<ActivityMainBinding>(activity_main) {
) { _, _ ->
handleNavigation(LIBRARY)
}
if (savedInstanceState == null) openPendingCollection()
}

private fun openPendingCollection() {
val collectionId = intent.getLongExtra(CollectionDeepLink.PENDING_COLLECTION_ID, 0L)
intent.removeExtra(CollectionDeepLink.PENDING_COLLECTION_ID)
if (collectionId > 0L) {
startActivity(CollectionActivity.getIntent(this, collectionId = collectionId))
}
}

private fun setupBackButtonListener() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import com.into.websoso.core.resource.R.string.my_library_attractive_point_fixed
import com.into.websoso.data.model.GenrePreferenceEntity
import com.into.websoso.data.model.NovelPreferenceEntity
import com.into.websoso.databinding.FragmentMyPageBinding
import com.into.websoso.feature.collection.component.CollectionEntry
import com.into.websoso.feature.collection.CollectionPreview
import com.into.websoso.ui.collection.CollectionActivity
import com.into.websoso.ui.main.MainViewModel
import com.into.websoso.ui.main.myPage.adapter.RestGenrePreferenceAdapter
Expand Down Expand Up @@ -101,7 +101,10 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(fragment_my_page) {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
WebsosoTheme {
CollectionEntry(onClick = ::navigateToCollection)
CollectionPreview(
onListClick = ::navigateToCollection,
onCollectionClick = { startActivity(CollectionActivity.getIntent(requireContext(), collectionId = it)) },
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.into.websoso.R
import com.into.websoso.core.common.ui.base.BaseActivity
import com.into.websoso.core.common.util.SingleEventHandler
import com.into.websoso.databinding.ActivityOnboardingBinding
import com.into.websoso.ui.collection.CollectionDeepLink
import com.into.websoso.ui.onboarding.model.OnboardingPage
import com.into.websoso.ui.onboarding.welcome.WelcomeActivity
import com.into.websoso.ui.termsAgreement.TermsAgreementDialogBottomSheet
Expand Down Expand Up @@ -54,7 +55,7 @@ class OnboardingActivity : BaseActivity<ActivityOnboardingBinding>(R.layout.acti
viewModel.isUserProfileSubmit.observe(this) { isUserProfileSubmit ->
if (isUserProfileSubmit) {
val nickname = viewModel.currentNicknameInput.value ?: "웹소소"
startActivity(WelcomeActivity.getIntent(this, nickname))
startActivity(CollectionDeepLink.forward(intent, WelcomeActivity.getIntent(this, nickname)))
finish()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.into.websoso.R.layout.activity_welcome
import com.into.websoso.core.common.ui.base.BaseActivity
import com.into.websoso.core.resource.R.string.welcome_hello_user
import com.into.websoso.databinding.ActivityWelcomeBinding
import com.into.websoso.ui.collection.CollectionDeepLink
import com.into.websoso.ui.main.MainActivity

class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>(activity_welcome) {
Expand All @@ -25,7 +26,7 @@ class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>(activity_welcome) {

private fun onCompleteButtonClick() {
binding.btnWelcomeStart.setOnClickListener {
startActivity(MainActivity.getIntent(this, true))
startActivity(CollectionDeepLink.forward(intent, MainActivity.getIntent(this, true)))
finish()
}
}
Expand Down
Loading
Loading