diff --git a/app/src/main/java/com/linku/MainApp.kt b/app/src/main/java/com/linku/MainApp.kt index 104e16b2..462b376d 100644 --- a/app/src/main/java/com/linku/MainApp.kt +++ b/app/src/main/java/com/linku/MainApp.kt @@ -79,6 +79,10 @@ fun MainApp( } } + // 닉네임 최상단 뒤치(사용하는 스크린) + val nickname by viewModel.nickname.collectAsStateWithLifecycle() + + // 앱 실행 시 실행하여 이전 계정 기록 삭제 // FIXME : 지민님한테 여쭈어보기. 매번 앱 실행할 때마다 최근 기록을 지우는 것보다는 로그아웃 때 지우는건 어떤지 LaunchedEffect(Unit) { @@ -125,6 +129,14 @@ fun MainApp( val navBackStackEntry by navigator.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route + LaunchedEffect(currentRoute) { + if (currentRoute == NavigationRoute.Home.route || + currentRoute == "curation_list" + ) { + viewModel.fetchNickname() + } + } + fun isTabRoute(current: String?, root: String): Boolean = current == root || current?.startsWith("$root/") == true || current?.startsWith("$root?") == true @@ -348,15 +360,6 @@ fun MainApp( showNavBar = true } - // 홈 탭 진입 및 복귀 할 때마다 닉네임 갱신 - LaunchedEffect(currentRoute) { - if (currentRoute == NavigationRoute.Home.route) { - viewModel.fetchNickname() - } - } - - val nickname by viewModel.nickname.collectAsStateWithLifecycle() - HomeApp( viewModel = homeViewModel, nickname = nickname.orEmpty().ifBlank { "링큐" }, @@ -392,7 +395,8 @@ fun MainApp( // 큐레이션 파트 리팩토링 적용 curationGraph( navigator = navigator, - showNavBar = { showNavBar = it } + showNavBar = { showNavBar = it }, + nickname = nickname.orEmpty().ifBlank { "링큐" } ) diff --git a/core/src/main/java/com/linku/core/repository/CurationRepository.kt b/core/src/main/java/com/linku/core/repository/CurationRepository.kt index 5849aae1..48b7ac1c 100644 --- a/core/src/main/java/com/linku/core/repository/CurationRepository.kt +++ b/core/src/main/java/com/linku/core/repository/CurationRepository.kt @@ -1,22 +1,6 @@ package com.linku.core.repository -import com.linku.core.model.CurationDetail -import com.linku.core.model.RecommendedLink - - +/** princeHw 작업 공간 */ interface CurationRepository { - //좋아요 기능 자체 삭제. - // 최근 큐레이션 불러오기 -// suspend fun getMyRecentCuration(userId: Long): CurationItem - - //추천(큐레이션 디테일) - suspend fun getRecommendedLinks(userId: Long, curationId: Long): List - - //사용자 디테일 정보 제공(큐레이션 디테일) - suspend fun getCurationDetail(curationId: Long): CurationDetail - - //큐레이션 추천(기본 페이지) - suspend fun getHomeRecommendedLinksTop2(userId: Long, curationId: Long): List - } diff --git a/data/src/main/java/com/linku/data/api/CurationApi.kt b/data/src/main/java/com/linku/data/api/CurationApi.kt index 36cfcd9d..b098e659 100644 --- a/data/src/main/java/com/linku/data/api/CurationApi.kt +++ b/data/src/main/java/com/linku/data/api/CurationApi.kt @@ -1,40 +1,7 @@ package com.linku.data.api -import com.linku.data.api.dto.BaseResponse -import com.linku.data.api.dto.server.CurationDetailResponse -import com.linku.data.api.dto.server.RecommendLinkItemDto -import retrofit2.http.GET -import retrofit2.http.Path -import retrofit2.http.Query -import retrofit2.Response -import okhttp3.ResponseBody +/** princeHw 작업 공간 */ interface CurationApi { - @GET("curations/latest/{userId}") - suspend fun getMyRecentCurationRaw( - @Path("userId") userId: Long - ): Response - - - @GET("curations/recommend-links") - suspend fun getRecommendLinks( - @Query("userId") userId: Long, - @Query("curationId") curationId: Long - ): BaseResponse> - - //큐레이션 디테일 -> 현재 화면에사 연동 뺌. ui 수정이 예정 되어 있음. - @GET("curations/detail/{curationId}") - suspend fun getCurationDetail( - @Path("curationId") curationId: Long - ): BaseResponse - - - //큐레이션 기본 페이지 추천 - @GET("curations/recommend-links/internal/top2") - suspend fun getInternalTop2( - @Query("userId") userId: Long, - @Query("curationId") curationId: Long - ): BaseResponse> - } \ No newline at end of file diff --git a/data/src/main/java/com/linku/data/di/repository/CurationRepositoryModule.kt b/data/src/main/java/com/linku/data/di/repository/CurationRepositoryModule.kt index f70dca01..77a67958 100644 --- a/data/src/main/java/com/linku/data/di/repository/CurationRepositoryModule.kt +++ b/data/src/main/java/com/linku/data/di/repository/CurationRepositoryModule.kt @@ -2,45 +2,25 @@ package com.linku.data.di.repository import com.linku.core.repository.CurationRepository import com.linku.data.api.CurationApi -import com.linku.data.api.ServerApi import com.linku.data.implementation.repository.CurationRepositoryImpl -import com.linku.data.preference.AuthPreference -import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton -/* 미래의 제가 수정할겁니다. */ + @Module @InstallIn(SingletonComponent::class) object CurationRepositoryModule { - -// @Provides -// @Singleton -// fun provideLinkuRepository( -// serverApi: ServerApi, -// authPreference: AuthPreference -// ): CurationRepository { -// return CurationRepositoryImpl( -// serverApi = serverApi, -// authPreference = authPreference -// ) -// } @Provides @Singleton -fun provideCurationRepository( // 함수명도 의미 맞게 변경 권장 - serverApi: ServerApi, - curationApi: CurationApi, // ★ 추가 - authPreference: AuthPreference, - moshi: Moshi +fun provideCurationRepository( + curationApi: CurationApi, ): CurationRepository { return CurationRepositoryImpl( - serverApi = serverApi, curationApi = curationApi, - moshi = moshi ) } } \ No newline at end of file diff --git a/data/src/main/java/com/linku/data/implementation/repository/CurationRepositoryImpl.kt b/data/src/main/java/com/linku/data/implementation/repository/CurationRepositoryImpl.kt index b04fddc7..7d651a42 100644 --- a/data/src/main/java/com/linku/data/implementation/repository/CurationRepositoryImpl.kt +++ b/data/src/main/java/com/linku/data/implementation/repository/CurationRepositoryImpl.kt @@ -1,159 +1,14 @@ package com.linku.data.implementation.repository -import com.linku.core.model.CurationDetail -import com.linku.core.model.CurationItem -import com.linku.core.model.RecommendedLink import com.linku.core.repository.CurationRepository import com.linku.data.api.CurationApi -import com.linku.data.api.ServerApi -import com.linku.data.api.dto.server.CurationLatestResponse -import com.squareup.moshi.JsonReader -import com.squareup.moshi.Moshi -import okio.Buffer import javax.inject.Inject -/* 여기 아직 아무것도 연동 안해서 일단은 패스. -미래의 나에게 부탁하겠음- */ +/** princeHw 작업 공간 */ class CurationRepositoryImpl @Inject constructor( - private val serverApi: ServerApi, - private val curationApi: CurationApi, - private val moshi: Moshi, + private val curationApi: CurationApi ) : CurationRepository { -// override suspend fun getMyRecentCuration(userId: Long): CurationItem { -// Log.d("CurationRepo", "getMyRecentCuration() via RAW path") // ✅ 추가 -// // ✅ Raw 호출 -// val resp = curationApi.getMyRecentCurationRaw(userId) -// if (!resp.isSuccessful) throw HttpException(resp) -// -// val bodyStr = resp.body()?.string() ?: throw IllegalStateException("빈 응답") -// -// Log.d("CurationApi", "response body = $bodyStr") -// -// // ✅ result 키 존재 여부 먼저 확인 -// return if (hasResultKey(bodyStr)) { -// val type = Types.newParameterizedType( -// BaseResponse::class.java, -// CurationLatestResponse::class.java -// ) -// val adapter = moshi.adapter>(type) -// val base = adapter.fromJson(bodyStr) -// ?: throw IllegalStateException("파싱 실패(BaseResponse)") -// val dto = base.result // 팀 규칙상 non-null -// CurationItem( -// id = dto.curationId, -// month = dto.month, -// thumbnailUrl = dto.thumbnailUrl -// ) -// } else { -// val emptyAdapter = moshi.adapter(BaseEmptyResponse::class.java) -// val empty = emptyAdapter.fromJson(bodyStr) -// ?: throw IllegalStateException("파싱 실패(BaseEmptyResponse)") -// if (!empty.isSuccess) { -// throw IllegalStateException("최근 큐레이션 호출 실패: ${empty.code}/${empty.message}") -// } -// // 최신 없음 → VM에서 생성 플로우로 넘기기 -// throw NoSuchElementException("최근 큐레이션이 없습니다.") -// } -// } - - private fun hasResultKey(json: String): Boolean { - val reader = JsonReader.of(Buffer().writeUtf8(json)) - if (reader.peek() != JsonReader.Token.BEGIN_OBJECT) return false - reader.beginObject() - var found = false - while (reader.hasNext()) { - val name = reader.nextName() - if (name == "result") { found = true; reader.skipValue(); break } - else reader.skipValue() - } - while (reader.hasNext()) reader.skipValue() - reader.endObject() - return found - } - - - private fun CurationLatestResponse.toDomain(): CurationItem { - return CurationItem( - id = this.curationId, - month = this.month, - thumbnailUrl = this.thumbnailUrl - ) - } - - override suspend fun getRecommendedLinks( - userId: Long, - curationId: Long - ): List { - val res = curationApi.getRecommendLinks(userId, curationId) // BaseResponse< List<...> > - if (!res.isSuccess) throw IllegalStateException(res.message ?: "추천 링크 호출 실패") - val dtos = res.result ?: emptyList() - - return dtos.mapNotNull { dto -> - val title = dto.title?.trim().orEmpty() - val url = dto.url?.trim().orEmpty() - if (title.isBlank() || url.isBlank()) return@mapNotNull null - val normalizedDomain = dto.domain - ?.takeIf { it.isNotBlank() && it.lowercase() !in setOf("invalid", "unknown") } - ?: runCatching { java.net.URL(url).host }.getOrNull() - - RecommendedLink( - isInternal = dto.userLinkuId != null, - userLinkuId = dto.userLinkuId, - title = title, - url = url, - imageUrl = dto.imageUrl?.takeIf { it.isNotBlank() }, - domain = normalizedDomain, - domainImageUrl = dto.domainImageUrl?.takeIf { it.isNotBlank() }, - categories = dto.categories?.filter { it.isNotBlank() } - ) - }.take(9) - } - - //큐레이션 디테일 사용자 정보 제공 - override suspend fun getCurationDetail(curationId: Long): CurationDetail { - val res = curationApi.getCurationDetail(curationId) // BaseResponse - if (!res.isSuccess) throw IllegalStateException(res.message ?: "큐레이션 상세 조회 실패") - val dto = res.result - return CurationDetail( - curationId = dto.curationId, - month = dto.month, - topTags = dto.topTags.orEmpty().take(3), - headerMent = dto.headerMent, - footerMent = dto.footerMent - ) - } - - - //큐레이션 기본 페이지 추천 - override suspend fun getHomeRecommendedLinksTop2( - userId: Long, curationId: Long - ): List { - val res = curationApi.getInternalTop2(userId, curationId) // BaseResponse> - if (!res.isSuccess) throw IllegalStateException(res.message ?: "홈 추천 링크 호출 실패") - val dtos = res.result ?: emptyList() - return dtos.mapNotNull { dto -> - val title = dto.title?.trim().orEmpty() - val url = dto.url?.trim().orEmpty() - if (title.isBlank() || url.isBlank()) return@mapNotNull null - val normalizedDomain = dto.domain - ?.takeIf { it.isNotBlank() && it.lowercase() !in setOf("invalid", "unknown") } - ?: runCatching { java.net.URL(url).host }.getOrNull() - - RecommendedLink( - isInternal = true, - userLinkuId = dto.userLinkuId, - title = title, - url = url, - imageUrl = dto.imageUrl?.takeIf { it.isNotBlank() }, - domain = normalizedDomain, - domainImageUrl = dto.domainImageUrl?.takeIf { it.isNotBlank() }, - categories = dto.categories?.filter { it.isNotBlank() } - ) - }.take(2) - } - - - } diff --git a/data/src/main/java/com/linku/data/mapper/SocialProfileMapper.kt b/data/src/main/java/com/linku/data/mapper/SocialProfileMapper.kt index 98d71fe7..07068d5d 100644 --- a/data/src/main/java/com/linku/data/mapper/SocialProfileMapper.kt +++ b/data/src/main/java/com/linku/data/mapper/SocialProfileMapper.kt @@ -1,6 +1,9 @@ package com.linku.data.mapper -import com.linku.core.model.auth.* +import com.linku.core.model.auth.Gender +import com.linku.core.model.auth.Interest +import com.linku.core.model.auth.Job +import com.linku.core.model.auth.Purpose import com.linku.data.api.dto.auth.signup.social.SocialProfileRequestDTO object SocialProfileMapper { diff --git a/design/src/main/java/com/linku/design/theme/color/ThemeColorScheme.kt b/design/src/main/java/com/linku/design/theme/color/ThemeColorScheme.kt index 038b1ec9..b65e487f 100644 --- a/design/src/main/java/com/linku/design/theme/color/ThemeColorScheme.kt +++ b/design/src/main/java/com/linku/design/theme/color/ThemeColorScheme.kt @@ -63,6 +63,29 @@ sealed class ThemeColorScheme( //구글 로그인 버튼 컬러용 val googleLoginColor: Color = Color(0xFF1F1F1F), //구글 로그인 보더 컬러용 - val googleLoginBorderColor: Color = Color(0xFF747775) + val googleLoginBorderColor: Color = Color(0xFF747775), + //큐레이션 전용 그라데이션 + val curationGradient: Brush = Brush.horizontalGradient( + listOf(Color(0xFF1451D5), Color(0xFF000208)) + ), + + //큐레이션 캘린더 박스 컬러 + val curationCalendarBoxColor: Color = Color(0xFFEFF4FF), + + //큐레이션 카드 배경 (이미지 로딩 전 fallback) + val curationCardBackground: Color = Color(0xFFF2F2F2), + + //큐레이션 카드 이미지 위 오버레이 그라데이션 + val curationCardOverlayGradient: Brush = Brush.verticalGradient( + colorStops = arrayOf( + 0.0f to Color(0x00000000), + 1.0f to Color(0x66000000) + ) + ), + + //큐레이션 감정 분석 섹션 타이틀 그라데이션 + val emotionTitleGradient: Brush = Brush.horizontalGradient( + listOf(Color(0xFF2C6FFF), Color(0xFF000208)) + ), ) \ No newline at end of file diff --git a/design/src/main/java/com/linku/design/top/bar/TopBar.kt b/design/src/main/java/com/linku/design/top/bar/TopBar.kt index 07e2454c..98696418 100644 --- a/design/src/main/java/com/linku/design/top/bar/TopBar.kt +++ b/design/src/main/java/com/linku/design/top/bar/TopBar.kt @@ -4,7 +4,14 @@ package com.linku.design.top.bar import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +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.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -13,6 +20,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -20,13 +28,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp -import com.linku.design.theme.font.Paperlogy import com.linku.design.modifier.noRippleClickable import com.linku.design.theme.LocalColorTheme -import com.linku.design.R as Res +import com.linku.design.theme.font.Paperlogy import com.linku.design.theme.font.Taebaek import com.linku.design.util.scaler -import androidx.compose.ui.graphics.Color +import com.linku.design.R as Res /* 공통 TopBar 컴포넌트 @@ -40,11 +47,12 @@ import androidx.compose.ui.graphics.Color * @param onClickAlarm 알림 아이콘 클릭 콜백 */ -private const val TOPBAR_SIMPLE_HEIGHT = 77.4f // 로고 + 알림만 +private const val TOPBAR_SIMPLE_HEIGHT = 80f // 로고 + 알림만 private const val TOPBAR_SEARCH_HEIGHT = 139f // 검색바 포함 //기존 파일은 206f private val DEFAULT_BACKGROUND = Color.White // 기본 배경 흰색 +//아 이것도 리펙해야하나...? @Composable fun TopBar( modifier: Modifier = Modifier, diff --git a/feature/curation/src/main/java/com/linku/curation/CurationApp.kt b/feature/curation/src/main/java/com/linku/curation/CurationApp.kt index 1efb6683..0a60ff2f 100644 --- a/feature/curation/src/main/java/com/linku/curation/CurationApp.kt +++ b/feature/curation/src/main/java/com/linku/curation/CurationApp.kt @@ -1,49 +1,20 @@ package com.linku.linku_android.curation -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.runtime.remember import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.compose.navigation -import com.linku.curation.CurationDetailViewModel import com.linku.curation.CurationViewModel -import com.linku.curation.ui.screen.CurationDetailScreen import com.linku.curation.ui.CurationScreen -import com.linku.curation.ui.screen.detail.CurationMonthDetailScreen +import com.linku.curation.ui.screen.CurationCard1Screen +import com.linku.curation.ui.screen.CurationCard3Screen -/** - * 큐레이션 기능의 내비게이션 그래프 정의 - * 이건 아예 전면 변경이라 일단 MainAPP에서 분리만 시켰습니다. - */ - -/** - * 아래는 추후 큐레이션 두번째 카드 ui 완성되는대로 붙이겠습니다.(뒤로 가기, 홈으로 이동) - * composable("curation_keyword_detail") { backStackEntry -> - * showNavBar(false) - * - * val parentEntry = remember(backStackEntry) { - * navigator.getBackStackEntry("curation") - * } - * val curationVm: CurationViewModel = hiltViewModel(parentEntry) - * - * CurationKeywordDetailScreen( - * viewModel = curationVm, - * onBack = { navigator.popBackStack() }, // 이전 화면(curation_list)으로 - * onHome = { navigator.navigate("home") { // 홈으로 이동 - * popUpTo("curation") { inclusive = false } - * launchSingleTop = true - * }} - * ) - * } - * */ -@OptIn (ExperimentalSharedTransitionApi::class) fun NavGraphBuilder.curationGraph( navigator: NavHostController, - showNavBar: (Boolean) -> Unit + showNavBar: (Boolean) -> Unit, + nickname: String, ) { navigation( startDestination = "curation_list", @@ -57,80 +28,22 @@ fun NavGraphBuilder.curationGraph( } val curationVm: CurationViewModel = hiltViewModel(parentEntry) - CurationScreen(viewModel = curationVm) + CurationScreen( + nickname = nickname, + viewModel = curationVm, + onCard1Click = { navigator.navigate("curation_card1") }, + onCard3Click = { navigator.navigate("curation_card3") } + ) } - composable( - route = "curation_month_detail/{curationId}?imageUrl={imageUrl}&cardIndex={cardIndex}", -// enterTransition = { EnterTransition.None }, -// exitTransition = { ExitTransition.None } - ) { backStack -> + composable("curation_card1") { showNavBar(false) + CurationCard1Screen(onBack = { navigator.popBackStack() }) + } - val curationId = backStack.arguments?.getString("curationId")?.toLong() ?: 0L - val imageUrl = backStack.arguments?.getString("imageUrl") - val cardIndex = backStack.arguments?.getString("cardIndex")?.toInt() ?: 0 - - CurationMonthDetailScreen( - curationId = curationId, - imageUrl = imageUrl, - onBack = { navigator.popBackStack() } - ) + composable("curation_card3") { + showNavBar(false) + CurationCard3Screen(onBack = { navigator.popBackStack() }) } } } -//fun NavGraphBuilder.curationGraph( -// navigator: NavHostController, -// showNavBar: (Boolean) -> Unit -//) { -// navigation( -// startDestination = "curation_list", -// route = "curation" -// ) { -// // 1. 리스트 화면 -// composable("curation_list") { backStackEntry -> -// // 리스트 화면에서는 바텀바 표시 -// showNavBar(true) -// -// // 부모 그래프(curation_graph)의 스코프를 가져옴 -// val parentEntry = remember(backStackEntry) { -// navigator.getBackStackEntry("curation") -// } -// val curationVm: CurationViewModel = hiltViewModel(parentEntry) -// -// CurationScreen( -// viewModel = curationVm, -// onOpenDetail = { userId, curationId -> -// navigator.navigate("curation_detail/$userId/$curationId") { -// launchSingleTop = true -// } -// } -// ) -// } -// -// // 2. 상세 화면 -// composable("curation_detail/{userId}/{curationId}") { backStack -> -// // 상세 화면 진입 시 필요하다면 바텀바를 숨길 수도 있음 (현재는 유지 중) -// -// val userId = backStack.arguments?.getString("userId")?.toLong() ?: 0L -// val curationId = backStack.arguments?.getString("curationId")?.toLong() ?: 0L -// -// val parentEntry = remember(backStack) { -// navigator.getBackStackEntry("curation") -// } -// -// // 공유 ViewModel (부모 스코프) -// val sharedVm: CurationViewModel = hiltViewModel(parentEntry) -// // 상세 전용 ViewModel (현재 상세화면 스코프) -// val detailVm: CurationDetailViewModel = hiltViewModel(backStack) -// -// CurationDetailScreen( -// userId = userId, -// curationId = curationId, -// detailViewModel = detailVm, -// homeViewModel = sharedVm, -// onBack = { navigator.popBackStack() } -// ) -// } -// } -//} \ No newline at end of file diff --git a/feature/curation/src/main/java/com/linku/curation/CurationDetailViewModel.kt b/feature/curation/src/main/java/com/linku/curation/CurationDetailViewModel.kt index 9772d737..83a5dbf0 100644 --- a/feature/curation/src/main/java/com/linku/curation/CurationDetailViewModel.kt +++ b/feature/curation/src/main/java/com/linku/curation/CurationDetailViewModel.kt @@ -1,111 +1,13 @@ package com.linku.curation import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.linku.core.model.RecommendedLink import com.linku.core.repository.CurationRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch import javax.inject.Inject -data class CurationLinksUiState( - val loading: Boolean = false, - val items: List = emptyList(), - val error: String? = null -) - -data class CurationDetailUiState( - val loading: Boolean = false, - val topTags: List = emptyList(), - val headerMent: String? = null,//TODO : api 확인하기. - val footerMent: String? = null, - val liked: Boolean? = null, - val likeBusy: Boolean = false, - val error: String? = null, - val month: String? = null -) - -//스킵 보정 헬퍼(url 이동) -private fun ensureHttpScheme(raw: String): String = - if (raw.startsWith("http://") || raw.startsWith("https://")) raw else "https://$raw" - - - @HiltViewModel class CurationDetailViewModel @Inject constructor( private val repo: CurationRepository ) : ViewModel() { - // 내부 보관용 ID 상태 추가 - private val _userId = MutableStateFlow(-1L) - val userId: StateFlow = _userId - - private val _curationId = MutableStateFlow(-1L) - - private val _links = MutableStateFlow(CurationLinksUiState()) - val links: StateFlow = _links - - // 외부에서 userId/curationId를 주입하거나, 내부에서 rememberSaveable 등으로 유지 가능 - fun loadRecommendedLinks(userId: Long, curationId: Long) { - _links.value = _links.value.copy(loading = true, error = null) - viewModelScope.launch { - runCatching { repo.getRecommendedLinks(userId, curationId) } - .onSuccess { list -> - android.util.Log.d("CurationDetailVM", "추천링크 성공: user=$userId, curation=$curationId, count=${list.size}") - _links.value = CurationLinksUiState( - loading = false, - items = list, - error = null) - } - .onFailure { e -> - android.util.Log.e("CurationDetailVM", "추천링크 실패: user=$userId, curation=$curationId, error=${e.message}", e) - _links.value = CurationLinksUiState( - loading = false, - items = emptyList(), - error = e.message) - } - } - } - - /** 화면 진입 시 한 번 호출: userId/curationId 주입 + 좋아요 상태 로드 */ - fun attach(userId: Long, curationId: Long, loadDetail: Boolean = true, loadLinks: Boolean = false) { - _userId.value = userId - _curationId.value = curationId - if (loadDetail) loadCurationDetail(curationId) - if (loadLinks) loadRecommendedLinks(userId, curationId) - } - - private val _detail = MutableStateFlow(CurationDetailUiState()) - val detail: StateFlow = _detail - - fun loadCurationDetail(curationId: Long) { - _detail.value = _detail.value.copy(loading = true, error = null) - viewModelScope.launch { - runCatching { repo.getCurationDetail(curationId) } - .onSuccess { d -> - android.util.Log.d("CurationDetailVM", "디테일 성공: curation=$curationId, topTags=${d.topTags}, header=${d.headerMent}") - _detail.value = CurationDetailUiState( - loading = false, - month = d.month, - topTags = d.topTags, - headerMent = d.headerMent, - footerMent = d.footerMent - ) - } - .onFailure { e -> - android.util.Log.e("CurationDetailVM", "디테일 실패: curation=$curationId, error=${e.message}", e) - _detail.value = CurationDetailUiState(loading = false, error = e.message) - } - } - } - - - - - /** 편의: 디테일/링크/좋아요 한 번에 로드 */ - fun loadAll(userId: Long, curationId: Long) { - attach(userId, curationId, loadDetail = true, loadLinks = true) - } } diff --git a/feature/curation/src/main/java/com/linku/curation/CurationViewModel.kt b/feature/curation/src/main/java/com/linku/curation/CurationViewModel.kt index 01d08c2d..df73eba2 100644 --- a/feature/curation/src/main/java/com/linku/curation/CurationViewModel.kt +++ b/feature/curation/src/main/java/com/linku/curation/CurationViewModel.kt @@ -1,221 +1,14 @@ package com.linku.curation -import android.util.Log -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.linku.core.model.CurationItem -import com.linku.core.model.search.RecentQuery -import com.linku.core.repository.CurationRepository -import com.linku.core.repository.LinkuRepository -import com.linku.core.repository.RecentSearchRepository -import com.linku.core.repository.UserRepository -import com.linku.data.preference.AuthPreference -import com.linku.design.top.search.FastSearchItem import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import javax.inject.Inject -// 이거 싹 다 밀어 버릴 예정.. @HiltViewModel class CurationViewModel @Inject constructor( - private val repository: CurationRepository, - private val userRepository: UserRepository, - private val authPreference: AuthPreference, - private val recentRepository: RecentSearchRepository, - private val linkuRepository: LinkuRepository, -) : ViewModel() { - - private var hasPrefetched = false //한 번만 실행. - - private val _isGenerating = MutableStateFlow(false) - val isGenerating: StateFlow = _isGenerating - - private val _errorMessage = MutableStateFlow(null) - val errorMessage: StateFlow = _errorMessage - - val session = authPreference.sessionState - - // TODO: 추후 UserRepository 단일 진실 공급원(SSOT) 캐싱 구조로 프로필 리팩토링 예정 - /** - * UserRepository가 캐싱 흐름을 관리: - * 뷰모델들이 각각 API를 치는 게 아니라, UserRepository.userInfoState: StateFlow 같은 공용 스트림을 열어둡니다. - * - * 뷰모델들은 그 스트림을 구독만 하고, 최초 진입점(예: MainApp 부팅 시점)이나 새로고침이 필요할 때 딱 한 번만 userRepository.fetchUserInfo()를 호출해 칠판을 업데이트하는 구조가 가장 이상적입니다. - * - * */ - val nickname: StateFlow = MutableStateFlow("세나").asStateFlow() - val jobName: StateFlow = MutableStateFlow("직장인").asStateFlow() - - private val _recentCuration = MutableStateFlow(null) - val recentCuration: StateFlow = _recentCuration - - - private val _currentCurationId = MutableStateFlow(-1L) - val currentCurationId: StateFlow = _currentCurationId - - - // 로딩/중복탭 방지 플래그(선택) - private val _likeBusy = MutableStateFlow(false) - val likeBusy: StateFlow = _likeBusy - - // --- 공통 uid 가드 - // 아예 싹 수정될 코드로 임시로 에러 방지 위해서만 수정했습니다.... - private suspend fun requireUserId(): Long { - val uid = authPreference.getUserId() ?: -1L - return uid - } - - // retrofit2에 의존하지 않고, 예외에 code() 메서드가 있으면 꺼내오는 유틸 - private fun httpStatusCodeOrMinus1(throwable: Throwable): Int { - return runCatching { - val m = throwable::class.java.methods - .firstOrNull { it.name == "code" && it.parameterCount == 0 } - (m?.invoke(throwable) as? Int) ?: -1 - }.getOrDefault(-1) - } - - - - fun setCurrentCurationId(id: Long) { _currentCurationId.value = id } - - // ---------- search method ---------- - // 검색창 탑 시트 가시성 상태 - var searchTopSheetVisible by mutableStateOf(false) - private set - fun updateSearchTopSheetVisible(newState: Boolean) { - Log.d("searchTopSheetVisible", newState.toString()) - searchTopSheetVisible = newState - } - - // 빠른 링크 검색 목록 - private var _fastSearchItems = MutableStateFlow>(emptyList()) - val fastSearchItems: StateFlow> = _fastSearchItems.asStateFlow() - - // 빠른 링크 검색 - fun fastSearch(keyword: String){ - Log.d("CurationViewModel", "fastSearch") - - viewModelScope.launch{ - Log.d("CurationViewModel", "fastSearch launch") - - _errorMessage.value = null - try{ - Log.d("CurationViewModel", "fastSearch try") - _fastSearchItems.value = linkuRepository.fastSearch(keyword).map{ - FastSearchItem( - id = it.linkuId, - title = it.title, - url = it.linkUrl - ) - } - - Log.d("CurationViewModel", "fastSearch try result: ${_fastSearchItems.value}") - }catch (e: Exception){ - Log.d("CurationViewModel", "fastSearch catch: $e.message") - - _errorMessage.value = e.message - }finally { - Log.d("CurationViewModel", "fastSearch finally") - } - } - } - - //최근 검색 목록 - val recentQueryList: StateFlow> = - recentRepository.observe(limit = 20) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList() - ) - - // 최근 검색 기록 추가 - fun addRecentQuery(query: String) { - Log.d("CurationViewModel", "addRecentQuery") - - viewModelScope.launch { - Log.d("CurationViewModel", "addRecentQuery launch") - - try{ - Log.d("CurationViewModel", "addRecentQuery try") - - recentRepository.add(query) - }catch (e: Exception){ - Log.d("CurationViewModel", "addRecentQuery catch: $e.message") - }finally { - Log.d("CurationViewModel", "addRecentQuery finally") - } - } - Log.d("CurationViewModel", "addRecentQuery return") - } - - // 최근 검색 기록 삭제 - fun removeRecentQuery(query: String) { - Log.d("CurationViewModel", "removeRecentQuery") - - viewModelScope.launch { - Log.d("CurationViewModel", "removeRecentQuery launch") - - try{ - Log.d("CurationViewModel", "removeRecentQuery try") - - recentRepository.remove(query) - - }catch (e: Exception){ - Log.d("CurationViewModel", "removeRecentQuery catch: $e.message") - }finally { - Log.d("CurationViewModel", "removeRecentQuery finally") - } - } - Log.d("CurationViewModel", "removeRecentQuery return") - } - - //403 빈 상태 오류!(미로그인) - private fun setEmptyCurationState(markPrefetched: Boolean = true) { - _recentCuration.value = null - _currentCurationId.value = -1L - _errorMessage.value = null // ❗️사용자에겐 조용히 - _isGenerating.value = false - if (markPrefetched) hasPrefetched = true // 리트라이 루프 방 - } - - /** 외부에서 로그인/로그아웃 변화 시 호출 */ - fun invalidate() { - hasPrefetched = false - } - - - // 최근 검색 기록 전체 삭제 - fun clearRecentQuery() { - Log.d("CurationViewModel", "clearRecentQuery") - - viewModelScope.launch { - Log.d("CurationViewModel", "clearRecentQuery launch") - - try{ - Log.d("CurationViewModel", "clearRecentQuery try") - - recentRepository.clear() - - }catch (e: Exception){ - Log.d("CurationViewModel", "clearRecentQuery catch: $e.message") - }finally { - Log.d("CurationViewModel", "clearRecentQuery finally") - } - } - Log.d("CurationViewModel", "clearRecentQuery return") - } - // ---------- search method ---------- +) : ViewModel() { } diff --git a/feature/curation/src/main/java/com/linku/curation/ui/CurationScreen.kt b/feature/curation/src/main/java/com/linku/curation/ui/CurationScreen.kt index cbe11bff..95a13084 100644 --- a/feature/curation/src/main/java/com/linku/curation/ui/CurationScreen.kt +++ b/feature/curation/src/main/java/com/linku/curation/ui/CurationScreen.kt @@ -1,182 +1,93 @@ package com.linku.curation.ui -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.SharedTransitionLayout -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Scaffold +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.linku.curation.CurationViewModel -import com.linku.design.top.bar.TopBar -import com.linku.design.util.scaler -import androidx.compose.foundation.Image -import androidx.compose.foundation.pager.PagerState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.graphics.Color -import com.linku.curation.R -import com.linku.curation.ui.effect.highlight.RadialGradientCircle -import com.linku.curation.ui.main_card.CurationMainCardPager -import com.linku.curation.ui.screen.detail.CurationMonthDetailOverlay -import com.linku.design.theme.font.Paperlogy -import androidx.compose.foundation.pager.rememberPagerState import com.linku.curation.ui.calendar.CalendarBox +import com.linku.curation.ui.header.CurationHeader +import com.linku.curation.ui.main_card.CurationMainCardPager import com.linku.curation.ui.screen.CurationKeywordDetailScreen +import com.linku.curation.ui.util.CurationBackground +import com.linku.design.theme.LinkuPreview +import com.linku.design.theme.linkuColors +import com.linku.design.top.bar.TopBar +import com.linku.design.util.scaler - -@OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CurationScreen( - viewModel: CurationViewModel = hiltViewModel(), - //onOpenDetail: (userId: Long, curationId: Long, imageUrl: String?, cardIndex: Int) -> Unit = { _, _, _, _ -> } // 파라미터 4개로 변경 + nickname: String, + viewModel: CurationViewModel = hiltViewModel(), //TODO princehw가 구현해줄거임! + onCard1Click: () -> Unit = {}, // TODO 다음 PR에서 구현.. + onCard3Click: () -> Unit = {}, // TODO 다음 PR에서 구현.. ) { - val nickname by viewModel.nickname.collectAsState() - - // Pager 상태를 Screen에서 고정 - val pagerState = rememberPagerState( - initialPage = 0, - pageCount = { 3 } - ) - - // 디테일 오버레이 상태 - var showDetail by remember { mutableStateOf(false) } + val pagerState = rememberPagerState(initialPage = 0, pageCount = { 3 }) var showKeywordDetail by remember { mutableStateOf(false) } - var selectedImageUrl by remember { mutableStateOf(null) } - var selectedPage by remember { mutableStateOf(0) } - + val displayNickname = nickname.ifBlank { "세나" } - SharedTransitionLayout { - Box(modifier = Modifier.fillMaxSize()) { - - // 전체 화면 기준 배경 - // 핑크 원 - RadialGradientCircle( - color = Color(0xFFC800FF), - modifier = Modifier.offset( - x = (-88).scaler, - y = (-12).scaler - ) - ) - //블루 원 - RadialGradientCircle( - color = Color(0xFF2C6FFF), //color = Color(0xFFC800FF), - modifier = Modifier.offset( - x = 24.scaler, - y = (-6).scaler - ) - ) - // 로고 - Image( - painter = painterResource(id = R.drawable.img_curation_logo), - contentDescription = "logo", - modifier = Modifier - .offset( - x = 224.scaler, - y = 95.scaler - ) - .size( - width = 197.scaler, - height = 140.scaler - ) - ) + Box(modifier = Modifier.fillMaxSize()) { + CurationBackground(showLogo = true) - // 실제 화면 - Scaffold( - topBar = { - TopBar(showSearchBar = false, backgroundColor = null) - }, - containerColor = Color.Transparent - ) { innerPadding -> + Column(modifier = Modifier.fillMaxSize()) { + TopBar(showSearchBar = false, backgroundColor = null) - CurationScreenContent( - nickname = nickname.ifBlank { "세나" }, - pagerState = pagerState, - isDetailOpen = showDetail, - // TODO: 실제 curationId - onCardClick = { index, imageUrl -> - selectedPage = index - selectedImageUrl = imageUrl - when (index) { - 1 -> showKeywordDetail = true // 2번 카드 → 키워드 화면 - else -> showDetail = true // 1, 3번 카드 → 기존 오버레이? - } - }, - modifier = Modifier.padding(top = innerPadding.calculateTopPadding()) - ) - } + Spacer(modifier = Modifier.height(28.scaler)) - // 디테일 오버레이 (CurationScreen 위에 띄움) - if (showDetail) { - CurationMonthDetailOverlay( - page = selectedPage, - imageUrl = selectedImageUrl, - onBack = { showDetail = false } - ) - } + CurationScreenContent( + nickname = displayNickname, + pagerState = pagerState, + onCardClick = { index, _ -> + when (index) { + 0 -> onCard1Click() + 1 -> showKeywordDetail = true + 2 -> onCard3Click() + } + } + ) + } - // 2번 카드 전용 추가 : 키워드 화면 - if (showKeywordDetail) { - CurationKeywordDetailScreen( - onBack = { showKeywordDetail = false }, - onHome = { showKeywordDetail = false } - ) - } + if (showKeywordDetail) { + CurationKeywordDetailScreen( + nickname = displayNickname, + onBack = { showKeywordDetail = false }, + onHome = { showKeywordDetail = false } + ) } } } -// CurationScreen.kt + @Composable private fun CurationScreenContent( nickname: String, pagerState: PagerState, - isDetailOpen: Boolean, onCardClick: (index: Int, imageUrl: String?) -> Unit, modifier: Modifier = Modifier ) { - Column( - modifier = modifier.fillMaxSize() - ) { - // 타이틀 이미지 - Image( - painter = painterResource(id = R.drawable.img_curation_title), - contentDescription = null, - modifier = Modifier - .padding(start = 24.scaler, top = 28.scaler) - .size(width = 102.scaler, height = 15.scaler) - ) - - Spacer(modifier = Modifier.height(12.scaler)) - - // 닉네임 텍스트 - Text( - text = "${nickname}님을 위한 링큐레이션", - style = TextStyle( - fontSize = 22.sp, - lineHeight = 30.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight(700), - color = Color(0xFF1451D5) - ), - modifier = Modifier.padding(horizontal = 24.scaler) - ) + Column(modifier = modifier.fillMaxSize()) { + CurationHeader(nickname = nickname) Spacer(modifier = Modifier.height(26.scaler)) - // 카드 페이저 (고정된 위치에서 시작) CurationMainCardPager( - imageUrls = listOf(null, null, null), + imageUrls = List(3) { "" }, //아 pm이 나중에 3개 이상으로 확장될 수도 있대요. 그래서 이렇게 했어요 pagerState = pagerState, - isDetailOpen = isDetailOpen, onCardClick = onCardClick ) @@ -184,754 +95,23 @@ private fun CurationScreenContent( Text( text = "지난 큐레이션", - style = TextStyle( - fontSize = 22.sp, - lineHeight = 30.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight(700), - color = Color(0xFF000208) - ), + fontSize = 22.sp, + lineHeight = 30.sp, + fontWeight = FontWeight(700), + color = MaterialTheme.linkuColors.black, modifier = Modifier.padding(horizontal = 24.scaler) ) Spacer(modifier = Modifier.height(18.scaler)) - CalendarBox( - modifier = Modifier.padding(horizontal = 18.scaler) - ) - + CalendarBox(modifier = Modifier.padding(horizontal = 18.scaler)) } } -@OptIn(ExperimentalSharedTransitionApi::class) -@Preview( - showBackground = true, - backgroundColor = 0xFFFFFFFF, // 어두운 배경 - widthDp = 390, - heightDp = 844 -) +@Preview(showBackground = true) @Composable fun PreviewCurationScreen() { - val pagerState = rememberPagerState( - initialPage = 0, - pageCount = { 3 } - ) - - SharedTransitionLayout { - Box(modifier = Modifier.fillMaxSize()) { - - // 핑크 원 - RadialGradientCircle( - color = Color(0xFFC800FF), - modifier = Modifier.offset( - x = (-88).scaler, - y = (-12).scaler - ) - ) - - // 블루 원 - RadialGradientCircle( - color = Color(0xFF2C6FFF), - modifier = Modifier.offset( - x = 24.scaler, - y = (-6).scaler - ) - ) - - // 로고 - Image( - painter = painterResource(id = R.drawable.img_curation_logo), - contentDescription = "logo", - modifier = Modifier - .offset( - x = 224.scaler, - y = 95.scaler - ) - .size( - width = 197.scaler, - height = 140.scaler - ) - ) - - Scaffold( - topBar = { - TopBar(showSearchBar = false, backgroundColor = null) - }, - containerColor = Color.Transparent - ) { innerPadding -> - CurationScreenContent( - nickname = "세나", - pagerState = pagerState, - isDetailOpen = false, - onCardClick = { _, _ -> }, - modifier = Modifier.padding(top = innerPadding.calculateTopPadding()) - ) - } - } + LinkuPreview { + CurationScreen(nickname = "세나") } } -/* -@Composable -private fun CurationScreenContent( - nickname: String, - pagerState: PagerState, - onCardClick: (index: Int, imageUrl: String?) -> Unit, // 추가 - modifier: Modifier = Modifier -) { - LazyColumn( - modifier = modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = 32.dp) - ) { - item { - Column { - - Image( - painter = painterResource(id = R.drawable.img_curation_title), - contentDescription = null, - modifier = Modifier - .padding( - start = 24.scaler, - top = 28.scaler - ) - .size( - width = 102.scaler, - height = 15.scaler - ) - ) - - Spacer(modifier = Modifier.height(12.scaler)) - - Text( - text = "${nickname}님을 위한 링큐레이션", - style = TextStyle( - fontSize = 22.sp, - lineHeight = 30.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight(700), - color = Color(0xFF1451D5) - ), - modifier = Modifier.padding(horizontal = 20.scaler) - ) - Spacer(modifier = Modifier.height(26.scaler)) - - CurationMainCardPager( - imageUrls = listOf(null, null, null), - pagerState = pagerState, - onCardClick = onCardClick // 전달 - ) - - - } - } - } -}*/ - - -/*@Preview( - showBackground = true, - backgroundColor = 0xFFFFFFFF -) -@Composable -fun PreviewCurationScreenFull() { - Box(modifier = Modifier.fillMaxSize()) { - - // 배경 원 - RadialGradientCircle( - color = Color(0xFFC800FF), - modifier = Modifier.offset( - x = (-88).scaler, - y = (0).scaler - ) - ) - - RadialGradientCircle( - color = Color(0xFF2C6FFF), - modifier = Modifier.offset( - x = 24.scaler, - y = (-12).scaler - ) - ) - - // 로고 - Image( - painter = painterResource(id = R.drawable.img_curation_logo), - contentDescription = null, - modifier = Modifier - .offset( - x = 224.scaler, - y = 95.scaler - ) - .size( - width = 197.scaler, - height = 140.scaler - ) - ) - - // 콘텐츠 - Scaffold( - topBar = { - TopBar(showSearchBar = false,backgroundColor = null) - }, - containerColor = Color.Transparent - ) { innerPadding -> - - CurationScreenContent( - nickname = "세나", - onCardClick = { _, _ -> }, - - modifier = Modifier.padding( - top = innerPadding.calculateTopPadding() - ) - ) - } - } -} -*/ - -//package com.linku.curation.ui -// -// -//import androidx.compose.foundation.Image -//import androidx.compose.foundation.background -//import androidx.compose.foundation.layout.Column -//import androidx.compose.foundation.layout.Row -//import androidx.compose.foundation.layout.Spacer -//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.shape.RoundedCornerShape -//import androidx.compose.material3.MaterialTheme -//import androidx.compose.material3.Text -//import androidx.compose.material3.Icon -//import androidx.compose.runtime.Composable -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.graphics.Color -//import androidx.compose.ui.res.painterResource -//import androidx.compose.ui.tooling.preview.Preview -//import androidx.compose.ui.unit.dp -//import com.linku.curation.ui.recommend_list.CurationRecommendedLinksSection -//import com.linku.curation.R -//import androidx.compose.foundation.lazy.LazyColumn -//import androidx.compose.foundation.lazy.items -//import androidx.compose.foundation.layout.PaddingValues -//import androidx.compose.foundation.clickable -//import androidx.compose.foundation.layout.Arrangement -//import androidx.compose.foundation.layout.Box -//import androidx.compose.foundation.layout.BoxWithConstraints -//import androidx.compose.foundation.layout.width -//import androidx.compose.material3.Scaffold -//import androidx.compose.runtime.LaunchedEffect -//import androidx.compose.runtime.collectAsState -//import androidx.compose.runtime.getValue -//import androidx.compose.runtime.remember -//import androidx.compose.ui.draw.clip -//import androidx.compose.ui.layout.ContentScale -//import androidx.compose.ui.text.font.FontWeight -//import androidx.compose.ui.unit.sp -//import androidx.hilt.navigation.compose.hiltViewModel -//import com.linku.curation.CurationViewModel -//import com.linku.curation.ui.list_card.LikedCurationCard -//import com.linku.curation.Paperlogy -//import com.linku.design.theme.LocalColorTheme -//import com.linku.design.theme.color.Basic -//import com.linku.design.R as Res -//import java.time.LocalDate -//import java.time.format.DateTimeFormatter -//import java.util.Locale -//import androidx.compose.ui.platform.LocalUriHandler -//import androidx.compose.ui.text.style.TextAlign -//import androidx.compose.runtime.mutableStateOf -//import androidx.compose.runtime.setValue -//import com.linku.curation.ui.main_card.CurationHighlightSection -//import com.linku.curation.ui.main_card.HighlightCurationCard -//import com.linku.design.top.search.SearchBarTopSheet -//import com.linku.core.model.RecommendedLink -//import com.linku.curation.ui.list_card.LikedCurationSkeleton -//import com.linku.curation.ui.recommend_list.RecommendedLinkCardSkeleton -//import com.linku.design.top.bar.TopBar -// -//// 간단 확장함수 -//private fun String.toLabel(): String = runCatching { -// val y = substring(0, 4).toInt() -// val m = substring(5, 7).toInt() -// "${y}년 ${m}월호" -//}.getOrElse { this } -// -//fun ensureHttpScheme(raw: String): String = -// if (raw.startsWith("http://") || raw.startsWith("https://")) raw -// else "https://$raw" -// -//@Composable -//fun CurationScreen( -// viewModel: CurationViewModel = hiltViewModel(), -// onOpenDetail: (Long, Long) -> Unit = { _, _ -> } -//) { -// // 추가: TopSheet 표시 여부 -// var showSearch by remember { mutableStateOf(false) } -// val uri = LocalUriHandler.current -// val nickname by viewModel.nickname.collectAsState() -// -// val userId by viewModel.userId.collectAsState(initial = -1L) -// val currentCurationId by viewModel.currentCurationId.collectAsState(initial = -1L) -// -// val canOpenDetail = userId > 0 && currentCurationId > 0 -// -// LaunchedEffect(canOpenDetail) { -// if (canOpenDetail) { -// viewModel.loadHomeRecommendedLinksTop2(userId, currentCurationId) -// } -// } -// -// val homeLinksState by viewModel.homeLinks.collectAsState() -// -// -// //좋아요 리스트 상태 수집 -// val likedItems by viewModel.likedCurations.collectAsState() -// val likedLoading by viewModel.likedLoading.collectAsState() -// -// // 현재 월을 "8월" 같은 형식으로 가져옴 -// val currentMonth = remember { -// LocalDate.now().format(DateTimeFormatter.ofPattern("M월", Locale.KOREAN)) -// } -// -// // 닉네임 불러오기 -// LaunchedEffect(Unit) { -// viewModel.loadNickname() -// } -// -// Scaffold( -// topBar = { -// TopBar( -// onClickSearch = { viewModel.updateSearchTopSheetVisible(true) } -// ) -// }, -// containerColor = LocalColorTheme.current.white -// ) { innerPadding -> -// -// LazyColumn( -// modifier = Modifier -// .fillMaxSize(), -// contentPadding = PaddingValues( -// top = innerPadding.calculateTopPadding(),//탑바 고정. -// bottom = 32.dp -// ) -// ) { -// -// // 현재 날짜에서 전달 구하기 -// val prevMonthLabel = LocalDate.now() -// .minusMonths(1) -// .format(DateTimeFormatter.ofPattern("M월", Locale.KOREAN)) -// -// item { -// // 제목 영역 (24dp) -// Text( -// text = "${nickname}님을 위한 ${prevMonthLabel}의 큐레이션", -// style = MaterialTheme.typography.titleMedium.copy( -// fontFamily = Paperlogy, -// fontWeight = FontWeight.Bold, -// fontSize = 20.sp -// ), -// color = LocalColorTheme.current.black, -// modifier = Modifier -// .padding(start = 24.dp, end = 24.dp, top = 19.dp) -// ) -// -// Spacer(modifier = Modifier.height(20.dp)) -// -// // 하이라이트 카드 (좌우 20dp) -// CurationHighlightSection( -// modifier = Modifier -// .fillMaxWidth() -// .padding(horizontal = 20.dp), -// viewModel = viewModel, -// onOpenDetail = { onOpenDetail(userId, currentCurationId) } -// ) -// -// Spacer(modifier = Modifier.height(25.dp)) -// } -// -// -// -// // 2. 추천 링크 -// // 내부에서 padding 제거하고 modifier로 전달 -// item { -// -// Text( -// text = "추천 링크", -// style = MaterialTheme.typography.titleMedium.copy( -// fontFamily = Paperlogy, -// fontWeight = FontWeight.Bold, -// fontSize = 20.sp -// ), -// color = LocalColorTheme.current.black, -// modifier = Modifier.padding(start = 24.dp) -// ) -// -// Spacer(modifier = Modifier.height(20.dp)) -// when { -// //로딩 시 스켈레톤 + 쉬머 -// homeLinksState.loading && homeLinksState.items.isEmpty() -> { -// Column(modifier = Modifier.padding(horizontal = 16.dp)) { -// repeat(2) { -// RecommendedLinkCardSkeleton() -// Spacer(Modifier.height(10.dp)) -// } -// } -// } -// else -> { -// CurationRecommendedLinksSection( -// modifier = Modifier.fillMaxWidth(), -// links = homeLinksState.items, -// loading = homeLinksState.loading, -// onRetry = { -// if (canOpenDetail) { -// viewModel.loadHomeRecommendedLinksTop2( -// userId, -// currentCurationId -// ) -// } -// }, -// onClick = { url -> runCatching { uri.openUri(url) } } -// ) -// } -// } -// -// Spacer(modifier = Modifier.height(2.dp)) //이거 간격이 큰데 -// -// // 3. 좋아요한 큐레이션 제목 -// Column( -// modifier = Modifier.padding( -// start = 24.dp, -// end = 24.dp, -// top = 24.dp, -// bottom = 0.dp -// ) -// ) { -// Text( -// text = "${nickname}님이 좋아요한 큐레이션", -// style = MaterialTheme.typography.titleMedium.copy( -// fontFamily = Paperlogy, -// fontWeight = FontWeight.Bold, -// fontSize = 20.sp -// ), -// color = LocalColorTheme.current.black -// ) -// } -// -// Spacer(modifier = Modifier.height(18.dp)) -// } -// -// -// -// -// item { -// // 어댑터로 변환 (title은 고정문구/월 라벨 포맷) -// val uiItems = likedItems.map { -// UICurationItem( -// title = "링큐 큐레이션", -// date = it.month.toLabel(), // "2025-07" -> "2025년 7월호" -// imageUrl = it.thumbnailUrl, -// liked = true -// ) -// } -// -// Column(modifier = Modifier.padding(horizontal = 16.dp)) { -// -// -// when { -// likedLoading && uiItems.isEmpty() -> { -// Spacer(Modifier.height(10.dp)) -// repeat(3) { -// LikedCurationSkeleton() //스켈레톤 + 쉬머 적용함. -// Spacer(Modifier.height(10.dp)) -// } -// } -// -// uiItems.isEmpty() -> { -// LikedCurationEmptyState( -// modifier = Modifier -// .fillMaxWidth() -// .padding(vertical = 16.dp) -// ) -// } -// -// else -> { -// uiItems.forEachIndexed { idx, item -> -// val domain = likedItems.getOrNull(idx) -// -// LikedCurationCard( -// item = item, -// onCardClick = { -// // 카드 탭 → 상세 -// if (userId > 0 && domain != null) { -// onOpenDetail(userId, domain.id) -// } -// }, -// onHeartClick = { -// likedItems.getOrNull(idx)?.let { domain -> -// viewModel.unlikeFromLikedList(domain.id) // 서버 취소 + 낙관적 제거 -// } -// } -// ) -// Spacer(Modifier.height(10.dp)) -// } -// } -// } -// } -// } -// } -// } -// -// -// // 검색창 탑 시트 -// SearchBarTopSheet( -// visible = viewModel.searchTopSheetVisible, -// onLinkClick = {}, -// onDismiss = { viewModel.updateSearchTopSheetVisible(false) }, -// onQueryChange = { viewModel.fastSearch(it) }, -// onQuerySave = { viewModel.addRecentQuery(it) }, -// onQueryDelete = { viewModel.removeRecentQuery(it) }, -// onQueryClear = { viewModel.clearRecentQuery() }, -// fastSearchItems = viewModel.fastSearchItems.collectAsState().value, -// recentQueries = viewModel.recentQueryList.collectAsState().value.map{it.text} -// ) -// } -// -// -// -////좋아요한 큐레이션이 없는 경우 보여지는 화면. -//@Composable -//fun LikedCurationEmptyState( -// modifier: Modifier = Modifier, -// emptyIconRes: Int = R.drawable.img_curation_liked_null // 점선+링크가 합쳐진 PNG -//) { -// Column( -// modifier = modifier -// .fillMaxWidth() -// .padding(top = 8.dp), // 필요시 조절 -// horizontalAlignment = Alignment.CenterHorizontally -// ) { -// // 아이콘(점선 박스 포함) 크기 고정 -// Image( -// painter = painterResource(id = emptyIconRes), -// contentDescription = null, -// modifier = Modifier.size(82.dp), -// contentScale = ContentScale.Fit -// ) -// -// Spacer(Modifier.height(12.dp)) -// -// Text( -// text = "아직 좋아요 한 큐레이션이 아직 없어요!", -// style = MaterialTheme.typography.titleMedium.copy( -// fontFamily = Paperlogy, -// fontWeight = FontWeight.Medium, -// fontSize = 15.sp, -// color = LocalColorTheme.current.black -// ), -// textAlign = TextAlign.Center -// ) -// -// Spacer(Modifier.height(4.dp)) -// -// Text( -// text = "좋아요를 눌러보러 갈까요?", -// style = MaterialTheme.typography.bodyMedium.copy( -// fontFamily = Paperlogy, -// fontWeight = FontWeight.Medium, -// fontSize = 13.sp, -// color = Color(0xFF87898F) -// ), -// textAlign = TextAlign.Center -// ) -// } -//} -// -//@Composable -//private fun LinksPreparingHome(modifier: Modifier = Modifier) { -// Column( -// modifier = modifier -// .height(60.dp), // 필요시 조절 -// verticalArrangement = Arrangement.Center, -// horizontalAlignment = Alignment.CenterHorizontally -// ) { -// -// -// Spacer(Modifier.height(12.dp)) -// -// // 설명 -// Text( -// "AI가 한달 감정&상황 데이터를 바탕으로\n추천 링크를 준비하고 있어요", -// style = MaterialTheme.typography.bodyMedium.copy( -// fontFamily = Paperlogy, -// fontWeight = FontWeight.Medium, -// fontSize = 13.sp, -// color = Color(0xFF87898F) -// ), -// textAlign = androidx.compose.ui.text.style.TextAlign.Center -// ) -// -// Spacer(Modifier.height(14.dp)) -// -// // 안내 문구 -// Text( -// "* 검증 과정으로 인해 추천 링크 수가 제한될 수 있습니다.", -// style = MaterialTheme.typography.bodyMedium.copy( -// fontFamily = Paperlogy, -// fontWeight = FontWeight.Medium, -// fontSize = 13.sp, -// color = Color(0xFFFF5E5E) -// ), -// textAlign = androidx.compose.ui.text.style.TextAlign.Center -// ) -// } -//} -// -//@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF, widthDp = 390, heightDp = 2000) -//@Composable -//fun PreviewCurationScreenExact() { -// -// val previewLinks = listOf( -// RecommendedLink( -// isInternal = true, -// userLinkuId = 1L, -// title = "성신여대 홈페이지", -// url = "https://sungshin.ac.kr", -// imageUrl = null, -// domain = "sungshin.ac.kr", -// domainImageUrl = null, -// categories = listOf("기타") -// ), -// RecommendedLink( -// isInternal = false, -// userLinkuId = null, -// title = "나만의 자기관리 체크리스트 만들기 - Adobe", -// url = "https://adobe.com", -// imageUrl = null, -// domain = "adobe.com", -// domainImageUrl = null, -// categories = listOf("정보") -// ) -// ) -// -// val previewLiked = listOf( -// UICurationItem( -// title = "링큐 큐레이션", -// date = "2025년 7월호", -// imageUrl = null, -// liked = true -// ), -// UICurationItem( -// title = "링큐 큐레이션", -// date = "2025년 6월호", -// imageUrl = null, -// liked = true -// ) -// ) -// -// val uri = LocalUriHandler.current -// -// BoxWithConstraints( -// modifier = Modifier.fillMaxSize() -// ) { -// val width = maxWidth -// -// LazyColumn( -// modifier = Modifier -// .fillMaxSize() -// .background(LocalColorTheme.current.white), -// contentPadding = PaddingValues(bottom = 32.dp) -// ) { -// item { -// TopBar( -// showSearchBar = true, -// //onClickSearch = {}, // Preview라서 빈 람다 -// onClickAlarm = {} -// ) } -// -// item { Spacer(Modifier.height(19.dp)) } -// -// item { -// Text( -// text = "세나님을 위한 11월의 큐레이션", -// fontFamily = Paperlogy, -// fontSize = 20.sp, -// fontWeight = FontWeight.Bold, -// color = LocalColorTheme.current.black, -// modifier = Modifier.padding(horizontal = 24.dp) -// ) -// } -// -// item { Spacer(Modifier.height(20.dp)) } -// -// item { -// HighlightCurationCard( -// imageUrl = null, -// title = "링큐 큐레이션", -// date = "2025년 11월호", -// liked = true, -// likeBusy = false, -// modifier = Modifier -// .width(width) -// .padding(horizontal = 20.dp) -// ) -// } -// -// item { Spacer(Modifier.height(25.dp)) } -// -// item { -// -// Text( -// text = "추천 링크", -// style = MaterialTheme.typography.titleMedium.copy( -// fontFamily = Paperlogy, -// fontWeight = FontWeight.Bold, -// fontSize = 20.sp -// ), -// color = LocalColorTheme.current.black, -// modifier = Modifier.padding(start = 24.dp) -// ) -// -// Spacer(modifier = Modifier.height(20.dp)) -// -// -// CurationRecommendedLinksSection( -// modifier = Modifier.fillMaxWidth(), -// links = previewLinks, -// loading = false, -// onRetry = {}, -// onClick = { url -> runCatching { uri.openUri(url) } } -// ) -// } -// -// item { Spacer(Modifier.height(25.dp)) } -// -// item { -// Text( -// text = "세나님이 좋아요한 큐레이션", -// fontFamily = Paperlogy, -// fontSize = 20.sp, -// fontWeight = FontWeight.Bold, -// color = LocalColorTheme.current.black, -// modifier = Modifier.padding(start = 24.dp, end = 24.dp) -// ) -// } -// -// item { Spacer(Modifier.height(18.dp)) } -// -// items(previewLiked) { item -> -// Column(modifier = Modifier.padding(horizontal = 16.dp)) { -// LikedCurationCard( -// item = item, -// onCardClick = {}, -// onHeartClick = {} -// ) -// Spacer(Modifier.height(10.dp)) -// } -// } -// } -// } -//} \ No newline at end of file diff --git a/feature/curation/src/main/java/com/linku/curation/ui/calendar/CalendarBox.kt b/feature/curation/src/main/java/com/linku/curation/ui/calendar/CalendarBox.kt index 462b7309..8f4ba0e0 100644 --- a/feature/curation/src/main/java/com/linku/curation/ui/calendar/CalendarBox.kt +++ b/feature/curation/src/main/java/com/linku/curation/ui/calendar/CalendarBox.kt @@ -1,31 +1,39 @@ package com.linku.curation.ui.calendar import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme 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.Color -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.linku.design.theme.font.Paperlogy +import com.linku.design.theme.LinkuPreview +import com.linku.design.theme.linkuColors import com.linku.design.util.scaler @Composable fun CalendarBox( modifier: Modifier = Modifier ) { + val colorTheme = MaterialTheme.linkuColors + Row( modifier = modifier .width(372.scaler) .height(80.scaler) .background( - color = Color(0xFFEFF4FF), + color = colorTheme.curationCalendarBoxColor, shape = RoundedCornerShape(size = 22.scaler) ) .padding(horizontal = 20.scaler), @@ -45,13 +53,10 @@ fun CalendarBox( ) { Text( text = "지난 월간 큐레이션을 다시 볼 수 있어요", - style = TextStyle( - fontSize = 14.sp, - lineHeight = 20.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight(400), - color = Color(0xFF43454B) - ), + fontSize = 14.sp, + lineHeight = 20.sp, + fontWeight = FontWeight(400), + color = colorTheme.gray[800], modifier = Modifier.padding(top = 2.scaler) ) @@ -59,29 +64,20 @@ fun CalendarBox( Text( text = "월간 큐레이션 모아보기", - style = TextStyle( - fontSize = 18.sp, - lineHeight = 20.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight(700), - color = Color(0xFF000208) - ) + fontSize = 18.sp, + lineHeight = 20.sp, + fontWeight = FontWeight(700), + color = colorTheme.black ) } } } - -@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Preview @Composable fun PreviewCalendarBox() { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - contentAlignment = Alignment.Center - ) { + LinkuPreview { CalendarBox() } } \ No newline at end of file diff --git a/feature/curation/src/main/java/com/linku/curation/ui/calendar/CalendarIconBox.kt b/feature/curation/src/main/java/com/linku/curation/ui/calendar/CalendarIconBox.kt index 7c9414ea..a7b8bc0e 100644 --- a/feature/curation/src/main/java/com/linku/curation/ui/calendar/CalendarIconBox.kt +++ b/feature/curation/src/main/java/com/linku/curation/ui/calendar/CalendarIconBox.kt @@ -1,33 +1,30 @@ package com.linku.curation.ui.calendar -import android.os.Build -import androidx.annotation.RequiresApi import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp +import com.linku.curation.R +import com.linku.design.theme.LinkuPreview import com.linku.design.theme.font.Laundrygothic +import com.linku.design.theme.linkuColors import com.linku.design.util.scaler -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.runtime.remember import java.time.LocalDate -import com.linku.curation.R + // 월 약어 리스트 private val MONTH_LABELS = listOf( "JAN", "FEB", "MAR", "APR", "MAY", "JUN", @@ -35,11 +32,12 @@ private val MONTH_LABELS = listOf( ) -@RequiresApi(Build.VERSION_CODES.O) @Composable fun CalendarIconBox( modifier: Modifier = Modifier ) { + val colorTheme = MaterialTheme.linkuColors + // 전달 계산 + 연도 넘김도 자동으로 처리됨. val prevMonth = remember { LocalDate.now().minusMonths(1) @@ -49,8 +47,8 @@ fun CalendarIconBox( Box( modifier = modifier - .width(45.dp) - .height(48.dp), + .width(45.scaler) + .height(48.scaler), contentAlignment = Alignment.TopCenter ) { // 배경 달력 이미지 @@ -73,30 +71,25 @@ fun CalendarIconBox( // 연도 Text( text = year, - style = TextStyle( - fontSize = 6.sp, - fontFamily = Laundrygothic.font, - fontWeight = FontWeight(400), - color = Color(0xFFFFFFFF), - textAlign = TextAlign.Center, - ), - modifier = Modifier.padding(horizontal = 13.scaler) + fontSize = 6.sp, + lineHeight = 6.sp, + fontFamily = Laundrygothic.font, // PaperLogy 아님! + fontWeight = FontWeight(400), + color = colorTheme.white, + textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.height(7.scaler)) // 월 Text( text = monthLabel, - style = TextStyle( - fontSize = 14.sp, - lineHeight = 30.sp, - fontFamily = Laundrygothic.font, - fontWeight = FontWeight(700), - color = Color(0xFF000000), - textAlign = TextAlign.Center, - ), - modifier = Modifier.padding(horizontal = 1.scaler) + fontSize = 14.sp, + lineHeight = 30.sp, + fontFamily = Laundrygothic.font, // PaperLogy 아님! + fontWeight = FontWeight(700), + color = colorTheme.black, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 0.scaler) ) } @@ -104,15 +97,10 @@ fun CalendarIconBox( } } -@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Preview @Composable fun PreviewCalendarIconBox() { - Box( - modifier = Modifier - .wrapContentSize() - .padding(16.dp), - contentAlignment = Alignment.Center - ) { + LinkuPreview { CalendarIconBox() } } \ No newline at end of file diff --git a/feature/curation/src/main/java/com/linku/curation/ui/detail_card/HighlightCard.kt b/feature/curation/src/main/java/com/linku/curation/ui/detail_card/HighlightCard.kt deleted file mode 100644 index 461c46cc..00000000 --- a/feature/curation/src/main/java/com/linku/curation/ui/detail_card/HighlightCard.kt +++ /dev/null @@ -1,405 +0,0 @@ -package com.linku.curation.ui.detail_card - - -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.* -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import com.linku.curation.R -import com.linku.curation.CurationDetailUiState -import com.linku.design.theme.font.Paperlogy -import java.util.* - - -//큐레이션 디테일 화면 맨 위에 있는 카드 -> 곧 지워지지 않을까 싶지만, 재사용성 여부로 남김. - -@Composable -fun HighlightCard( - nickname: String, - monthLabel: String, - onBack: () -> Unit, - detailState: CurationDetailUiState, - liked: Boolean, - likeBusy: Boolean, - onToggleLike: () -> Unit, - modifier: Modifier = Modifier -) { - Box( - modifier = Modifier -// .graphicsLayer { //TODO : 그림자 약함. -// // -// shadowElevation = 6.dp.toPx() // Blur 강함 -// shape = RoundedCornerShape(bottomStart = 22.dp, bottomEnd = 22.dp) -// clip = false -// } - .background( - color = Color(0xFFCB59EB), - shape = RoundedCornerShape(bottomStart = 22.dp, bottomEnd = 22.dp) - ) - ){ - - /** 오른쪽 하단 로고 */ - Image( - painter = painterResource(id = R.drawable.ic_logo_light), - contentDescription = null, - modifier = Modifier - .align(Alignment.BottomEnd) - .offset( - x = (22.5).dp, // 오른쪽에서 안쪽으로 이동 (피그마 22.5dp) - y = (-50).dp // 아래에서 위로 이동 (피그마 50dp) - ) - .size(150.dp) // 피그마 로고 크기 그대로 - .graphicsLayer(alpha = 0.65f), - contentScale = ContentScale.Fit - ) - - /** ---------------------- 상단 바 ---------------------- */ - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 59.dp) - ) { - // 뒤로가기 - Icon( - painter = painterResource(R.drawable.ic_back), - contentDescription = "뒤로", - tint = Color.White, - modifier = Modifier - .padding(start = 22.dp) - .size(18.dp) - .align(Alignment.CenterStart) - .clickable { onBack() } - ) - - // 제목 - Text( - text = "큐레이션 콘텐츠", - color = Color.White, - style = TextStyle( - fontSize = 16.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight.Medium - ), - modifier = Modifier.align(Alignment.Center) - ) - } - - /** ---------------------- 본문 콘텐츠 ---------------------- */ - Column( - modifier = Modifier - .padding(start = 22.dp, end = 22.dp) - .padding(top = 111.dp) - ) { - - if (detailState.loading) { - - TextSkeleton(width = 180.dp, height = 20.dp) - Spacer(Modifier.height(13.dp)) - - TextSkeleton(width = 250.dp, height = 16.dp) - Spacer(Modifier.height(6.dp)) - TextSkeleton(width = 220.dp, height = 16.dp) - Spacer(Modifier.height(6.dp)) - TextSkeleton(width = 180.dp, height = 16.dp) - - Spacer(Modifier.height(24.dp)) - - TextSkeleton(width = 200.dp, height = 14.dp) - Spacer(Modifier.height(10.dp)) - - Row { - repeat(3) { - ChipSkeleton() - Spacer(Modifier.width(10.dp)) - } - } - - Spacer(Modifier.height(24.dp)) - - } else { - - Text( - text = "링큐 큐레이션 ㅣ ${formatMonthTag(detailState.month)}", - color = Color.White, - style = TextStyle( - fontSize = 20.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight.Bold - ), - maxLines = 1 - ) - - Spacer(Modifier.height(13.dp)) - - Text( - text = detailState.headerMent - ?.replace("(닉네임)", nickname) - ?.replace("{닉네임}", nickname) - ?: "생각은 많은데 정리가 안 되죠.\n${nickname}님의 머릿속을 환기시켜줄 콘텐츠들을 모았어요!", - color = Color.White, - style = MaterialTheme.typography.bodyMedium.copy( - fontSize = 16.sp, - lineHeight = 22.sp, - fontFamily = Paperlogy.font, - ), - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - - Spacer(Modifier.height(24.dp)) - - Text( - text = "${nickname}님의 ${monthLabel} 상황/감정태그 요약", - color = Color.White, - style = MaterialTheme.typography.bodySmall.copy( - fontSize = 13.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight.Medium - ) - ) - - Spacer(Modifier.height(10.dp)) - - EmotionTags(detailState) - - Spacer(Modifier.height(22.dp)) - } - } - - /** ---------------------- 좋아요 버튼 ---------------------- */ - HeartToggleButton( - liked = liked, - busy = likeBusy, - onClick = onToggleLike, - modifier = Modifier - .padding(end = 22.dp, top = 111.dp) - .align(Alignment.TopEnd) - ) - } -} - -@Composable -private fun EmotionTags(detailState: CurationDetailUiState) { - when { - detailState.loading -> { - Row { - repeat(3) { - Box( - Modifier - .width(165.dp) - .height(26.dp) - .clip(RoundedCornerShape(10.dp)) - .background(Color.White.copy(alpha = 0.25f)) - ) - Spacer(Modifier.width(10.dp)) - } - } - } - - detailState.topTags.isNotEmpty() -> { - Row { - detailState.topTags.take(3).forEach { - EmotionChip("#$it") - Spacer(Modifier.width(10.dp)) - } - } - } - - else -> { - Row { - listOf("#슬픔", "#커리어고민", "#짜증").forEach { - EmotionChip(it) - Spacer(Modifier.width(10.dp)) - } - } - } - } -} - -private fun formatMonthTag(month: String?): String { - if (month.isNullOrBlank()) return "" - return try { - val (y, m) = month.split("-") - "${y}년 ${m.toInt()}월호" - } catch (_: Exception) { - month - } -} - -@Composable -private fun HeartToggleButton( - modifier: Modifier = Modifier, - liked: Boolean, - busy: Boolean, - onClick: () -> Unit -) { - Box( - modifier = modifier - .padding(1.dp) - .size(width = 18.dp, height = 16.dp) - .clickable(enabled = !busy) { onClick() }, - contentAlignment = Alignment.Center - ) { - Icon( - painter = painterResource( - id = if (liked) R.drawable.ic_heart else R.drawable.ic_heart_outline - ), - contentDescription = null, - tint = Color.White, - modifier = Modifier.fillMaxSize() - ) - } -} - -@Composable -private fun EmotionChip(text: String) { - Box( - modifier = Modifier - .height(26.dp) - .clip(RoundedCornerShape(10.dp)) - .background(Color(0xFFFBEEFF)) - .padding(horizontal = 10.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = text, - color = Color(0xFF9A3AB5), - style = MaterialTheme.typography.bodySmall.copy( - fontFamily = Paperlogy.font, - fontSize = 12.sp, - fontWeight = FontWeight.Medium - ), - maxLines = 1 - ) - } -} - - -@Composable -fun TextSkeleton(width: Dp, height: Dp = 16.dp) { - val brush = rememberShimmerBrush() - - Box( - modifier = Modifier - .width(width) - .height(height) - .clip(RoundedCornerShape(6.dp)) - .background(brush) - ) -} - -@Composable -fun ChipSkeleton() { - val brush = rememberShimmerBrush() - - Box( - modifier = Modifier - .height(26.dp) - .width(70.dp) - .clip(RoundedCornerShape(10.dp)) - .background(brush) - ) -} - -@Composable -private fun rememberShimmerBrush(): Brush { - val transition = rememberInfiniteTransition(label = "shimmer") - - val shift = transition.animateFloat( - initialValue = 0f, - targetValue = 1000f, - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 1400, - easing = LinearEasing - ) - ), - label = "shift" - ).value - - // Figma 기준 컬러 - val colors = listOf( - Color(0xFFFBEFFF), - Color(0xFFF4D9FF), - Color(0xFFE3A3F5) - ) - - return Brush.linearGradient( - colors = colors, - start = Offset(shift - 400f, shift - 400f), - end = Offset(shift, shift) - ) -} - - - - - - -@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF, widthDp = 390) -@Composable -private fun PreviewHighlightCard_Phone() { - val demoState = CurationDetailUiState( - loading = false, - topTags = listOf("설렘", "통학 중", "공부 중"), - headerMent = "세나님의 하루가 반짝였던 순간이에요. 그 감정에 어울리는 콘텐츠를 추천해요.", - footerMent = "설렘은 가장 강력한 동기부여예요. 지금, 그 에너지를 믿어보세요.", - month = "2025-07" - ) - - HighlightCard( - nickname = "세나", - monthLabel = "7월", - onBack = {}, - detailState = demoState, - liked = true, - likeBusy = false, - onToggleLike = {} - ) -} - -@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF, widthDp = 800) -@Composable -private fun PreviewHighlightCard_Tablet() { - val demoState = CurationDetailUiState( - loading = false, - topTags = listOf("설렘", "성장", "커리어 고민"), - headerMent = "요즘 ${"세나"}님의 마음에 가장 많이 떠오른 감정이에요. 이 감정에 어울리는 콘텐츠를 추천할게요!", - footerMent = "너무 잘 하고 있어요. 지금의 고민도 세나님만의 길이 될 거예요.", - month = "2025-07" - ) - - HighlightCard( - nickname = "세나", - monthLabel = "7월", - onBack = {}, - detailState = demoState, - liked = false, - likeBusy = false, - onToggleLike = {} - ) -} \ No newline at end of file diff --git a/feature/curation/src/main/java/com/linku/curation/ui/effect/highlight/RadialGradientCircle.kt b/feature/curation/src/main/java/com/linku/curation/ui/effect/highlight/RadialGradientCircle.kt index c8599a91..f758b65b 100644 --- a/feature/curation/src/main/java/com/linku/curation/ui/effect/highlight/RadialGradientCircle.kt +++ b/feature/curation/src/main/java/com/linku/curation/ui/effect/highlight/RadialGradientCircle.kt @@ -6,11 +6,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush @@ -18,22 +18,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp +import com.linku.design.theme.linkuColors import com.linku.design.util.LocalFigmaDimens import com.linku.design.util.rememberFigmaDimens import com.linku.design.util.scaler -/** - * Figma Ellipse 541 - * - Size: 321 x 321 (Figma 기준) - * - Radial Gradient - * - Color: #2C6FFF (12% → 0%) - * - Background decorative component - */ +// 큐레이션 대신 해주는데 기본으로 쓰는 값들은 넣어 드려야지.... @Composable fun RadialGradientCircle( modifier: Modifier = Modifier, - color: Color, - alpha: Float = 0.12f, // 기본값: 피그마 12% + color: Color = MaterialTheme.linkuColors.accentColor, + alpha: Float = 0.12f, size: Dp = 321.scaler ) { val radiusPx = with(LocalDensity.current) { @@ -42,13 +37,12 @@ fun RadialGradientCircle( Box( modifier = modifier - .requiredSize(size) // 부모 constraint 무시하고 강제로 정사각형 유지 - .alpha(alpha) + .requiredSize(size) .clip(CircleShape) .background( brush = Brush.radialGradient( colorStops = arrayOf( - 0f to color, // 중심 100% + 0f to color.copy(alpha = alpha), 1f to Color.Transparent ), center = Offset(radiusPx, radiusPx), @@ -66,7 +60,7 @@ private fun RadialGradientCirclePreview() { Box( modifier = Modifier .fillMaxSize() - .background(Color(0xFFEDEDED)), + .background(Color(0xFFEDEDED)), //귀찮아서 프리뷰는 하드 코딩 그대로 놓음... 귀찮아... contentAlignment = Alignment.Center ) { RadialGradientCircle( diff --git a/feature/curation/src/main/java/com/linku/curation/ui/emotion/CurationEmotionBar.kt b/feature/curation/src/main/java/com/linku/curation/ui/emotion/CurationEmotionBar.kt new file mode 100644 index 00000000..8535c39a --- /dev/null +++ b/feature/curation/src/main/java/com/linku/curation/ui/emotion/CurationEmotionBar.kt @@ -0,0 +1,61 @@ +package com.linku.curation.ui.emotion + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.lerp +import com.linku.design.theme.LinkuPreview +import com.linku.design.theme.linkuColors + +private val BAR_MAX_WIDTH = + 248.dp // 프로그래스바 최대 길이(100%) 일 때, 근데 100%이면 나머지 2개는 어떻게 하지? 음? 몰라 현우 오빠가 알아서 잘 해줄거임. 화이팅! +private val BAR_MIN_WIDTH = 22.dp // 프로그래스 바 최소 길기(0% 이어도 이정도 마지노선 길이는 가져야 함.) +private val BAR_HEIGHT = 45.dp // 높이는 모두 동일함. + +/** + * 큐레이션 감정 분석 프로그레스 바 + * + * @param progress 0.0(최소) ~ 1.0(최대) 비율. 백엔드에서 수신 + * @param rank 순위 (0 = 1위, 1 = 2위, 2 = 3위). 색상 자동 결정: blue[300] / blue[200] / blue[100] + */ +@Composable +fun CurationEmotionBar( + modifier: Modifier = Modifier, + progress: Float = 0.75f, + rank: Int = 0, //0,1,2 (혹시 마음에 안 들면 1번부터 하게 편하게 수정해주세요~!) +) { + val colorTheme = MaterialTheme.linkuColors + + // 연동해주는데 귀찮게 언제 컬러 지정하게 하면 안됨. 귀찮은건 미리 다 했습니다~! + val barColor = when (rank) { + 0 -> colorTheme.blue[300] + 1 -> colorTheme.blue[200] + else -> colorTheme.blue[100] + } + val barWidth = lerp(BAR_MIN_WIDTH, BAR_MAX_WIDTH, progress.coerceIn(0f, 1f)) + + Box( + modifier = modifier + .width(barWidth) + .height(BAR_HEIGHT) + .background( + color = barColor, + shape = RoundedCornerShape(12.dp) + ) + ) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewCurationEmotionBar() { + LinkuPreview { + CurationEmotionBar(progress = 0.75f, rank = 1) + } +} diff --git a/feature/curation/src/main/java/com/linku/curation/ui/emotion/CurationEmotionChip.kt b/feature/curation/src/main/java/com/linku/curation/ui/emotion/CurationEmotionChip.kt new file mode 100644 index 00000000..722f85c6 --- /dev/null +++ b/feature/curation/src/main/java/com/linku/curation/ui/emotion/CurationEmotionChip.kt @@ -0,0 +1,61 @@ +package com.linku.curation.ui.emotion + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.linku.design.theme.LinkuPreview +import com.linku.design.theme.linkuColors + +/** + * 큐레이션 감정 분석 키워드 칩 + * + * @param text 표시할 키워드 (예: "#짜증") + */ +@Composable +fun CurationEmotionChip( + modifier: Modifier = Modifier, + text: String = "짜증", +) { + val colorTheme = MaterialTheme.linkuColors + + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .wrapContentWidth() + .height(26.dp) + .border( + width = 1.dp, + color = colorTheme.blue[200], + shape = RoundedCornerShape(10.dp) + ) + .padding(horizontal = 8.dp) + ) { + Text( + text = text, + fontSize = 13.sp, + lineHeight = 15.sp, + fontWeight = FontWeight(400), + color = colorTheme.blue[200] + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewCurationEmotionChip() { + LinkuPreview { + CurationEmotionChip(text = "#짜증") + } +} diff --git a/feature/curation/src/main/java/com/linku/curation/ui/emotion/CurationEmotionSection.kt b/feature/curation/src/main/java/com/linku/curation/ui/emotion/CurationEmotionSection.kt new file mode 100644 index 00000000..2b7c6d62 --- /dev/null +++ b/feature/curation/src/main/java/com/linku/curation/ui/emotion/CurationEmotionSection.kt @@ -0,0 +1,91 @@ +package com.linku.curation.ui.emotion + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.linku.design.theme.LinkuPreview +import com.linku.design.theme.linkuColors +import java.util.Calendar + +data class EmotionItem( + val progress: Float, + val keyword: String +) + +/** + * 큐레이션 감정 분석 섹션 + * + * 이전 달 상황/감정 키워드 상위 3개를 프로그레스 바 + 칩으로 표시. + * 순위별 바 색상: 1위 blue[300], 2위 blue[200], 3위 blue[100] + * + * @param items 백엔드에서 수신한 감정 항목 리스트 (progress + keyword, 최대 3개) + */ +@Composable +fun CurationEmotionSection( + modifier: Modifier = Modifier, + items: List = emptyList(), +) { + val colorTheme = MaterialTheme.linkuColors + + val month = remember { + Calendar.getInstance().apply { add(Calendar.MONTH, -1) }.get(Calendar.MONTH) + 1 + } + + Column(modifier = modifier) { + Text( + text = "${month}월 상황/감정 요약", + style = TextStyle( + brush = colorTheme.emotionTitleGradient, + fontSize = 18.sp, + lineHeight = 25.sp, + fontWeight = FontWeight(600) + ) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + items.take(3).forEachIndexed { index, item -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + CurationEmotionBar( + progress = item.progress, + rank = index + ) + CurationEmotionChip(text = item.keyword) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewCurationEmotionSection() { + LinkuPreview { + CurationEmotionSection( + items = listOf( + EmotionItem(progress = 1.0f, keyword = "#업무 준비 중"), + EmotionItem(progress = 0.76f, keyword = "#커리어고민"), + EmotionItem(progress = 0.09f, keyword = "#짜증") + ), + modifier = Modifier.wrapContentSize() + ) + } +} diff --git a/feature/curation/src/main/java/com/linku/curation/ui/header/CurationHeader.kt b/feature/curation/src/main/java/com/linku/curation/ui/header/CurationHeader.kt new file mode 100644 index 00000000..775d0fc9 --- /dev/null +++ b/feature/curation/src/main/java/com/linku/curation/ui/header/CurationHeader.kt @@ -0,0 +1,60 @@ +package com.linku.curation.ui.header + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import com.linku.curation.R +import com.linku.design.theme.LinkuPreview +import com.linku.design.theme.linkuColors +import com.linku.design.util.scaler + +@Composable +fun CurationHeader( + modifier: Modifier = Modifier, + nickname: String, +) { + val colorTheme = MaterialTheme.linkuColors + + Column(modifier = modifier) { + Image( + painter = painterResource(id = R.drawable.img_curation_title), + contentDescription = null, + modifier = Modifier + .padding(start = 24.scaler) + .size(width = 102.scaler, height = 15.scaler) + ) + + Spacer(modifier = Modifier.height(12.scaler)) + + Text( + text = "${nickname}님을 위한 링큐레이션", + fontSize = 22.sp, + lineHeight = 30.sp, + fontWeight = FontWeight(700), + style = TextStyle( + brush = colorTheme.curationGradient + ), + modifier = Modifier.padding(horizontal = 24.scaler) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewCurationHeader() { + LinkuPreview { + CurationHeader(nickname = "세나") + } +} diff --git a/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationCardItem.kt b/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationCardItem.kt index 5240d595..10df6fc7 100644 --- a/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationCardItem.kt +++ b/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationCardItem.kt @@ -3,50 +3,52 @@ package com.linku.curation.ui.main_card import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer 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.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import coil3.compose.SubcomposeAsyncImage -import com.linku.curation.R -import com.linku.design.util.scaler -import java.time.LocalDate -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.Alignment -import androidx.compose.material3.Text import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.linku.design.theme.font.Paperlogy -import androidx.compose.foundation.layout.Row +import coil3.compose.SubcomposeAsyncImage +import com.linku.curation.R import com.linku.curation.ui.item.CurationCheckOutButton +import com.linku.design.theme.LinkuPreview +import com.linku.design.theme.linkuColors +import com.linku.design.util.scaler +import java.util.Calendar -// 카드 컨텐츠 데이터 클래스(api 연동 아님.) +// 카드 컨텐츠 데이터 클래스(api 연동XX) -> 현우 오빠 화이팅 🎉 data class CurationCardContent( val title: String, val description: String ) -// 카드별 고정 컨텐츠 (1번은 동적으로 연도/월 계산) +// 카드별 고정 컨텐츠 (1번은 동적으로 연도/월 계산) -> 이건 멘트 고정이라고 합니다!(돈 워리) fun getCurationCardContents(): List { - val prevMonth = LocalDate.now().minusMonths(1) - val year = prevMonth.year.toString() - val month = prevMonth.monthValue + val cal = Calendar.getInstance().apply { add(Calendar.MONTH, -1) } + val year = cal.get(Calendar.YEAR).toString() + val month = cal.get(Calendar.MONTH) + 1 return listOf( - // 현재 기준 바로 직전 지난 달을 보여줌. CurationCardContent( title = "${year}\n월간 큐레이션 ${month}월호", description = "이번 달을 위한 링크, 링큐가 준비했어요" @@ -62,19 +64,27 @@ fun getCurationCardContents(): List { ) } -// CurationMainCardPager.kt -// private 키워드 제거! +/** + * 큐레이션 메인 카드 아이템 (이미지 + 타이틀 + 설명 + 페이지 인디케이터 + 체크아웃 버튼) + * + * @param modifier 외부에서 전달하는 Modifier (기본값: Modifier) + * @param imageUrl 카드 배경 이미지 URL. blank / "null" 문자열이면 fallbackImage로 대체됨 + * @param page 현재 카드의 0-based 페이지 인덱스. getCurationCardContents()에서 해당 인덱스의 콘텐츠를 가져오는 데도 사용됨 + * @param totalPage 전체 카드 페이지 수 (페이지 인디케이터 표시용, 기본값: 3) + * @param onCheckOutClick 카드 우측 하단 체크아웃 버튼 클릭 콜백 + * @param fallbackImage imageUrl이 유효하지 않을 때 표시할 기본 이미지 리소스 + */ @Composable fun CurationCardItem( - imageUrl: String?, + modifier: Modifier = Modifier, + imageUrl: String, page: Int = 0, totalPage: Int = 3, onCheckOutClick: () -> Unit = {}, - modifier: Modifier = Modifier, - @DrawableRes fallbackImage: Int = R.drawable.img_curation_example // 테스트용 기본값 + @DrawableRes fallbackImage: Int = R.drawable.img_curation_example // 일단 프리뷰 테스트를 위해 기본 이미지 넣어두었습니다~! ) { - val resolvedImageUrl = imageUrl - ?.takeIf { it.isNotBlank() && it != "null" } + val colorTheme = MaterialTheme.linkuColors + val resolvedImageUrl = imageUrl.takeIf { it.isNotBlank() && it != "null" } val contents = getCurationCardContents() val content = contents.getOrNull(page) ?: contents[0] @@ -82,14 +92,12 @@ fun CurationCardItem( modifier = modifier .shadow( elevation = 16.dp, - //shape = RoundedCornerShape(24.scaler), - ambientColor = Color.Black.copy(alpha = 0.4f), - spotColor = Color.Black.copy(alpha = 0.4f) + ambientColor = colorTheme.black.copy(alpha = 0.4f), + spotColor = colorTheme.black.copy(alpha = 0.4f) ) .clip(RoundedCornerShape(24.scaler)) - .background(Color(0xFFF2F2F2)) + .background(colorTheme.curationCardBackground) ) { - // 배경 이미지 if (resolvedImageUrl == null) { Image( painter = painterResource(id = fallbackImage), @@ -109,102 +117,59 @@ fun CurationCardItem( Box( modifier = Modifier .matchParentSize() - .background( - brush = Brush.verticalGradient( - colorStops = arrayOf( - 0.0f to Color(0x00000000), - 1.0f to Color(0x66000000) - ) - ) - ) + .background(colorTheme.curationCardOverlayGradient) ) - // 텍스트 오버레이 Column( modifier = Modifier .align(Alignment.TopStart) - .padding(top = 262.scaler) + .padding(top = 248.scaler) // TODO : 피그마 대로 구현하니 너무 내려감... 임의로 262 -> 248로 수정함. 디자이너가 괜찮으면 좋겠음... ) { - // 제목 Text( text = content.title, - style = TextStyle( - fontSize = 24.sp, - lineHeight = 30.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight(700), - color = Color(0xFFFFFFFF), - ), + fontSize = 24.sp, + lineHeight = 30.sp, + fontWeight = FontWeight(700), + color = colorTheme.white, modifier = Modifier.padding(start = 35.scaler) ) Spacer(modifier = Modifier.height(18.scaler)) - // 소개글 Text( text = content.description, - style = TextStyle( - fontSize = 15.sp, - lineHeight = 22.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight(400), - color = Color(0xFFFFFFFF), - ), + fontSize = 15.sp, + lineHeight = 22.sp, + fontWeight = FontWeight(400), + color = colorTheme.white, modifier = Modifier.padding(start = 35.scaler) ) Spacer(modifier = Modifier.height(18.scaler)) - // 01 | 03 + 보러가기 버튼 Box( modifier = Modifier .fillMaxWidth() - .padding( - start = 35.scaler, - end = 35.scaler - ) + .padding(horizontal = 35.scaler) ) { - // 페이지 표시 + val pageIndicatorStyle = TextStyle( + fontSize = 14.sp, + lineHeight = 16.sp, + fontWeight = FontWeight(500), + color = colorTheme.gray[300], + textAlign = TextAlign.Center + ) + Row( modifier = Modifier.align(Alignment.CenterStart), + horizontalArrangement = Arrangement.spacedBy(12.scaler), verticalAlignment = Alignment.CenterVertically ) { - Text( - text = String.format("%02d", page + 1), - style = TextStyle( - fontSize = 14.sp, - lineHeight = 16.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight(500), - color = Color(0xFFD7D9DF), - textAlign = TextAlign.Center, - ) - ) - Text( - text = " | ", - style = TextStyle( - fontSize = 14.sp, - lineHeight = 16.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight(500), - color = Color(0xFFD7D9DF), - textAlign = TextAlign.Center, - ) - ) - Text( - text = String.format("%02d", totalPage), - style = TextStyle( - fontSize = 14.sp, - lineHeight = 16.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight(500), - color = Color(0xFFD7D9DF), - textAlign = TextAlign.Center, - ) - ) + Text(text = (page + 1).toString().padStart(2, '0'), style = pageIndicatorStyle) + Text(text = "|", style = pageIndicatorStyle) + Text(text = totalPage.toString().padStart(2, '0'), style = pageIndicatorStyle) } - // 보러가기 버튼 CurationCheckOutButton( onClick = onCheckOutClick, modifier = Modifier.align(Alignment.CenterEnd) @@ -212,4 +177,15 @@ fun CurationCardItem( } } } -} \ No newline at end of file +} + +@Preview(showBackground = true) +@Composable +private fun PreviewCurationCardItem() { + LinkuPreview { + CurationCardItem( + modifier = Modifier.size(width = 346.scaler, height = 432.scaler), + imageUrl = "", + ) + } +} diff --git a/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationMainCard.kt b/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationMainCard.kt index 01171ee0..71839928 100644 --- a/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationMainCard.kt +++ b/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationMainCard.kt @@ -1,32 +1,42 @@ package com.linku.curation.ui.main_card +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview -import com.linku.curation.R import coil3.compose.SubcomposeAsyncImage +import com.linku.curation.R +import com.linku.design.theme.linkuColors import com.linku.design.util.scaler -import androidx.compose.foundation.Image -import androidx.compose.ui.res.painterResource + /** * 큐레이션 메인 카드 * * - Backend 이미지 URL 표시 - * - imageUrl == null → 예시 이미지 표시 + * - imageUrl이 빈 문자열이면 fallback 이미지 표시 * - 사이즈: 346 x 432 (scaler) + * + * @param modifier 외부에서 전달하는 Modifier (기본값: Modifier) + * @param imageUrl 백엔드에서 받아오는 카드 이미지 URL. 빈 문자열이면 fallbackImage로 대체됨 + * @param fallbackImage imageUrl이 없을 때 표시할 기본 이미지 리소스 (기본값: img_curation_example) */ @Composable fun CurationMainCard( - imageUrl: String?, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + imageUrl: String, + @DrawableRes fallbackImage: Int = R.drawable.img_curation_example, //이건 프리뷰 확인 용이라 나중에 지워주세요:) ) { + val colorTheme = MaterialTheme.linkuColors + Box( modifier = modifier .size( @@ -34,12 +44,11 @@ fun CurationMainCard( height = 432.scaler ) .clip(RoundedCornerShape(24.scaler)) - .background(Color(0xFFF2F2F2)) + .background(colorTheme.curationCardBackground) ) { - if (imageUrl.isNullOrBlank()) { - // null / preview / fallback //ui 확인을 위해 일단 큐레이션 기본 이미지 보여주도록 수정함. 이후 기본 이미지 확정시 수정함. + if (imageUrl.isBlank()) { Image( - painter = painterResource(id = R.drawable.img_curation_example), + painter = painterResource(id = fallbackImage), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize() @@ -59,6 +68,6 @@ fun CurationMainCard( @Preview(showBackground = true) @Composable private fun PreviewCurationMainCard() { - CurationMainCard(imageUrl = null) + CurationMainCard(imageUrl = "") } diff --git a/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationMainCardPager.kt b/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationMainCardPager.kt index 4a651b1e..6963f44b 100644 --- a/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationMainCardPager.kt +++ b/feature/curation/src/main/java/com/linku/curation/ui/main_card/CurationMainCardPager.kt @@ -1,107 +1,105 @@ package com.linku.curation.ui.main_card -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Image +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil3.compose.SubcomposeAsyncImage -import com.linku.curation.R +import com.linku.design.theme.linkuColors import com.linku.design.util.scaler import kotlin.math.absoluteValue -import androidx.compose.animation.core.snap - /** * 큐레이션 메인 카드 페이저 - * - 중앙: 346 x 432 - * - 양옆: 137.5 x 230 - * - 간격: 12dp + * + * 3장의 카드를 좌우로 스와이프할 수 있는 HorizontalPager. + * 중앙 카드는 원본 크기(1.0)로, 양옆 카드는 83% 스케일로 표시되며 + * 스케일 전환 시 텍스트·이미지가 함께 축소된다 (graphicsLayer 사용). -> 다인 언니 수정했어 히히 + * + * @param modifier 외부에서 전달하는 Modifier + * @param pagerState HorizontalPager 스크롤 상태. 외부에서 rememberPagerState로 생성해서 전달 + * @param imageUrls 각 카드에 표시할 이미지 URL 리스트. blank / "null" 문자열이면 fallback 이미지 표시 + * @param onCardClick 카드 하단 체크아웃 버튼 클릭 시 호출. (페이지 인덱스, 이미지 URL) 전달 */ @Composable fun CurationMainCardPager( - pagerState: PagerState, - imageUrls: List, // 3개의 이미지 URL - isDetailOpen: Boolean, modifier: Modifier = Modifier, - onCardClick: (index: Int, imageUrl: String?) -> Unit = { _, _ -> } // 추가 + pagerState: PagerState, + imageUrls: List, + onCardClick: (index: Int, imageUrl: String) -> Unit = { _, _ -> }, ) { - - Column( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally ) { HorizontalPager( state = pagerState, - userScrollEnabled = !isDetailOpen, - contentPadding = PaddingValues(horizontal = 33.scaler), // 양옆 여백으로 미리보기 + contentPadding = PaddingValues(horizontal = 33.scaler), pageSpacing = 12.scaler, modifier = Modifier .fillMaxWidth() .height(432.scaler) ) { page -> + // 현재 페이지 기준으로 이 카드가 얼마나 떨어져 있는지 (0 = 중앙, 1 = 한 칸 옆) val pageOffset = ( (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction ).absoluteValue - // 0 = 중앙(선택), 1 = 양옆 + // offset 0.5 미만이면 중앙 카드로 판별 val isSelected = pageOffset < 0.5f - // 애니메이션으로 크기 전환 - val cardWidth by animateDpAsState( - targetValue = if (isSelected) 346.scaler else 288.scaler, - animationSpec = if (isDetailOpen) snap() else tween(300), - label = "cardWidth" - ) - - val cardHeight by animateDpAsState( - targetValue = if (isSelected) 432.scaler else 360.scaler, - animationSpec = if (isDetailOpen) snap() else tween(300), - label = "cardHeight" + // 중앙: 1.0, 양옆: 0.832 — graphicsLayer로 레이아웃 크기는 고정하고 시각적으로만 축소 + val scale by animateFloatAsState( + targetValue = if (isSelected) 1f else 0.832f, + animationSpec = spring( // 스프링이 쫀득한 대신, 시간 조절이 안됩니다. 디자이너, pm이 원해도 애니메이션 시간 지정 불가함.... + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ), + label = "cardScale" ) - - // 스케일 기반 부드러운 전환 (선택사항) - val scale = 1f - (pageOffset * 0.3f).coerceIn(0f, 0.3f) - Box( modifier = Modifier - .fillMaxHeight(), + .fillMaxWidth() + .height(432.scaler), contentAlignment = Alignment.Center ) { CurationCardItem( imageUrl = imageUrls[page], page = page, totalPage = imageUrls.size, - onCheckOutClick = { onCardClick(page, imageUrls[page]) }, // ← 추가 + onCheckOutClick = { onCardClick(page, imageUrls[page]) }, modifier = Modifier - .width(cardWidth) - .height(cardHeight) + .width(346.scaler) + .height(432.scaler) + .graphicsLayer { + scaleX = scale + scaleY = scale + } ) } } Spacer(modifier = Modifier.height(18.scaler)) - // 인디케이터 CurationPagerIndicator( pageCount = imageUrls.size, currentPage = pagerState.currentPage @@ -109,13 +107,19 @@ fun CurationMainCardPager( } } - +/** + * 페이저 하단 인디케이터 도트 + * + * 선택된 페이지는 가로로 길게 늘어난 pill 형태, 나머지는 원형 도트로 표시. + */ @Composable private fun CurationPagerIndicator( pageCount: Int, currentPage: Int, modifier: Modifier = Modifier ) { + val colorTheme = MaterialTheme.linkuColors + Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(7.dp), @@ -123,25 +127,15 @@ private fun CurationPagerIndicator( ) { repeat(pageCount) { index -> val isSelected = index == currentPage - Box( modifier = Modifier .width(if (isSelected) 28.dp else 7.dp) .height(7.dp) .background( - color = if (isSelected) Color(0xFF87898F) else Color(0xFFD7D9DF), + color = if (isSelected) colorTheme.gray[600] else colorTheme.gray[300], shape = RoundedCornerShape(if (isSelected) 3.5.dp else 7.dp) ) ) } } } - -/* -@Preview(showBackground = true) -@Composable -private fun PreviewCurationMainCardPager() { - CurationMainCardPager( - imageUrls = listOf(null, null, null) - ) -}*/ \ No newline at end of file diff --git a/feature/curation/src/main/java/com/linku/curation/ui/recommend_list/CurationRecommendedLinksSection.kt b/feature/curation/src/main/java/com/linku/curation/ui/recommend_list/CurationRecommendedLinksSection.kt index 15445394..41bca5d6 100644 --- a/feature/curation/src/main/java/com/linku/curation/ui/recommend_list/CurationRecommendedLinksSection.kt +++ b/feature/curation/src/main/java/com/linku/curation/ui/recommend_list/CurationRecommendedLinksSection.kt @@ -1,15 +1,26 @@ package com.linku.curation.ui.recommend_list - +//TODO : 지현이 PR merge 되면 이거 제거 후 지현이가 만든거 사용하기 // 역할: 추천 링크 리스트 섹션 + 카드 // 기능: 서버에서 받은 RecommendedLink(title, imageUrl, url, categories, domain)를 표시 // 관심사: 리스트 반복, 썸네일/아이콘 표시, 클릭 콜백 -import androidx.compose.ui.unit.lerp import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -30,14 +41,15 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.lerp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import coil3.request.ImageRequest -import com.linku.curation.ui.resolveSourceIcon -import com.linku.curation.ui.resolveSourceLabel +import coil3.request.crossfade import com.linku.core.model.RecommendedLink import com.linku.curation.R -import coil3.request.crossfade +import com.linku.curation.ui.resolveSourceIcon +import com.linku.curation.ui.resolveSourceLabel import com.linku.curation.ui.util.ShimmerSkeleton import com.linku.curation.ui.util.rememberScaleFactor import com.linku.curation.ui.util.shimmer diff --git a/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationCard1Screen.kt b/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationCard1Screen.kt new file mode 100644 index 00000000..d967f83a --- /dev/null +++ b/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationCard1Screen.kt @@ -0,0 +1,16 @@ +package com.linku.curation.ui.screen + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +//TODO : 다음 PR에서 구현할거예요.... +@Composable +fun CurationCard1Screen( + onBack: () -> Unit +) { + BackHandler { onBack() } + Box(modifier = Modifier.fillMaxSize()) +} diff --git a/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationCard3Screen.kt b/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationCard3Screen.kt new file mode 100644 index 00000000..7927399f --- /dev/null +++ b/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationCard3Screen.kt @@ -0,0 +1,16 @@ +package com.linku.curation.ui.screen + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +//TODO : 다음 PR에서 구현할거예요.... +@Composable +fun CurationCard3Screen( + onBack: () -> Unit +) { + BackHandler { onBack() } + Box(modifier = Modifier.fillMaxSize()) +} diff --git a/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationDetailScreen.kt b/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationDetailScreen.kt deleted file mode 100644 index 3e701c60..00000000 --- a/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationDetailScreen.kt +++ /dev/null @@ -1,524 +0,0 @@ -package com.linku.curation.ui.screen - -import androidx.compose.ui.geometry.Offset -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.ui.unit.dp -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.ui.draw.clip - -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp -import com.linku.design.theme.LocalColorTheme -import com.linku.curation.CurationDetailViewModel -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import java.util.Locale -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.graphics.Color -import com.linku.core.model.RecommendedLink -import com.linku.curation.CurationLinksUiState - -import kotlin.math.min -import com.linku.curation.CurationViewModel - -import androidx.compose.runtime.* -import androidx.compose.ui.text.TextStyle -import com.linku.curation.CurationDetailUiState -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.linku.core.util.ensureHttpScheme -import com.linku.curation.ui.detail_card.HighlightCard -import com.linku.curation.ui.recommend_list.CurationRecommendedLinksSection -import com.linku.curation.ui.recommend_list.RecommendedLinkCardSkeleton -import com.linku.curation.ui.recommend_list.SkeletonEnd -import com.linku.curation.ui.recommend_list.SkeletonStart -import com.linku.design.theme.font.Paperlogy - - -//헬퍼 - - -/* ===== 실제 화면: VM에서 닉네임 받아와 Content 호출 ===== */ -@Composable -fun CurationDetailScreen( - userId: Long, - curationId: Long, - nickname: String? = null, - //viewModel: CurationViewModel = hiltViewModel(), - detailViewModel: CurationDetailViewModel, // 외부 주입만 사용 -> 수정. - homeViewModel: CurationViewModel, - //detailViewModel: CurationDetailViewModel = hiltViewModel(), - //homeViewModel: CurationViewModel = hiltViewModel(), // 닉네임 전용,하트 상태/토글 재사용 - onBack: () -> Unit = {}, - -) { - - - val nicknameState = homeViewModel.nickname.collectAsState(initial = "") - val finalNickname = ((nickname ?: nicknameState.value) ?: "").ifBlank { "세나" } - - // 추천 링크 로드 (디테일 VM) - LaunchedEffect(userId, curationId) { - detailViewModel.loadAll(userId, curationId) // detail + links + like(뷰모델 내부 분기) - } - - val detail by detailViewModel.detail.collectAsStateWithLifecycle() - val linksState by detailViewModel.links.collectAsStateWithLifecycle() - - - val likeBusy by homeViewModel.likeBusy.collectAsStateWithLifecycle() - - - val monthLabel = remember { - LocalDate.now() - .format(DateTimeFormatter.ofPattern("M월", Locale.KOREAN)) - } - - CurationDetailScreenContent( - nickname = finalNickname, - monthLabel = monthLabel, - linksState = linksState, - onBack = onBack, - detailState = detail, - - - ) -} - -/* ===== UI 전용 Content ===== */ -@Composable -private fun CurationDetailScreenContent( - nickname: String, - monthLabel: String, - linksState: CurationLinksUiState, - onBack: () -> Unit = {}, - detailState: CurationDetailUiState, - -) { - val uri = LocalUriHandler.current - - Column( - modifier = Modifier - .fillMaxSize() - .background(LocalColorTheme.current.white) - - ) { - - - // HighlightCard는 스크롤 되지 않도록 해야함. - - Box( - modifier = Modifier - - ) { - HighlightCard( - nickname = nickname, - monthLabel = monthLabel, - onBack = onBack, - detailState = detailState, - // 에러 해결을 위해 아래 3개 파라미터 추가 - 추후 api 변동시 삭제 예정. - liked = false, - likeBusy = false, - onToggleLike = {} - - ) - } - - - Spacer(Modifier.height(20.dp)) - - //여기만 스크롤이 가능해야함. - Column( - modifier = Modifier - .weight(1f) - .verticalScroll(rememberScrollState()) - ) { - if (linksState.loading) { - SectionTitleSkeleton( - modifier = Modifier.padding(start = 24.dp) - ) - } else { - Text( - text = "추천 링크", - style = MaterialTheme.typography.titleMedium.copy( - fontFamily = Paperlogy.font, - fontWeight = FontWeight.Bold, - fontSize = 20.sp - ), - color = LocalColorTheme.current.black, - modifier = Modifier.padding(start = 24.dp) - ) - } - - Spacer(Modifier.height(20.dp)) - - CurationRecommendedLinksPagerWrapper( - links = linksState.items, - loading = linksState.loading, - onRetry = { /* reload */ }, - onClick = { url -> uri.openUri(ensureHttpScheme(url)) }, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(Modifier.height(16.dp)) - - PositiveNoteCard( - footerMent = detailState.footerMent, - loading = detailState.loading, - modifier = Modifier.padding(horizontal = 20.dp) - ) - - Spacer(Modifier.height(32.dp)) - } - } -} - - - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun CurationRecommendedLinksPagerWrapper( - links: List, - loading: Boolean, - onRetry: () -> Unit, - onClick: (String) -> Unit, - modifier: Modifier = Modifier -) { - val items = remember(links) { links.take(9) } - val perPage = 3 - val pageCount = ((items.size + perPage - 1) / perPage).coerceAtLeast(1) - - Column(modifier) { - - // 로딩 중일 때: 스켈레톤 3개만 표시하고 페이저/인디케이터 숨김 - if (loading) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - SkeletonLinks() - - Spacer(Modifier.height(16.dp)) - - // 인디케이터 영역 명확히 확보 - Box( - modifier = Modifier - .fillMaxWidth() - .height(24.dp), - contentAlignment = Alignment.Center - ) { - PagerIndicatorSkeleton() - } - } - return - } - // 링크 없음 - if (items.isEmpty()) { - EmptyLinks(onRetry) - return - } - - val pagerState = rememberPagerState(pageCount = { pageCount }) - - HorizontalPager( - state = pagerState, - modifier = Modifier.fillMaxWidth() - ) { page -> - - val start = page * perPage - val end = min(start + perPage, items.size) - val slice = items.subList(start, end) - - CurationRecommendedLinksSection( - modifier = Modifier.fillMaxWidth(), - links = slice, - loading = false, //로딩 아님 (Wrapper에서 처리) - onRetry = onRetry, - onClick = onClick - ) - } - - Spacer(Modifier.height(12.dp)) - - // 인디케이터 - PagerIndicator( - pageCount = pageCount, - currentPage = pagerState.currentPage, - - ) - } -} - -//제목 스켈레톤 추가(추천 링크) -@Composable -private fun SectionTitleSkeleton( - modifier: Modifier = Modifier -) { - val shimmerBrush = rememberGrayShimmerBrush() - - Box( - modifier = modifier - .width(80.dp) - .height(20.dp) - .clip(RoundedCornerShape(6.dp)) - .background(shimmerBrush) - ) -} - - - -// 3) 빈 상태 -@Composable -private fun EmptyLinks(onRetry: () -> Unit) { - Column( - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .background(Color(0xFFF8F8F8)) - .padding(16.dp) - ) { - Text("아직 추천할 링크가 없어요.") - Spacer(Modifier.height(8.dp)) - Text("새로고침", color = Color(0xFFCB59EB), modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .padding(2.dp) - .then(Modifier.clickable { onRetry() })) - } -} - -@Composable -private fun SkeletonLinks() { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) // 홈 스크린과 동일 패딩 - ) { - repeat(3) { - RecommendedLinkCardSkeleton() // ← 모든 디자인 동일 - Spacer(Modifier.height(10.dp)) - } - } -} - - - - -@Composable -fun TagChip(text: String) { - Box( - modifier = Modifier - .background(Color(0xFFEFEFEF), RoundedCornerShape(8.dp)) - .padding(horizontal = 6.dp, vertical = 2.dp) - ) { - Text( - text = text, - style = MaterialTheme.typography.bodySmall.copy( - fontFamily = Paperlogy.font, - fontSize = 12.sp, - color = Color.Gray - ) - ) - } -} - -/* ===== 하단 긍정 메시지 카드 ===== */ -@Composable -private fun PositiveNoteCard( - footerMent: String?, - loading: Boolean, - modifier: Modifier = Modifier -) { - val shimmerBrush = rememberGrayShimmerBrush() - - Box( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(18.dp)) - .then( - if (loading) { - Modifier.background(shimmerBrush) - } else { - Modifier.background(Color(0xFFFBEEFF)) - } - ) - .padding(horizontal = 20.dp, vertical = 25.dp) - ) { - if (!loading) { - Text( - text = footerMent?.trim() - ?: "지금 떠오르지 않아도 괜찮아요.\n영감은 가끔, 쉬고 있을 때 더 잘 찾아오거든요.", - style = TextStyle( - fontSize = 14.sp, - lineHeight = 20.sp, - fontFamily = Paperlogy.font, - fontWeight = FontWeight(400), - color = Color(0xFF43454B) - ) - ) - } - } -} - -//감정 박스 스켈레놑+ 쉬머 -@Composable -private fun rememberGrayShimmerBrush(): Brush { - val transition = rememberInfiniteTransition(label = "gray_shimmer") - - val shift = transition.animateFloat( - initialValue = 0f, - targetValue = 1000f, - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 1400, - easing = LinearEasing - ) - ), - label = "shift" - ).value - - return Brush.linearGradient( - colors = listOf( - SkeletonStart, - SkeletonEnd, - SkeletonStart - ), - start = Offset(shift - 400f, 0f), - end = Offset(shift, 0f) - ) -} - -//인디케이터 + 스켈레톤 + 쉬머 -@Composable -private fun PagerIndicatorSkeleton() { - val shimmerBrush = rememberGrayShimmerBrush() - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 6.dp), - contentAlignment = Alignment.Center - ) { - Box( - modifier = Modifier - .width(54.dp) - .height(6.dp) - .clip(RoundedCornerShape(3.5.dp)) - .background(shimmerBrush) - ) - } -} - -@Composable -private fun PagerIndicator( - pageCount: Int, - currentPage: Int -) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - repeat(pageCount) { index -> - val selected = currentPage == index - - Box( - modifier = Modifier - .padding(horizontal = 4.dp) - .width(if (selected) 32.dp else 6.dp) - .height(6.dp) - .clip( - if (selected) RoundedCornerShape(3.5.dp) - else RoundedCornerShape(50) - ) - .background( - if (selected) Color(0xFFE5ACF4) - else Color(0xFFE9EAEE) - ) - ) - } - } -} - - - - - -@Preview(showBackground = true, showSystemUi = true) -@Composable -private fun PreviewCurationDetailScreen() { - val demo = listOf( - RecommendedLink( - isInternal = true, - userLinkuId = 11L, - title = "성신여대 홈페이지", - url = "https://www.sungshin.ac.kr/sites/main_kor/main.jsp", - imageUrl = null, - domain = "sungshin.ac.kr", - domainImageUrl = null, - categories = listOf("기타") - ), - RecommendedLink( - isInternal = false, - userLinkuId = null, - title = "나만의 자기관리 체크리스트 만들기 - Adobe", - url = "https://www.adobe.com/kr/acrobat/hub/create-a-self-care-checklist.html", - imageUrl = "https://picsum.photos/400/240", - domain = "adobe.com", - domainImageUrl = null, - categories = listOf("정보", "생산성") - ), - RecommendedLink( - isInternal = false, - userLinkuId = null, - title = "주간 읽을거리 모음", - url = "https://example.com/weekly", - imageUrl = null, - domain = null, - domainImageUrl = null, - categories = listOf("뉴스") - ) - ) - val demoDetail = CurationDetailUiState( - loading = false, - topTags = listOf("설렘", "통학 중", "공부 중"), - headerMent = "세나님의 하루가 반짝였던 순간이에요. 그 감정에 어울리는 콘텐츠를 추천해요.", - footerMent = "설렘은 가장 강력한 동기부여예요. 지금, 그 에너지를 믿어보세요." - ) - - - Surface { - CurationDetailScreenContent( - nickname = "세나", - monthLabel = "8월", - linksState = CurationLinksUiState(loading = false, items = demo), - detailState = demoDetail, - onBack = {}, - - ) - } -} - - - - - diff --git a/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationKeywordDetailScreen.kt b/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationKeywordDetailScreen.kt index 1dc74dc9..76b099ad 100644 --- a/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationKeywordDetailScreen.kt +++ b/feature/curation/src/main/java/com/linku/curation/ui/screen/CurationKeywordDetailScreen.kt @@ -15,8 +15,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -30,25 +28,21 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import com.linku.curation.CurationViewModel -import com.linku.curation.R import com.linku.curation.ui.effect.highlight.RadialGradientCircle import com.linku.design.theme.font.Paperlogy import com.linku.design.util.LocalFigmaDimens import com.linku.design.util.scaler -//큐레이션 두번째 카드 - 디테일 화면 제작중... + @Composable fun CurationKeywordDetailScreen( + nickname: String, viewModel: CurationViewModel = hiltViewModel(), onBack: () -> Unit = {}, onHome: () -> Unit = {} ) { - val nickname by viewModel.nickname.collectAsState() - val jobName by viewModel.jobName.collectAsState() - CurationKeywordDetailContent( nickname = nickname, - jobName = jobName, onBack = onBack, onHome = onHome ) @@ -57,7 +51,6 @@ fun CurationKeywordDetailScreen( @Composable private fun CurationKeywordDetailContent( nickname: String, - jobName: String, onBack: () -> Unit = {}, onHome: () -> Unit = {} ) { @@ -115,7 +108,7 @@ private fun CurationKeywordDetailContent( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "${nickname}님과 같은 ${jobName}들은\n이번 달, 이런 키워드를 많이 봤어요", + text = "${nickname}님은 \n이번 달, 이런 키워드를 많이 봤어요", style = TextStyle( fontSize = 20.sp, lineHeight = 25.sp, @@ -163,7 +156,6 @@ private fun CurationKeywordDetailScreenPreview() { ) { CurationKeywordDetailContent( nickname = "세나", - jobName = "직장인" // 프리뷰용 고정값 ) } } \ No newline at end of file diff --git a/feature/curation/src/main/java/com/linku/curation/ui/screen/detail/CurationMonthDetailOverlay.kt b/feature/curation/src/main/java/com/linku/curation/ui/screen/detail/CurationMonthDetailOverlay.kt deleted file mode 100644 index ef05da72..00000000 --- a/feature/curation/src/main/java/com/linku/curation/ui/screen/detail/CurationMonthDetailOverlay.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.linku.curation.ui.screen.detail - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.* -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.lerp -import coil3.compose.SubcomposeAsyncImage -import com.linku.curation.R -import com.linku.curation.ui.util.CurationConstants -import com.linku.design.util.scaler -import kotlinx.coroutines.delay - - - -@Composable -fun CurationMonthDetailOverlay( - page: Int, - imageUrl: String?, - onBack: () -> Unit -) { - var animationState by remember { mutableStateOf(null) } - var shouldClose by remember { mutableStateOf(false) } - - val animationProgress by animateFloatAsState( - targetValue = when (animationState) { - true -> 1f - false -> 0f - null -> 0f - }, - animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), - label = "progress", - finishedListener = { finalValue -> - if (finalValue == 0f && animationState == false) { - shouldClose = true - } - } - ) - - val contentProgress by animateFloatAsState( - targetValue = when (animationState) { - true -> 1f - else -> 0f - }, - animationSpec = tween( - durationMillis = 250, - delayMillis = if (animationState == true) 100 else 0, - easing = FastOutSlowInEasing - ), - label = "contentProgress" - ) - - LaunchedEffect(Unit) { - delay(20) - animationState = true - } - - LaunchedEffect(shouldClose) { - if (shouldClose) { - onBack() - } - } - - BackHandler { - if (animationState == true) { - animationState = false - } - } - - // 크기/위치 계산 - val startHeight = CurationConstants.CARD_HEIGHT_VALUE.scaler - val endHeight = CurationConstants.DETAIL_CARD_HEIGHT_VALUE.scaler - val listCardTopOffset = CurationConstants.CARD_TOP_OFFSET_VALUE.scaler - val startPadding = CurationConstants.CARD_HORIZONTAL_PADDING_VALUE.scaler - - val horizontalPadding = lerp(startPadding, 0.scaler, animationProgress) - val cardHeight = lerp(startHeight, endHeight, animationProgress) - val topOffset = lerp(listCardTopOffset, 0.scaler, animationProgress) - - val resolvedImageUrl = imageUrl?.takeIf { it.isNotBlank() && it != "null" } - - Box(modifier = Modifier.fillMaxSize()) { - - // 배경 오버레이 - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.White.copy(alpha = animationProgress * 0.97f)) - ) - - // 카드 이미지 - Box( - modifier = Modifier - .offset(y = topOffset) - .fillMaxWidth() - .padding(horizontal = horizontalPadding) - .height(cardHeight) - .clip(RoundedCornerShape(24.scaler)) - .background(Color(0xFFF2F2F2)) - ) { - - if (resolvedImageUrl == null) { - Image( - painter = painterResource(id = R.drawable.img_curation_example), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } else { - SubcomposeAsyncImage( - model = resolvedImageUrl, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } - } - - // 상세 콘텐츠 - Column( - modifier = Modifier - .fillMaxSize() - .padding(top = endHeight) - .offset(y = 60.scaler * (1f - contentProgress)) - .graphicsLayer { alpha = contentProgress } - .background(Color.White) - ) { - CurationMonthDetailContent(page = page) - - } - } -} - -@Composable -private fun CurationMonthDetailContent(page: Int) { - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - .padding(horizontal = 20.scaler) - ) { - Spacer(modifier = Modifier.height(20.scaler)) - Text( - text = "큐레이션 상세 화면 #$page", - color = Color.Black - ) - } -} \ No newline at end of file diff --git a/feature/curation/src/main/java/com/linku/curation/ui/screen/detail/CurationMonthDetailScreen.kt b/feature/curation/src/main/java/com/linku/curation/ui/screen/detail/CurationMonthDetailScreen.kt deleted file mode 100644 index 82a6ca7f..00000000 --- a/feature/curation/src/main/java/com/linku/curation/ui/screen/detail/CurationMonthDetailScreen.kt +++ /dev/null @@ -1,183 +0,0 @@ -// CurationMonthDetailScreen.kt -package com.linku.curation.ui.screen.detail - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.* -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.lerp -import coil3.compose.SubcomposeAsyncImage -import com.linku.curation.R -import com.linku.curation.ui.main_card.CurationCardItem -import com.linku.design.util.scaler -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import kotlinx.coroutines.delay - -@Composable -fun CurationMonthDetailScreen( - curationId: Long, - imageUrl: String?, - onBack: () -> Unit -) { - val systemUiController = rememberSystemUiController() - - // 애니메이션 상태: null = 초기, true = 확장, false = 축소(뒤로가기) - var animationState by remember { mutableStateOf(null) } - - // 뒤로가기 완료 플래그 - var shouldNavigateBack by remember { mutableStateOf(false) } - - // 애니메이션 진행률 (0 = 리스트 상태, 1 = 디테일 상태) - val animationProgress by animateFloatAsState( - targetValue = when (animationState) { - true -> 1f - false -> 0f - null -> 0f - }, - animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), - label = "progress", - finishedListener = { finalValue -> - if (finalValue == 0f && animationState == false) { - shouldNavigateBack = true - } - } - ) - - // 콘텐츠 슬라이드 애니메이션 - val contentProgress by animateFloatAsState( - targetValue = when (animationState) { - true -> 1f - else -> 0f - }, - animationSpec = tween( - durationMillis = 250, - delayMillis = if (animationState == true) 100 else 0, - easing = FastOutSlowInEasing - ), - label = "contentProgress" - ) - - // 진입 시 확장 애니메이션 시작 - LaunchedEffect(Unit) { - delay(20) - animationState = true - } - - // 실제 뒤로가기 처리 - LaunchedEffect(shouldNavigateBack) { - if (shouldNavigateBack) { - onBack() - } - } - - // StatusBar 설정 - SideEffect { - systemUiController.setStatusBarColor( - color = Color.Transparent, - darkIcons = true - ) - } - - // 뒤로가기 처리 (애니메이션 역재생) - BackHandler { - if (animationState == true) { - animationState = false - } - } - - // 크기/위치 계산 - val startHeight = 432.scaler - val endHeight = 350.scaler - - val horizontalPadding = lerp(33.scaler, 0.scaler, animationProgress) - val cardHeight = lerp(startHeight, endHeight, animationProgress) - - val listCardTopOffset = 180.scaler - val topOffset = lerp(listCardTopOffset, 0.scaler, animationProgress) - - // 배경 알파 - 뒤로가기 시 서서히 투명해짐 - val backgroundAlpha = animationProgress - - val resolvedImageUrl = imageUrl - ?.takeIf { it.isNotBlank() && it != "null" } - - Box(modifier = Modifier.fillMaxSize()) { - - // 배경 오버레이 (서서히 나타났다 사라짐) - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.White.copy(alpha = animationProgress)) - ) - - // 카드 이미지 영역 - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = horizontalPadding) - .offset(y = topOffset) - .height(cardHeight) - .clip(RoundedCornerShape(24.scaler)) - .background(Color(0xFFF2F2F2)) - ) { - if (resolvedImageUrl == null) { - Image( - painter = painterResource(id = R.drawable.img_curation_example), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } else { - SubcomposeAsyncImage( - model = resolvedImageUrl, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } - } - - // 상세 콘텐츠 (아래에서 위로 슬라이드 + 페이드) - Column( - modifier = Modifier - .fillMaxSize() - .padding(top = endHeight) - .offset(y = 60.scaler * (1f - contentProgress)) - .graphicsLayer { alpha = contentProgress } - .background(Color.White) - ) { - CurationMonthDetailContent(curationId = curationId) - } - } -} - - -@Composable -private fun CurationMonthDetailContent( - curationId: Long -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.scaler) - ) { - Spacer(modifier = Modifier.height(20.scaler)) - - Text( - text = "큐레이션 상세 화면 #$curationId", - color = Color.Black - ) - } -} \ No newline at end of file diff --git a/feature/curation/src/main/java/com/linku/curation/ui/util/CurationBackground.kt b/feature/curation/src/main/java/com/linku/curation/ui/util/CurationBackground.kt new file mode 100644 index 00000000..c7b96b5a --- /dev/null +++ b/feature/curation/src/main/java/com/linku/curation/ui/util/CurationBackground.kt @@ -0,0 +1,48 @@ +package com.linku.curation.ui.util + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.linku.curation.R +import com.linku.curation.ui.effect.highlight.RadialGradientCircle +import com.linku.design.theme.LinkuPreview +import com.linku.design.theme.linkuColors +import com.linku.design.util.scaler + +@Composable +fun CurationBackground( + showLogo: Boolean = false +) { + val colorTheme = MaterialTheme.linkuColors + + RadialGradientCircle( + color = colorTheme.accentColor, + modifier = Modifier.offset(x = (-88).scaler, y = (-12).scaler) + ) + RadialGradientCircle( + color = colorTheme.blue[200], + modifier = Modifier.offset(x = 24.scaler, y = (-6).scaler) + ) + if (showLogo) { + Image( + painter = painterResource(id = R.drawable.img_curation_logo), + contentDescription = null, + modifier = Modifier + .offset(x = 224.scaler, y = 95.scaler) + .size(width = 197.scaler, height = 140.scaler) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewCurationBackground() { + LinkuPreview { + CurationBackground(showLogo = true) + } +}