ekxkaks ekxkaks 09-08
모듈 분리
@f3862758f4100d80bfa126697fc584e739abe792
app/build.gradle.kts
--- app/build.gradle.kts
+++ app/build.gradle.kts
@@ -38,12 +38,16 @@
 dependencies {
     implementation(platform(libs.androidx.compose.bom))
     implementation(libs.androidx.activity.compose)
+    implementation(libs.androidx.compose.animation)
+    implementation(libs.androidx.compose.animation.core)
     implementation(libs.androidx.compose.material3)
     implementation(libs.androidx.compose.ui)
     implementation(libs.androidx.compose.ui.graphics)
     implementation(libs.androidx.compose.ui.tooling.preview)
     implementation(libs.androidx.core.ktx)
     implementation(libs.androidx.lifecycle.runtime.ktx)
+    implementation("io.coil-kt:coil-compose:2.6.0")
+    implementation("androidx.exifinterface:exifinterface:1.3.7")
     testImplementation(libs.junit)
     androidTestImplementation(platform(libs.androidx.compose.bom))
     androidTestImplementation(libs.androidx.compose.ui.test.junit4)
app/src/main/java/kr/co/app4ad/photoslide/MainActivity.kt
--- app/src/main/java/kr/co/app4ad/photoslide/MainActivity.kt
+++ app/src/main/java/kr/co/app4ad/photoslide/MainActivity.kt
@@ -1,21 +1,53 @@
 package kr.co.app4ad.photoslide
 
+import android.content.Context
+import android.net.Uri
 import android.os.Bundle
+import android.util.Log
 import androidx.activity.ComponentActivity
+import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.compose.setContent
 import androidx.activity.enableEdgeToEdge
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
 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.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
 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.material3.Button
+import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
 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 androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.exifinterface.media.ExifInterface
+import coil.compose.rememberAsyncImagePainter
+import coil.request.ImageRequest
 import kr.co.app4ad.photoslide.ui.theme.PhotoSlideTheme
 
 class MainActivity : ComponentActivity() {
@@ -24,45 +56,283 @@
         enableEdgeToEdge()
         setContent {
             PhotoSlideTheme {
-                Surface(modifier = Modifier.fillMaxSize(), color = Color.Black) {
-                    Box(
-                        modifier = Modifier.fillMaxSize(),
-                        contentAlignment = Alignment.Center
-                    ){
-                        Text(
-                            text = "포토 슬라이스 준비 중...",
-                            color = Color.White
-                        )
-                    }
+                Surface(
+                    modifier = Modifier.fillMaxSize(),
+                    color = Color.Black
+                ) {
+                    PhotoSlideApp()
                 }
             }
         }
     }
 }
+
+/**
+ * 1. 전체 앱 상태 관리 및 라우팅 담당 (State Holder)
+ */
 @Composable
-fun PhotoSlideScreen() {
+fun PhotoSlideApp() {
+    var selectedImageUris by remember { mutableStateOf<List<Uri>>(emptyList()) }
+    var currentIndex by remember { mutableIntStateOf(0) }
+
+    val photoPickerLauncher = rememberImagePickerLauncher { uris ->
+        if (uris.isNotEmpty()) {
+            selectedImageUris = uris
+            currentIndex = 0
+        }
+    }
+
     Box(
         modifier = Modifier.fillMaxSize(),
         contentAlignment = Alignment.Center
     ) {
-        // res/drawable에 넣은 이미지 파일 이름 (확장자 제외)
-        Image(
-            painter = painterResource(id = R.drawable.flow02),
-            contentDescription = "슬라이드쇼 샘플 이미지",
-            modifier = Modifier.fillMaxSize(),
-            contentScale = ContentScale.Fit        )
+        if (selectedImageUris.isEmpty()) {
+            EmptyPhotoView(
+                onPickClick = {
+                    photoPickerLauncher.launch(
+                        PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
+                    )
+                }
+            )
+        } else {
+            ActiveSlideView(
+                imageUris = selectedImageUris,
+                currentIndex = currentIndex,
+                onNextClick = {
+                    currentIndex = (currentIndex + 1) % selectedImageUris.size
+                },
+                onChangePhotosClick = {
+                    photoPickerLauncher.launch(
+                        PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
+                    )
+                }
+            )
+        }
     }
 }
 
-@Preview(showBackground = true)
+/**
+ * 2. 사진 불러오기 (Picker) 로직 모듈
+ */
 @Composable
-fun PhotoSlidePreview() {
-    PhotoSlideTheme {
-        Surface(
-            modifier = Modifier.fillMaxSize(),
-            color = Color.Black
+fun rememberImagePickerLauncher(onUrisSelected: (List<Uri>) -> Unit) =
+    rememberLauncherForActivityResult(
+        contract = ActivityResultContracts.PickMultipleVisualMedia(),
+        onResult = onUrisSelected
+    )
+
+/**
+ * 3. 사진이 없을 때 안내 및 선택 화면 모듈
+ */
+@Composable
+fun EmptyPhotoView(onPickClick: () -> Unit) {
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.Center,
+        modifier = Modifier.fillMaxSize()
+    ) {
+        Text(
+            text = "슬라이드쇼에 사용할 사진을 선택해 주세요",
+            color = Color.White,
+            modifier = Modifier.padding(bottom = 16.dp)
+        )
+        Button(onClick = onPickClick) {
+            Text("사진 여러 장 고르기")
+        }
+    }
+}
+/**
+ * 4. 사진 슬라이드 쇼 렌더링, 타이머 프로그레스바 및 전환 애니메이션 모듈
+ */
+@Composable
+fun ActiveSlideView(
+    imageUris: List<Uri>,
+    currentIndex: Int,
+    onNextClick: () -> Unit,
+    onChangePhotosClick: () -> Unit
+) {
+    val slideDuration = 10000L // 사진 한 장당 표시 시간 (3초)
+    val progress = remember { Animatable(0f) }
+
+    // 사진이 2장 이상일 때, 인덱스가 바뀔 때마다 0부터 1까지 차오르는 타이머 애니메이션 실행
+    LaunchedEffect(currentIndex, imageUris) {
+        if (imageUris.size > 1) {
+            progress.snapTo(0f)
+            progress.animateTo(
+                targetValue = 1f,
+                animationSpec = tween(durationMillis = slideDuration.toInt(), easing = LinearEasing)
+            )
+            onNextClick()
+        }
+    }
+
+    val currentUri = imageUris[currentIndex]
+
+    BoxWithConstraints(
+        modifier = Modifier
+            .fillMaxSize()
+            .background(Color.Black),
+        contentAlignment = Alignment.Center
+    ) {
+        val maxWidth = maxWidth
+        val maxHeight = maxHeight
+
+        // Crossfade를 적용하여 사진이 바뀔 때 부드럽게 페이드 인/아웃 되도록 전환 효과 부여
+        Crossfade(
+            targetState = currentUri,
+            animationSpec = tween(durationMillis = 1000), // 전환에 걸리는 시간 (0.6초)
+            label = "PhotoTransition"
+        ) { uri ->
+            PhotoImageView(
+                uri = uri,
+                maxWidth = maxWidth,
+                maxHeight = maxHeight
+            )
+        }
+
+        // 화면 상단에 남은 시간을 시각적으로 보여주는 프로그레스 인디케이터 바
+        if (imageUris.size > 1) {
+            LinearProgressIndicator(
+                progress = { progress.value },
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(4.dp)
+                    .align(Alignment.TopCenter),
+                color = Color.White,
+                trackColor = Color.Gray.copy(alpha = 0.3f)
+            )
+        }
+
+        SlideControlBar(
+            currentIndex = currentIndex,
+            totalSize = imageUris.size,
+            onNextClick = onNextClick,
+            onChangePhotosClick = onChangePhotosClick
+        )
+    }
+}
+
+/**
+ * 5. 단일 사진 렌더링, getPhotoOrientation 함수 반영 및 오버레이 표시 모듈
+ */
+@Composable
+fun PhotoImageView(
+    uri: Uri,
+    maxWidth: Dp,
+    maxHeight: Dp
+) {
+    val context = LocalContext.current
+
+    // getPhotoOrientation 함수를 사용하여 회전 각도 추출
+    var rotation = remember(uri) {
+        getPhotoOrientation(context, uri)
+    }
+    rotation = 0F
+    Log.d ("tag rotation" +
+            "", "rotation = $rotation" )
+    // EXIF orientation 값을 로그 또는 디버그 표시용으로 읽기 위한 추가 로직
+    val orientationVal = remember(uri) {
+        try {
+            context.contentResolver.openInputStream(uri)?.use { inputStream ->
+                ExifInterface(inputStream).getAttributeInt(
+                    ExifInterface.TAG_ORIENTATION,
+                    ExifInterface.ORIENTATION_NORMAL
+                )
+            } ?: 1
+        } catch (e: Exception) {
+            1
+        }
+    }
+
+    val painter = rememberAsyncImagePainter(
+        model = ImageRequest.Builder(context)
+            .data(uri)
+            .crossfade(true)
+            .build()
+    )
+
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .background(Color.Black),
+        contentAlignment = Alignment.Center
+    ) {
+        val isRotated = rotation == 90f || rotation == 270f
+
+        Image(
+            painter = painter,
+            contentDescription = "슬라이드 사진",
+            modifier = Modifier
+                .fillMaxSize()
+                .rotate(rotation),
+            contentScale = if (isRotated) ContentScale.Inside else ContentScale.Fit
+        )
+
+        // 사진 위에 orientation 값과 회전 각도 표시
+        Box(
+            modifier = Modifier
+                .align(Alignment.TopStart)
+                .padding(16.dp)
+                .background(Color.Black.copy(alpha = 0.6f))
+                .padding(horizontal = 8.dp, vertical = 4.dp)
         ) {
-            PhotoSlideScreen()
+            Text(
+                text = "Orientation: $orientationVal (Rotation: ${rotation.toInt()}°)",
+                color = Color.White,
+                fontSize = 14.sp
+            )
+        }
+    }
+}
+/**
+ * 5-1 EXIF 정보로 회전각 읽어오기
+ */
+fun getPhotoOrientation(context: Context, uri: Uri): Float {
+    return try {
+        context.contentResolver.openInputStream(uri)?.use { inputStream ->
+            val exif = ExifInterface(inputStream)
+            val orientation = exif.getAttributeInt(
+                ExifInterface.TAG_ORIENTATION,
+                ExifInterface.ORIENTATION_NORMAL
+            )
+
+            when (orientation) {
+                ExifInterface.ORIENTATION_ROTATE_90 -> 90f
+                ExifInterface.ORIENTATION_ROTATE_180 -> 180f
+                ExifInterface.ORIENTATION_ROTATE_270 -> 270f
+                else -> 0f
+            }
+        } ?: 0f
+    } catch (e: Exception) {
+        0f
+    }
+}
+/**
+ * 6. 하단 제어 바 (사진 전환 및 재선택 버튼) 모듈
+ */
+@Composable
+fun SlideControlBar(
+    currentIndex: Int,
+    totalSize: Int,
+    onNextClick: () -> Unit,
+    onChangePhotosClick: () -> Unit
+) {
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .padding(16.dp),
+        contentAlignment = Alignment.BottomCenter
+    ) {
+        Row(
+            horizontalArrangement = Arrangement.spacedBy(8.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Button(onClick = onChangePhotosClick) {
+                Text("사진 다시 고르기")
+            }
+            Button(onClick = onNextClick) {
+                Text("다음 사진 (${currentIndex + 1}/$totalSize)")
+            }
         }
     }
 }
(No newline at end of file)
gradle/libs.versions.toml
--- gradle/libs.versions.toml
+++ gradle/libs.versions.toml
@@ -8,6 +8,8 @@
 activityCompose = "1.8.0"
 kotlin = "2.2.10"
 composeBom = "2026.02.01"
+animationCore = "1.12.0"
+animation = "1.12.0"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -24,6 +26,8 @@
 androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
 androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
 androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-compose-animation-core = { group = "androidx.compose.animation", name = "animation-core", version.ref = "animationCore" }
+androidx-compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "animation" }
 
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }
Add a comment
List