Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ dependencies {
implementation(projects.feature.signin)
implementation(projects.feature.library)
implementation(projects.feature.feed)
implementation(projects.feature.collection)

// AndroidX 및 Jetpack 기본 라이브러리
implementation(libs.androidx.core.ktx)
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@
android:name=".ui.main.MainActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".ui.collection.CollectionActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".ui.notification.NotificationActivity"
android:exported="false"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.into.websoso.ui.collection

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.feature.collection.CollectionNavHost
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class CollectionActivity : ComponentActivity() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

activity로 두는게 아닌 SAA 형태로 조금씩 바꾸는게 좋을 것 같습니다. 굳이 레거시 구조를 유지할 이유가 없는 것 같아요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

SAA를 지향하는 방향에는 공감합니다!
다만 현재 앱에는 앱 레벨의 Compose NavHost가 없고, MainActivityFragmentContainerViewFragmentManager를 통해 홈/피드/서재/마이페이지 Fragment를 직접 전환하고 있습니다.
MyPageFragment 역시 이번 작업에서 추가한 것이 아니라 기존 화면입니다.
컬렉션 진입 구조를 설계하면서 아래와 같은 방법들을 고려해보았습니다.

  • CollectionFragment를 추가하면 기존 Fragment 구조를 다시 확장하게 됩니다.
  • 컬렉션을 MainActivity의 Compose destination으로 연결하려면 하단 탭, 기존 Fragment 호스팅, 뒤로 가기 처리를 포함한 앱 단위 내비게이션 변경이 필요합니다.
  • 별도 CollectionActivity를 사용하면 기존 마이페이지와 메인 내비게이션에는 영향을 주지 않으면서, 새로 개발하는 컬렉션 내부 흐름은 Compose Navigation으로 구성할 수 있습니다.

이번 PR은 컬렉션 기능을 단계적으로 추가하는 첫 PR이기 때문에 기존 화면의 마이그레이션까지 범위를 넓히기보다, 수정 범위를 컬렉션에 집중하는 편이 안전하다고 판단해 CollectionActivity를 진입 경계로 선택했습니다. Activity가 추가되어 향후 SAA 전환 시 마이그레이션 대상이 늘어난다는 단점은 있지만, 현재 구조와 PR 범위에서는 가장 적절한 선택이라고 보았습니다.
SAA 로의 전환은 MainActivity 내비게이션 마이그레이션 작업으로 별도 분리하는 것이 적절하다고 생각합니다.
혹시 기존 메인 구조를 크게 변경하지 않으면서 이번 PR에서 점진적으로 적용할 수 있다고 생각하신 구체적인 구조가 있다면 의견 부탁드립니다!

+공식 Android 문서에서도 기존 View/Fragment 앱은 Compose와 공존시키며 화면 단위로 점진적으로 이전하도록 권장하고 있습니다. Fragment가 남아 있는 동안 ComposeView를 사용하는 것도 전환 방식으로 안내하고 있으며, 모든 navigation destination을 Composable로 전환할 수 있을 때 Navigation Compose로 마이그레이션하도록 설명하고 있습니다.
Compose 마이그레이션 전략, ComposeView와 Fragment 연동

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

setContent {
WebsosoTheme {
CollectionNavHost()
}
}
}

