diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0a32bb0..54fe210 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -99,6 +99,11 @@ dependencies { implementation(libs.androidx.media3.datasource) implementation(libs.androidx.media3.datasource.okhttp) + //Filament + implementation("com.google.android.filament:filament-android:1.57.1") + implementation("com.google.android.filament:gltfio-android:1.57.1") + implementation("com.google.android.filament:filament-utils-android:1.57.1") + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b7fb28a..a6eba29 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + +} \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/growth/GrowthResponseDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/growth/GrowthResponseDto.kt new file mode 100644 index 0000000..cb1741d --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/growth/GrowthResponseDto.kt @@ -0,0 +1,16 @@ +package com.ssafy.tiggle.data.model.growth + +import com.ssafy.tiggle.domain.entity.growth.GrowthResult + +data class GrowthResponseDto ( + val totalAmount:Long=0L, + val toNextLevel:Int=0, + val level:Int=0 +) + +fun GrowthResponseDto.toDomain(): GrowthResult = + GrowthResult( + totalAmount=totalAmount, + toNextLevel=toNextLevel, + level=level + ) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/data/repository/GrowthRepositoryImpl.kt b/app/src/main/java/com/ssafy/tiggle/data/repository/GrowthRepositoryImpl.kt new file mode 100644 index 0000000..242fe4d --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/repository/GrowthRepositoryImpl.kt @@ -0,0 +1,33 @@ +package com.ssafy.tiggle.data.repository + +import com.ssafy.tiggle.data.datasource.remote.GrowthApiService +import com.ssafy.tiggle.data.datasource.remote.UserApiService +import com.ssafy.tiggle.data.model.growth.toDomain +import com.ssafy.tiggle.data.model.piggybank.response.toDomain +import com.ssafy.tiggle.domain.repository.UserRepository +import com.ssafy.tiggle.domain.entity.dutchpay.UserSummary +import com.ssafy.tiggle.domain.entity.growth.GrowthResult +import com.ssafy.tiggle.domain.repository.GrowthRepository +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GrowthRepositoryImpl @Inject constructor( + private val growthApiService: GrowthApiService +) : GrowthRepository { + + override suspend fun getGrowthResult(): Result { + return try { + val response = growthApiService.getGrowthResult() + + if (response.result && response.data != null) { + Result.success(response.data.toDomain()) + } else { + Result.failure(Exception(response.message ?: "성장 정보 불러오기에 실패했습니다.")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + +} diff --git a/app/src/main/java/com/ssafy/tiggle/di/NetworkModule.kt b/app/src/main/java/com/ssafy/tiggle/di/NetworkModule.kt index c025fb6..149b77f 100644 --- a/app/src/main/java/com/ssafy/tiggle/di/NetworkModule.kt +++ b/app/src/main/java/com/ssafy/tiggle/di/NetworkModule.kt @@ -8,6 +8,7 @@ import com.ssafy.tiggle.data.datasource.remote.AuthApiService import com.ssafy.tiggle.data.datasource.remote.DonationApiService import com.ssafy.tiggle.data.datasource.remote.DutchPayApiService import com.ssafy.tiggle.data.datasource.remote.FcmApiService +import com.ssafy.tiggle.data.datasource.remote.GrowthApiService import com.ssafy.tiggle.data.datasource.remote.PiggyBankApiService import com.ssafy.tiggle.data.datasource.remote.UniversityApiService import com.ssafy.tiggle.data.datasource.remote.UserApiService @@ -148,4 +149,9 @@ object NetworkModule { @Singleton fun provideDonationApiService(retrofit: Retrofit): DonationApiService = retrofit.create(DonationApiService::class.java) + + @Provides + @Singleton + fun provideGrowthService(retrofit: Retrofit): GrowthApiService = + retrofit.create(GrowthApiService::class.java) } diff --git a/app/src/main/java/com/ssafy/tiggle/di/RepositoryModule.kt b/app/src/main/java/com/ssafy/tiggle/di/RepositoryModule.kt index 2ef73f7..15fdf1a 100644 --- a/app/src/main/java/com/ssafy/tiggle/di/RepositoryModule.kt +++ b/app/src/main/java/com/ssafy/tiggle/di/RepositoryModule.kt @@ -4,6 +4,7 @@ import com.ssafy.tiggle.data.repository.AuthRepositoryImpl import com.ssafy.tiggle.data.repository.DonationRepositoryImpl import com.ssafy.tiggle.data.repository.DutchPayRepositoryImpl import com.ssafy.tiggle.data.repository.FcmRepositoryImpl +import com.ssafy.tiggle.data.repository.GrowthRepositoryImpl import com.ssafy.tiggle.data.repository.PiggyBankRepositoryImpl import com.ssafy.tiggle.data.repository.UniversityRepositoryImpl import com.ssafy.tiggle.data.repository.UserRepositoryImpl @@ -11,6 +12,7 @@ import com.ssafy.tiggle.domain.repository.AuthRepository import com.ssafy.tiggle.domain.repository.DonationRepository import com.ssafy.tiggle.domain.repository.DutchPayRepository import com.ssafy.tiggle.domain.repository.FcmRepository +import com.ssafy.tiggle.domain.repository.GrowthRepository import com.ssafy.tiggle.domain.repository.PiggyBankRepository import com.ssafy.tiggle.domain.repository.UniversityRepository import com.ssafy.tiggle.domain.repository.UserRepository @@ -69,4 +71,10 @@ abstract class RepositoryModule { abstract fun bindDonationRepository( donationRepositoryImpl: DonationRepositoryImpl ): DonationRepository + + @Binds + @Singleton + abstract fun bindGrowthRepository( + growthRepositoryImpl: GrowthRepositoryImpl + ): GrowthRepository } diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/growth/GrowthResult.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/growth/GrowthResult.kt new file mode 100644 index 0000000..a35b41f --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/growth/GrowthResult.kt @@ -0,0 +1,7 @@ +package com.ssafy.tiggle.domain.entity.growth + +data class GrowthResult ( + val totalAmount:Long=0L, + val toNextLevel:Int=0, + val level:Int=0 +) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/domain/repository/GrowthRepository.kt b/app/src/main/java/com/ssafy/tiggle/domain/repository/GrowthRepository.kt new file mode 100644 index 0000000..a7ee8f1 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/repository/GrowthRepository.kt @@ -0,0 +1,8 @@ +package com.ssafy.tiggle.domain.repository + +import com.ssafy.tiggle.domain.entity.growth.GrowthResult + +interface GrowthRepository { + + suspend fun getGrowthResult(): Result +} diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/GetGrowthResultUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/GetGrowthResultUseCase.kt new file mode 100644 index 0000000..2039cd0 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/GetGrowthResultUseCase.kt @@ -0,0 +1,15 @@ +package com.ssafy.tiggle.domain.usecase.growth + +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayRequest +import com.ssafy.tiggle.domain.entity.growth.GrowthResult +import com.ssafy.tiggle.domain.repository.DutchPayRepository +import com.ssafy.tiggle.domain.repository.GrowthRepository +import javax.inject.Inject + +class GetGrowthResultUseCase @Inject constructor( + private val growthRepository: GrowthRepository +) { + suspend operator fun invoke(): Result { + return growthRepository.getGrowthResult() + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/GrowthUseCases.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/GrowthUseCases.kt new file mode 100644 index 0000000..f6f0bb9 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/growth/GrowthUseCases.kt @@ -0,0 +1,7 @@ +package com.ssafy.tiggle.domain.usecase.growth + +import javax.inject.Inject + +data class GrowthUseCases @Inject constructor( + val getGrowthResultUseCase: GetGrowthResultUseCase +) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/Character3D.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/Character3D.kt new file mode 100644 index 0000000..a625f2d --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/Character3D.kt @@ -0,0 +1,328 @@ +package com.ssafy.tiggle.presentation.ui.growth + +import android.annotation.SuppressLint +import android.content.Context +import android.view.Choreographer +import android.view.SurfaceView +import android.view.MotionEvent +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import com.google.android.filament.utils.ModelViewer +import com.google.android.filament.utils.Utils +import com.google.android.filament.IndirectLight +import com.google.android.filament.gltfio.ResourceLoader +import android.graphics.PixelFormat +import android.util.Log +import com.google.android.filament.EntityManager +import com.google.android.filament.LightManager +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.math.PI + +/** + * 레벨에 따라 다른 GLB를 로드하는 3D 캐릭터 컴포저블 + */ +// 전역 변수 +private var baseTransform: FloatArray? = null +// 애니메이션 자동재생용 상태 +private var animIndex: Int = -1 +private var animDurationSec: Float = 0f +private var animStartNanos: Long = -1L + +@SuppressLint("ClickableViewAccessibility") +@Composable +fun Character3D( + level: Int, + modifier: Modifier = Modifier, + enableOrbit: Boolean = true, +) { + val context = LocalContext.current + var modelViewer by remember { mutableStateOf(null) } + var currentLevel by remember { mutableStateOf(level) } + + // 렌더링 프레임 콜백 + val choreographer = remember { Choreographer.getInstance() } + val frameCallback = remember { + object : Choreographer.FrameCallback { + override fun doFrame(frameTimeNanos: Long) { + // ── 애니메이션이 있으면 시간계산해서 적용 ── + modelViewer?.let { mv -> + val animator = mv.animator + if (animIndex >= 0 && animator != null && animator.animationCount > 0) { + if (animStartNanos < 0L) animStartNanos = frameTimeNanos + val tSec = ((frameTimeNanos - animStartNanos) / 1_000_000_000.0f) + // duration이 0일 가능성 방지 + val dur = if (animDurationSec > 1e-4f) animDurationSec else 1f + val loopTime = (tSec % dur) + animator.applyAnimation(animIndex, loopTime) + animator.updateBoneMatrices() + } + } + modelViewer?.render(frameTimeNanos) + choreographer.postFrameCallback(this) + } + } + } + + AndroidView( + modifier = modifier, + factory = { ctx -> + val surfaceView = SurfaceView(ctx) + + // SurfaceView 투명 설정 + surfaceView.holder.setFormat(android.graphics.PixelFormat.TRANSLUCENT) + surfaceView.setZOrderOnTop(false) // 다른 뷰 뒤에 배치 + + try { + // Filament 초기화 + Utils.init() + + // ModelViewer 생성 (기본 생성자 사용) + val viewer = ModelViewer(surfaceView) + modelViewer = viewer + + // 투명 배경 설정 + setupTransparentBackground(viewer) + + // 개선된 조명 설정 + setupFrontLight(viewer) + + // 터치 이벤트 처리 - 직접 구현 + if (enableOrbit) { + enableHorizontalDragRotation(surfaceView, viewer) + } + + // 모델 로드 + loadModelForLevel(ctx, viewer, level) + + // 렌더링 시작 + choreographer.postFrameCallback(frameCallback) + + } catch (e: Exception) { + e.printStackTrace() + } + + surfaceView + }, + update = { _ -> + // 레벨 변경 시 모델 재로드 + if (currentLevel != level) { + currentLevel = level + modelViewer?.let { viewer -> + loadModelForLevel(context, viewer, level) + } + } + }, + onRelease = { + choreographer.removeFrameCallback(frameCallback) + modelViewer = null + } + ) +} +private fun setupFrontLight(modelViewer: ModelViewer) { + // 배경은 투명/스카이박스 없음 (필요시 색만 바꾸세요) + modelViewer.scene.skybox = null + modelViewer.scene.indirectLight = null // ✅ 간접광도 제거 (정면 라이트만) + + // 기존에 씬에 있던 라이트가 있다면 정리(선택) + // Filament는 라이트 엔티티를 추적하지 않으므로, + // 새 씬이 아니라면 별도 관리가 필요합니다. 일단 추가만 하는 상황이면 생략 가능. + + // 정면에서 살짝 내려 비추는 한 개의 방향광 + val key = EntityManager.get().create() + LightManager.Builder(LightManager.Type.DIRECTIONAL) + .color(1.0f, 1.0f, 1.0f) // 순백색 라이트 + .intensity(200_000f) // 밝기 (필요하면 80k~200k 사이로 조절) + .direction(0f, -0.2f, -1f) // ✅ 화면 정면(–Z)에서 약간 아래로 + .castShadows(false) // 그림자 비활성화 + .build(modelViewer.engine, key) + modelViewer.scene.addEntity(key) +} +/** + * 투명 배경 설정 + */ +private fun setupTransparentBackground(modelViewer: ModelViewer) { + try { + modelViewer.scene.skybox = null + modelViewer.view.blendMode = com.google.android.filament.View.BlendMode.TRANSLUCENT + + val clearOptions = modelViewer.renderer.clearOptions + clearOptions.clear = true + // 투명 대신 흰색으로 설정 + clearOptions.clearColor = floatArrayOf( + 0.94f, // 거의 흰색에 가까운 + 0.98f, + 1.0f, + 1.0f + ) + modelViewer.renderer.clearOptions = clearOptions + + } catch (e: Exception) { + e.printStackTrace() + } +} + +/** + * 레벨에 맞는 모델 로드 (안전한 방식) + */ +private fun loadModelForLevel(context: Context, modelViewer: ModelViewer, level: Int) { + try { + val assetPath = LevelModels.assetPathFor(level) + val buffer = readAssetFile(context, assetPath) ?: return + + modelViewer.loadModelGlb(buffer) + + // 리소스 로더 호출 (텍스처/머티리얼 GPU 업로드) + modelViewer.asset?.let { asset -> + ResourceLoader(modelViewer.engine).loadResources(asset) + } + val asset = modelViewer.asset + val animator = modelViewer.animator + val animationCount = animator?.animationCount ?: 0 + val hasAnimation = animationCount > 0 + + if (hasAnimation) { + // 애니메이션이 있으니까 크기 줄이기 + val tm = modelViewer.engine.transformManager + val root = asset!!.root + val rootInst = tm.getInstance(root) + + val scale = 0.28f + val translateY = -0.20f + + // column-major 4x4, translation은 [12],[13],[14] 슬롯 + val trs = floatArrayOf( + scale, 0f, 0f, 0f, + 0f, scale,0f, 0f, + 0f, 0f, scale,0f, + 0f, translateY, 0f, 1f + ) + tm.setTransform(rootInst, trs) + + // 공통 프레임 루프에서 돌리도록 애니메이션 정보만 세팅 + animIndex = 0 + animDurationSec = animator?.getAnimationDuration(0) ?: 1f + animStartNanos = -1L // 다음 프레임에서 시작점 리셋 + + + var animationTime = 0f + val choreographer = Choreographer.getInstance() + val cb = object : Choreographer.FrameCallback { + override fun doFrame(frameTimeNanos: Long) { + animationTime += 0.016f // 프레임당 약 16ms 진행 + + animator?.applyAnimation(0, animationTime) // 0번 애니메이션 적용 + animator?.updateBoneMatrices() + + modelViewer.render(frameTimeNanos) + choreographer.postFrameCallback(this) + } + } + choreographer.postFrameCallback(cb) + } else { + // 기존처럼 모델 크기 정규화 + modelViewer.transformToUnitCube() + } + + // root 트랜스폼 저장 + modelViewer.asset?.let { asset -> + val tm = modelViewer.engine.transformManager + val ti = tm.getInstance(asset.root) + if (ti != 0) { + baseTransform = FloatArray(16) + tm.getTransform(ti, baseTransform) + } + } + + } catch (e: Exception) { + e.printStackTrace() + } +} + + +/** + * assets에서 GLB 파일 읽기 + */ +private fun readAssetFile(context: Context, path: String): ByteBuffer? { + return try { + context.assets.open(path).use { inputStream -> + val bytes = ByteArray(inputStream.available()) + inputStream.read(bytes) + + ByteBuffer.allocateDirect(bytes.size).apply { + order(ByteOrder.nativeOrder()) + put(bytes) + rewind() + } + } + } catch (e: IOException) { + e.printStackTrace() + null + } +} + +private var lastTouchX = 0f +private var accumulatedRotation = 0f // 누적 회전 각도 + +@SuppressLint("ClickableViewAccessibility") +private fun enableHorizontalDragRotation(surfaceView: SurfaceView, modelViewer: ModelViewer) { + val engine = modelViewer.engine + val tm = engine.transformManager + + surfaceView.setOnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + lastTouchX = event.x + } + MotionEvent.ACTION_MOVE -> { + val dx = event.x - lastTouchX + lastTouchX = event.x + + val deltaAngle = dx * 0.5f + accumulatedRotation += deltaAngle + + modelViewer.asset?.let { asset -> + val root = asset.root + val ti = tm.getInstance(root) + if (ti != 0 && baseTransform != null) { + // 드래그 MOVE에서 + val radians = (accumulatedRotation * PI / 180f).toFloat() + + // Y축 회전 행렬을 floatArray로 직접 만들기 + val cos = kotlin.math.cos(radians) + val sin = kotlin.math.sin(radians) + val rotMat = floatArrayOf( + cos, 0f, -sin, 0f, + 0f, 1f, 0f, 0f, + sin, 0f, cos, 0f, + 0f, 0f, 0f, 1f + ) + + // baseTransform × rotMat + val finalMat = multiplyMat4(baseTransform!!, rotMat) + tm.setTransform(ti, finalMat) + } + } + } + } + true + } +} + +// 행렬 곱 (4x4) : a * b +private fun multiplyMat4(a: FloatArray, b: FloatArray): FloatArray { + val out = FloatArray(16) + for (i in 0..3) { + for (j in 0..3) { + var sum = 0f + for (k in 0..3) { + sum += a[i + k*4] * b[k + j*4] + } + out[i + j*4] = sum + } + } + return out +} \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthScreen.kt index 80e89c8..9c7d5a0 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthScreen.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthScreen.kt @@ -8,6 +8,7 @@ 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -40,6 +41,7 @@ import com.ssafy.tiggle.presentation.ui.theme.AppTypography import com.ssafy.tiggle.presentation.ui.theme.TiggleBlue import com.ssafy.tiggle.presentation.ui.theme.TiggleGrayLight import com.ssafy.tiggle.presentation.ui.theme.TiggleGrayText +import com.ssafy.tiggle.presentation.ui.theme.TiggleSkyBlue @Composable fun GrowthScreen( @@ -80,9 +82,7 @@ fun GrowthScreen( // 성장 카드 (아이콘들 포함) GrowthCard( - totalAmount = uiState.totalDonationAmount, - nextGoalAmount = uiState.nextGoalAmount, - currentLevel = uiState.currentLevel, + uiState=uiState, onDonationHistoryClick = onDonationHistoryClick, onDonationStatusClick = onDonationStatusClick, onDonationRankingClick = onDonationRankingClick @@ -152,22 +152,21 @@ private fun GrowthIconItem( @Composable private fun GrowthCard( - totalAmount: Int, - nextGoalAmount: Int, - currentLevel: String, + uiState: GrowthUiState, onDonationHistoryClick: () -> Unit, onDonationStatusClick: () -> Unit, - onDonationRankingClick: () -> Unit + onDonationRankingClick: () -> Unit, ) { Card( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxSize(), shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = TiggleGrayLight), + colors = CardDefaults.cardColors(containerColor = TiggleSkyBlue), elevation = CardDefaults.cardElevation(0.dp) ) { Column( modifier = Modifier .fillMaxWidth() + .fillMaxHeight() .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -181,21 +180,8 @@ private fun GrowthCard( Spacer(Modifier.height(24.dp)) // 캐릭터 이미지 (현재는 비워둠) - Box( - modifier = Modifier - .size(200.dp) - .background( - Color.White.copy(alpha = 0.5f), - RoundedCornerShape(16.dp) - ), - contentAlignment = Alignment.Center - ) { - // TODO: 캐릭터 이미지 추가 - Text( - text = "캐릭터 영역", - color = TiggleGrayText, - fontSize = 14.sp - ) + Box(Modifier.fillMaxWidth().height(500.dp).background(Color.Transparent) ) { + Character3D(level = uiState.growth.level, modifier = Modifier.fillMaxSize()) } Spacer(Modifier.height(20.dp)) @@ -205,7 +191,7 @@ private fun GrowthCard( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "레벨 7", + text = "레벨 ${uiState.growth.level+1}", fontSize = 14.sp, color = TiggleGrayText, modifier = Modifier @@ -219,7 +205,7 @@ private fun GrowthCard( Spacer(Modifier.width(12.dp)) Text( - text = currentLevel, + text = "쏠", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = TiggleBlue @@ -230,7 +216,7 @@ private fun GrowthCard( // 총 티끌 금액 Text( - text = "총 티끌: ${Formatter.formatCurrency(totalAmount.toLong())}", + text = "총 티끌: ${Formatter.formatCurrency(uiState.growth.totalAmount)}", fontSize = 16.sp, color = Color.Black, fontWeight = FontWeight.Medium @@ -253,7 +239,7 @@ private fun GrowthCard( // 다음 레벨까지 필요한 금액 Text( - text = "다음 레벨까지 ${Formatter.formatCurrency(nextGoalAmount.toLong())}", + text = "다음 레벨까지 ${Formatter.formatCurrency(uiState.growth.toNextLevel.toLong())}", fontSize = 12.sp, color = TiggleGrayText ) diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthUiState.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthUiState.kt index 218fe62..3297df6 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthUiState.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthUiState.kt @@ -1,13 +1,13 @@ package com.ssafy.tiggle.presentation.ui.growth +import com.ssafy.tiggle.domain.entity.growth.GrowthResult + /** * 성장 화면의 UI 상태 */ data class GrowthUiState( val isLoading: Boolean = false, - val totalDonationAmount: Int = 17800, // 총 티끌 금액 - val nextGoalAmount: Int = 2500, // 다음 레벨까지 필요한 금액 - val currentLevel: String = "쓸", // 현재 레벨 + val growth: GrowthResult= GrowthResult(), val characterStatus: String = "행복", // 캐릭터 상태 val donationHistory: List = emptyList(), val donationRanking: List = emptyList(), diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthViewModel.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthViewModel.kt index 2081b7b..1c24089 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthViewModel.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/GrowthViewModel.kt @@ -1,14 +1,21 @@ package com.ssafy.tiggle.presentation.ui.growth import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ssafy.tiggle.domain.usecase.growth.GrowthUseCases import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import retrofit2.HttpException import javax.inject.Inject @HiltViewModel -class GrowthViewModel @Inject constructor() : ViewModel() { +class GrowthViewModel @Inject constructor( + val growthUseCases: GrowthUseCases +) : ViewModel() { private val _uiState = MutableStateFlow(GrowthUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -19,16 +26,34 @@ class GrowthViewModel @Inject constructor() : ViewModel() { } private fun loadGrowthData() { - // TODO: 실제 API에서 데이터 로드 - // 현재는 더미 데이터 사용 - _uiState.value = _uiState.value.copy( - totalDonationAmount = 17800, - nextGoalAmount = 2500, - currentLevel = "쓸", - characterStatus = "행복" - ) + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + + val result = growthUseCases.getGrowthResultUseCase() + + result + .onSuccess { growth -> + _uiState.update { + it.copy( + isLoading = false, + growth = growth, + characterStatus = "행복" + ) + } + } + .onFailure { e -> + val isNotFound = (e is HttpException && e.code() == 404) + _uiState.update { + it.copy( + isLoading = false, + errorMessage = e.message ?: "성장 조회에 실패했습니다." + ) + } + } + } } - + + fun onDonationHistoryClick() { // TODO: 기부 기록 화면으로 이동 } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/LevelModels.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/LevelModels.kt new file mode 100644 index 0000000..c9277cd --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/growth/LevelModels.kt @@ -0,0 +1,24 @@ +package com.ssafy.tiggle.presentation.ui.growth + +import android.content.ContentValues.TAG +import android.util.Log + +object LevelModels { + fun assetPathFor(level: Int): String { + val bucket = when { + level ==0 -> "level_1.glb" + level ==1 -> "level_2.glb" + level ==2 -> "level_3.glb" + else -> "level_4.glb" + } + return "models/$bucket" + } + + // 필요 시 스케일/초기 카메라 등도 레벨별로 다르게 + fun scaleFor(level: Int): Float = when { + level < 5 -> 1.0f + level < 10 -> 1.0f + level < 20 -> 1.1f + else -> 1.2f + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 12cfa85..4fbfc50 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,9 @@ material3 = "1.3.2" navigation3Runtime = "1.0.0-alpha06" lifecycleViewmodelNavigation3Android = "1.0.0-alpha04" roomKtx = "2.7.2" - +filament = "1.57.1" +gltfioAndroid = "1.57.1" +filamentAndroid = "1.17.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3Exoplayer" } @@ -75,6 +77,11 @@ androidx-foundation = { group = "androidx.compose.foundation", name = "foundatio material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" } +#Filament +filament-android = { group = "com.google.android.filament", name = "filament-android", version.ref = "filament" } +filament-utils-android = { group = "com.google.android.filament", name = "filament-utils-android", version.ref = "filament" } +gltfio-android = { group = "com.google.android.filament", name = "gltfio-android", version.ref = "filament" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }