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" }