companion object {
fun getIntent(context: Context): Intent = Intent(context, CollectionActivity::class.java)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

fragment도 현재 compose 환경에서는 잘 사용되지 않아 레거시인것 같습니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Compose-first 구조의 최종 방향에서 Fragment를 제거하는 것이 좋다는 점에는 공감합니다!
다만 MyPageFragment는 이번 작업에서 새로 추가한 것이 아니라 기존 MainActivity의 마이페이지 탭을 구성하던 화면입니다. 이번 PR에서는 기존 마이페이지 전체를 변경하지 않고 컬렉션 진입부만 Compose로 구현하기 위해 ComposeView를 추가했습니다.

MyPageFragment를 제거하려면 기존 마이페이지 XML과 ViewModel 연결, 스크롤 및 툴바 동작 등 컬렉션과 관계없는 영역까지 함께 마이그레이션해야 합니다. 해당 작업은 이번 컬렉션 PR의 범위를 크게 벗어나기 때문에, 따라서 이번에는 기존 Fragment를 유지하고 ComposeView로 점진적 적용하게끔 선택했습니다. 마이페이지 전체의 Compose 전환은 별도 작업으로 분리하는 것이 안전하다고 판단했습니다.

@Sadturtleman Sadturtleman Aug 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

마이페이지 전체를 compose로 분리하는 것이 아닌 MyPageFragment, HomeFragment, FeedFragment는 지금 그대로 MainActivity의 탭으로 두고, 컬렉션만 앱 레벨 Compose NavHost의 destination으로 붙이면 마이페이지에 대한 이전은 나중에 하고 collection만 간단하게 변경할 수 있을거라 생각됩니다!

여기 ai가 알려준 상세 코드 첨부합니다.
구체적인 방법: MainActivity에 얇은 NavHost 레이어 추가

activity_main.xml에 fcv_main 위를 덮는 ComposeView 하나만 추가합니다.

<androidx.compose.ui.platform.ComposeView
android:id="@+id/cv_main_overlay"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

MainActivity에 앱 레벨 NavHost를 두되, startDestination은 아무것도 그리지 않는 main 으로 둡니다. 즉 기본 상태에서는 기존 Fragment 화면이 그대로 보이고, Compose destination으로
이동한 순간에만 오버레이가 올라옵니다.

// app/src/main/java/com/into/websoso/ui/main/AppNavHost.kt
@composable
fun AppNavHost(
navController: NavHostController,
modifier: Modifier = Modifier,
) {
NavHost(navController, startDestination = MainRoute, modifier = modifier) {
composable { } // Fragment 탭 영역 — 아무것도 그리지 않음
collectionNavGraph(navController) // feature:collection이 제공하는 그래프
}
}

// MainActivity                                                                                                                                                                    

private lateinit var navController: NavHostController

private fun setupComposeNavHost() {
binding.cvMainOverlay.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
navController = rememberNavController()
WebsosoTheme { AppNavHost(navController) }

          val entry by navController.currentBackStackEntryAsState()                                                                                                              
          val isRoot = entry?.destination?.hasRoute<MainRoute>() ?: true                                                                                                         
          LaunchedEffect(isRoot) {                                                                                                                                               
              binding.cvMainOverlay.isVisible = !isRoot                                                                                                                          
              binding.fcvMain.isVisible = isRoot                                                                                                                                 
              binding.bnvMain.isVisible = isRoot                                                                                                                                 
              binding.viewMainDivider.isVisible = isRoot                                                                                                                         
              backPressedCallback.isEnabled = isRoot   // ★ 중요                                                                                                                 
          }                                                                                                                                                                      
      }                                                                                                                                                                          
  }                                                                                                                                                                              

}

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
import android.text.style.ForegroundColorSpan
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
Expand All @@ -28,13 +29,15 @@ import com.into.websoso.core.common.util.SingleEventHandler
import com.into.websoso.core.common.util.getS3ImageUrl
import com.into.websoso.core.common.util.setListViewHeightBasedOnChildren
import com.into.websoso.core.common.util.tracker.Tracker
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.core.resource.R.drawable.img_loading_thumbnail
import com.into.websoso.core.resource.R.string.my_library_attractive_point_fixed_text
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.CollectionEntry
import com.into.websoso.ui.collection.CollectionActivity
import com.into.websoso.ui.main.MainViewModel
import com.into.websoso.ui.main.myPage.MyLibraryViewModel
import com.into.websoso.ui.main.myPage.adapter.RestGenrePreferenceAdapter
import com.into.websoso.ui.main.myPage.model.MyLibraryUiState
import com.into.websoso.ui.profileEdit.ProfileEditActivity
Expand Down Expand Up @@ -77,6 +80,7 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(fragment_my_page) {
setupObserver()
onProfileEditClick()
onStorageButtonClick()
setupCollectionEntry()
tracker.trackEvent("mypage")
}

Expand All @@ -92,6 +96,21 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(fragment_my_page) {
binding.lifecycleOwner = viewLifecycleOwner
}

private fun setupCollectionEntry() {
binding.cvMyPageCollectionEntry.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
WebsosoTheme {
CollectionEntry(onClick = ::navigateToCollection)
}
}
}
}

private fun navigateToCollection() {
startActivity(CollectionActivity.getIntent(requireContext()))
}

private fun setupRestGenrePreferenceAdapter() {
binding.lvMyLibraryRestGenre.adapter = restGenrePreferenceAdapter
}
Expand Down
21 changes: 19 additions & 2 deletions app/src/main/res/layout/fragment_my_page.xml
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,28 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cl_my_page_user_profile" />

<androidx.compose.ui.platform.ComposeView
android:id="@+id/cv_my_page_collection_entry"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/view_my_page_profile_divider" />

<View
android:id="@+id/view_my_page_collection_divider"
android:layout_width="0dp"
android:layout_height="3dp"
android:background="@color/gray_50_F4F5F8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cv_my_page_collection_entry" />

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_my_library_genre_preference"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/view_my_page_profile_divider"
app:layout_constraintTop_toBottomOf="@id/view_my_page_collection_divider"
tools:layout_editor_absoluteX="0dp">

<TextView
Expand Down Expand Up @@ -447,7 +464,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/view_my_page_profile_divider">
app:layout_constraintTop_toBottomOf="@id/view_my_page_collection_divider">

<TextView
android:id="@+id/tv_my_library_preference_analysis_title"
Expand Down
13 changes: 13 additions & 0 deletions feature/collection/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import com.into.websoso.setNamespace

plugins {
id("websoso.android.feature")
}

android {
setNamespace("feature.collection")
}

dependencies {
implementation(libs.navigation.compose)
}
4 changes: 4 additions & 0 deletions feature/collection/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.into.websoso.feature.collection

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.into.websoso.core.common.extensions.debouncedClickable
import com.into.websoso.core.designsystem.theme.Gray300
import com.into.websoso.core.designsystem.theme.Primary100
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.core.designsystem.theme.White
import com.into.websoso.core.resource.R

@Composable
fun CollectionEntry(
onClick: () -> Unit,
modifier: Modifier = Modifier,
collectionCount: Int = 0,
) {
Row(
modifier = modifier
.fillMaxWidth()
.background(White)
.debouncedClickable(onClick = onClick)
.padding(
horizontal = 20.dp,
vertical = 20.dp,
),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
Comment thread
Sadturtleman marked this conversation as resolved.
text = buildAnnotatedString {
append("컬렉션 ")
withStyle(style = SpanStyle(color = Primary100)) {
append(collectionCount.toString())
}
append("개")
},
color = Gray300,
style = WebsosoTheme.typography.title2,
)
Spacer(modifier = Modifier.weight(1f))
Image(
imageVector = ImageVector.vectorResource(R.drawable.btn_setting_right),
contentDescription = null,
modifier = Modifier.size(24.dp),
)
}
}

@Preview(showBackground = true)
@Composable
private fun CollectionEntryPreview() {
WebsosoTheme {
CollectionEntry(onClick = {})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.into.websoso.feature.collection

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

private const val COLLECTION_ROUTE = "collection"

@Composable
fun CollectionNavHost(modifier: Modifier = Modifier) {
val navController = rememberNavController()

NavHost(
navController = navController,
startDestination = COLLECTION_ROUTE,
modifier = modifier,
) {
composable(route = COLLECTION_ROUTE) {
CollectionScreen()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.into.websoso.feature.collection

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.into.websoso.core.designsystem.theme.Black
import com.into.websoso.core.designsystem.theme.Gray200
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.core.designsystem.theme.White

@Composable
fun CollectionScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.background(White)
.padding(horizontal = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
text = "컬렉션",
color = Black,
style = WebsosoTheme.typography.headline1,
)
Text(
text = "임시화면",
color = Gray200,
style = WebsosoTheme.typography.body2,
)
}
}

@Preview(showBackground = true)
@Composable
private fun CollectionScreenPreview() {
WebsosoTheme {
CollectionScreen()
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ lifecycle-extensions = "2.2.0"
datastore-preferences = "1.2.0"
security-crypto = "1.1.0"
paging = "3.3.6"
navigation = "2.9.8"

# Testing Libraries
junit = "4.13.2"
Expand Down Expand Up @@ -167,6 +168,7 @@ compose-material3 = { module = "androidx.compose.material3:material3" }
compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-hilt-navigation = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose" }
androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }

Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ include(
":feature:signin",
":feature:library",
":feature:feed",
":feature:collection",
)
include(":domain:feed")
include(":data:user")
Loading