diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..24636cb --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,34 @@ +version: 1 + +reviews: + auto_review: true # 자동 리뷰 활성화 + review_status: true # 체크 메시지 표시 + max_review_comments: 50 + + # 모든 브랜치 PR에 대해 자동으로 리뷰 + triggers: + pull_request: true # PR 생성 시 자동 리뷰 + pull_request_branches: # 어떤 브랜치든지 허용 + include: + - "*" # 모든 브랜치 + push: false + pull_request_review: false + +style: + tone: professional + detail_level: medium + include_line_comments: true + +exclude: + paths: + - "*.md" + - "*.png" + - "*.jpg" + - "*.gif" + - "*.svg" + - "build/**" + - "gradle/**" + +model: + provider: openai + name: gpt-4o-mini diff --git a/BRANCH_DIFFERENCES.md b/BRANCH_DIFFERENCES.md new file mode 100644 index 0000000..b86a660 --- /dev/null +++ b/BRANCH_DIFFERENCES.md @@ -0,0 +1,304 @@ +# practice-only 브랜치와 JeongIlhyuk/week7 브랜치 차이점 정리 + +## 변경 요약 +- **추가된 파일**: 4개 +- **수정된 파일**: 10개 +- **삭제된 파일**: 1개 + +--- + +## 1. 추가된 파일 (A) + +### 1.1 `app/src/main/java/com/example/kuit6_android_api/di/AppContainer.kt` +**새로 생성된 파일** +- 의존성 주입을 위한 AppContainer 클래스 추가 +- ApiService와 PostRepository를 한 곳에서 관리 +- Repository 패턴 구현을 위한 의존성 관리 + +**주요 내용:** +```kotlin +class AppContainer { + val apiService: ApiService = RetrofitClient.apiService + val postRepository: PostRepository = PostRepositoryImpl(apiService) +} +``` + +### 1.2 `app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt` +**새로 생성된 파일** +- PostViewModel을 기능별로 분리한 ViewModel 중 하나 +- 게시글 생성 관련 로직 담당 +- PostCreateUiState를 통한 상태 관리 + +**주요 기능:** +- `createPost()`: 게시글 생성 +- `uploadImage()`: 이미지 업로드 +- `clearUploadedImageUrl()`: 업로드된 이미지 URL 초기화 + +### 1.3 `app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt` +**새로 생성된 파일** +- 게시글 상세 조회 및 삭제 관련 ViewModel +- PostDetailUiState를 통한 상태 관리 + +**주요 기능:** +- `getPostDetail()`: 게시글 상세 조회 +- `deletePost()`: 게시글 삭제 + +### 1.4 `app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt` +**새로 생성된 파일** +- 게시글 수정 관련 ViewModel +- PostEditUiState를 통한 상태 관리 + +**주요 기능:** +- `getPostDetail()`: 게시글 상세 조회 +- `updatePost()`: 게시글 수정 +- `uploadImage()`: 이미지 업로드 +- `clearUploadedImageUrl()`: 업로드된 이미지 URL 초기화 + +--- + +## 2. 삭제된 파일 (D) + +### 2.1 `app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt` +**삭제된 파일** +- 기존의 통합 PostViewModel이 기능별로 분리됨 +- PostCreateViewModel, PostDetailViewModel, PostEditViewModel로 분리 +- ApiService를 직접 참조하던 구조에서 Repository 패턴으로 변경 + +--- + +## 3. 수정된 파일 (M) + +### 3.1 `app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt` + +**변경 사항:** +1. **Import 추가:** + - `PostCreateRequest` 추가 + - `okhttp3.MultipartBody` 추가 + +2. **인터페이스 포맷팅:** + - `PostRepository{` → `PostRepository {` (공백 추가) + +3. **메서드 추가:** + - `getPostDetail(postId: Long): Result` - 게시글 상세 조회 + - `createPost(author, title, content, imageUrl): Result` - 게시글 생성 + - `updatePost(postId, title, content, imageUrl): Result` - 게시글 수정 + - `deletePost(postId: Long): Result` - 게시글 삭제 + - `uploadImage(file: MultipartBody.Part): Result` - 이미지 업로드 + +4. **파일 끝 개행 추가** + +### 3.2 `app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt` + +**변경 사항:** +1. **Import 추가:** + - `PostCreateRequest` 추가 + - `BaseResponse` 추가 + - `okhttp3.MultipartBody` 추가 + +2. **클래스 포맷팅:** + - `PostRepositoryImpl (` → `PostRepositoryImpl(` (공백 제거) + - `): PostRepository {` → `) : PostRepository {` (공백 추가) + +3. **getPosts() 메서드 개선:** + - 반환 타입 명시: `BaseResponse>` + - 에러 메시지 변경: "게시긆 불러오기 실패" → "게시글 목록 조회 실패" + - 포맷팅 개선 (공백 추가) + +4. **새로운 메서드 구현 추가:** + - `getPostDetail()`: 게시글 상세 조회 구현 + - `createPost()`: 게시글 생성 구현 + - `updatePost()`: 게시글 수정 구현 + - `deletePost()`: 게시글 삭제 구현 + - `uploadImage()`: 이미지 업로드 구현 + +5. **파일 끝 개행 추가** + +### 3.3 `app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt` + +**변경 사항:** +1. **Import 추가:** + - `PostCreateViewModel` 추가 + - `PostDetailViewModel` 추가 + - `PostEditViewModel` 추가 + +2. **PostDetailScreen에서 Factory를 통해 Repository를 ViewModel에 주입:** + ```kotlin + viewModel = viewModel(factory = postViewModelFactory { PostDetailViewModel(it) }) + ``` + - Factory가 AppContainer에서 Repository를 가져와 ViewModel 생성자에 전달 + +3. **PostCreateScreen에서 Factory를 통해 Repository를 ViewModel에 주입:** + ```kotlin + viewModel = viewModel(factory = postViewModelFactory { PostCreateViewModel(it) }) + ``` + - Factory가 AppContainer에서 Repository를 가져와 ViewModel 생성자에 전달 + +4. **PostEditScreen에서 Factory를 통해 Repository를 ViewModel에 주입:** + ```kotlin + viewModel = viewModel(factory = postViewModelFactory { PostEditViewModel(it) }) + ``` + - Factory가 AppContainer에서 Repository를 가져와 ViewModel 생성자에 전달 + +### 3.4 `app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt` + +**변경 사항:** +1. **Import 변경:** + - `PostViewModel` → `PostCreateViewModel` + - `postViewModelFactory` 추가 + +2. **ViewModel 타입 변경:** + - `PostViewModel` → `PostCreateViewModel` + - Factory를 통해 Repository를 ViewModel에 주입 + +3. **상태 접근 방식 변경:** + - `viewModel.isUploading` → `uiState.isUploading` + - `viewModel.uploadedImageUrl` → `uiState.uploadedImageUrl` + - `val uiState = viewModel.uiState` 추가 + +4. **주석 추가:** + - Repository 주입 방식에 대한 설명 주석 추가 + +### 3.5 `app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt` + +**변경 사항:** +1. **Import 변경:** + - `PostViewModel` → `PostDetailViewModel` + - `postViewModelFactory` 추가 + +2. **ViewModel 타입 변경:** + - `PostViewModel` → `PostDetailViewModel` + - Factory를 통해 Repository를 ViewModel에 주입 + +3. **상태 접근 방식 변경:** + - `viewModel.postDetail` → `uiState.postDetail` + - `val uiState = viewModel.uiState` 추가 + +4. **LaunchedEffect 주석 해제:** + - `viewModel.getPostDetail(postId)` 호출 활성화 + +5. **주석 추가:** + - Repository 주입 방식에 대한 설명 주석 추가 + +### 3.6 `app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt` + +**변경 사항:** +1. **Import 변경:** + - `PostViewModel` → `PostEditViewModel` + - `postViewModelFactory` 추가 + +2. **ViewModel 타입 변경:** + - `PostViewModel` → `PostEditViewModel` + - Factory를 통해 Repository를 ViewModel에 주입 + +3. **상태 접근 방식 변경:** + - `viewModel.postDetail` → `uiState.postDetail` + - `viewModel.uploadedImageUrl` → `uiState.uploadedImageUrl` + - `viewModel.isUploading` → `uiState.isUploading` + - `val uiState = viewModel.uiState` 추가 + +4. **주석 추가:** + - Repository 주입 방식에 대한 설명 주석 추가 + +### 3.7 `app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt` + +**변경 사항:** +1. **Import 변경:** + - `PostViewModel` 제거 + - `PostListUiState` 제거 + - `collectAsState` 제거 + - `DisposableEffect`, `LocalLifecycleOwner`, `Lifecycle`, `LifecycleEventObserver` 추가 + - `postViewModelFactory` 추가 + +2. **Factory를 통한 Repository 주입 추가:** + - Factory가 AppContainer에서 Repository를 가져와 ViewModel 생성자에 전달 + - 기본값으로 ViewModel 생성 + +3. **상태 관리 방식 변경:** + - `StateFlow` 기반 → `mutableStateOf` 기반으로 변경 + - `collectAsState()` 제거, 직접 `uiState` 접근 + +4. **Lifecycle 관리 추가:** + - `DisposableEffect`를 사용한 Lifecycle 이벤트 감지 + - `ON_RESUME` 이벤트 시 자동 새로고침 + +5. **UI 구조 단순화:** + - `PostListUiState`의 sealed class 구조 제거 + - Loading, Success, Error 상태 분기 제거 + - 단순한 LazyColumn으로 변경 + +6. **LaunchedEffect 추가:** + - 초기 로드 시 `viewModel.refresh()` 호출 + +### 3.8 `app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt` + +**변경 사항:** +1. **Import 변경:** + - `PostListUiState` 제거 (파일 내부로 이동) + - `MutableStateFlow`, `StateFlow`, `asStateFlow` 제거 + - `mutableStateOf`, `getValue`, `setValue` 추가 + - `PostResponse` 추가 + +2. **상태 관리 방식 변경:** + - `StateFlow` 기반 → `mutableStateOf` 기반으로 변경 + - `_uiState`와 `uiState` 분리 구조 제거 + +3. **PostListUiState 정의 변경:** + - sealed class에서 data class로 변경 + - Loading, Success, Error 상태 제거 + - 단순히 `posts: List`만 포함 + +4. **생성자 파라미터 이름 변경:** + - `postRepository` → `repository` + +5. **초기화 로직 변경:** + - `init` 블록 제거 + - `loadPosts()` 메서드 제거 + - `refresh()` 메서드로 통합 + +6. **에러 처리 단순화:** + - Error 상태 제거, 실패 시 빈 리스트로 설정 + +7. **파일 끝 개행 추가** + +### 3.9 `app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt` + +**변경 사항:** +1. **주석 추가:** + - Repository 패턴과 수동 주입(App Container)에 대한 설명 주석 추가 + - Factory 패턴 사용 목적 설명 + +### 3.10 `gradle/libs.versions.toml` + +**변경 사항:** +1. **AGP 버전 업데이트:** + - `agp = "8.13.0"` → `agp = "8.13.1"` + +--- + +## 주요 아키텍처 변경 사항 + +### 1. Repository 패턴 도입 +- ApiService를 직접 참조하던 구조에서 Repository를 통한 추상화 +- 의존성 주입을 통한 테스트 용이성 향상 + +### 2. ViewModel 분리 +- 단일 `PostViewModel`을 기능별로 분리: + - `PostListViewModel`: 목록 조회 + - `PostCreateViewModel`: 게시글 생성 + - `PostDetailViewModel`: 상세 조회 및 삭제 + - `PostEditViewModel`: 게시글 수정 + +### 3. UiState 구현 +- 각 ViewModel에 UiState data class 추가 (PostCreateUiState, PostDetailUiState, PostEditUiState) +- PostListViewModel은 practice-only와 동일하게 StateFlow + sealed class 기반 PostListUiState 유지 +- UI 상태를 UiState로 명확히 정의하여 상태 관리 일관성 향상 + +### 4. 의존성 주입 구조 +- `AppContainer`를 통한 중앙 집중식 의존성 관리 +- Factory 패턴을 통한 ViewModel 생성 시 Repository 주입 +- ViewModel이 Repository를 파라미터로 받아 사용 (의존성 주입) + +### 5. Lifecycle 관리 개선 +- `LaunchedEffect`를 사용한 초기 로드 시 refresh 함수 실행 (미션 요구사항) +- PostListScreen에서 화면 진입 시 자동으로 데이터 새로고침 + diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md new file mode 100644 index 0000000..5e151ac --- /dev/null +++ b/PROJECT_CONTEXT.md @@ -0,0 +1,69 @@ +# 프로젝트 컨텍스트 및 개발 가이드라인 + +## 브랜치 구조 + +### practice-only 브랜치 +- **의미**: 미션 반영 전 상태 +- **내용**: 강의자가 실습한 코드를 그대로 구현한 것 +- **역할**: 기준점(baseline)으로 사용 +- **중요**: 이 브랜치의 코드 스타일과 패턴을 크게 벗어나지 않아야 함 + +### JeongIlhyuk/week7 브랜치 +- **의미**: 미션 반영 후 상태 +- **내용**: 미션 요구사항을 반영한 코드 +- **역할**: 미션 완료 버전 + +--- + +## 미션 요구사항 (체크리스트) + +- [x] 수동 주입(App Container) 구현 +- [x] Repository 패턴 사용 +- [x] ViewModel 분리 + - PostEditViewModel + - PostDetailViewModel (삭제까지 같이 하시면 됩니다) + - PostCreateViewModel +- [x] UiState 구현 +- [x] PostViewModel 삭제 +- [x] PostListViewModel의 refresh 함수를 메인화면에서 sideEffect로 항상 실행시키기 + +--- + +## 개발 원칙 + +### ⚠️ 중요 사항 +1. **미션 요구사항에 벗어나는 불필요한 변경 금지** + - 미션에서 요구하지 않은 변경사항은 만들지 않기 + - 예: StateFlow → mutableStateOf 변경은 미션 요구사항이 아님 (단순히 선택된 방식) + +2. **practice-only 브랜치의 코드 스타일 유지** + - 강의자가 실습한 코드의 패턴과 스타일을 존중 + - 불필요한 리팩토링이나 스타일 변경 지양 + +3. **변경사항 검증** + - 모든 변경사항이 미션 요구사항과 직접적으로 연관되어 있는지 확인 + - practice-only 브랜치와 비교하여 불필요한 차이점이 없는지 확인 + +--- + +## 브랜치 비교 시 주의사항 + +### 비교 방법 +```bash +# practice-only (미션 전) vs JeongIlhyuk/week7 (미션 후) +git diff practice-only JeongIlhyuk/week7 +``` + +### 확인해야 할 사항 +1. 변경사항이 미션 요구사항과 직접 연관이 있는가? +2. practice-only의 코드 스타일을 크게 벗어나지 않는가? +3. 불필요한 리팩토링이나 개선이 포함되어 있지 않은가? + +--- + +## 참고사항 + +- practice-only 브랜치는 강의자의 실습 코드이므로, 이를 기준으로 미션만 반영해야 함 +- 미션 요구사항 외의 "개선"이나 "최적화"는 지양 +- 코드 스타일, 네이밍, 구조는 practice-only 브랜치를 따르는 것을 원칙으로 함 + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0f991aa..958f28b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ android:maxSdkVersion="32" /> ApiService 직접 참조 + // 현재:뷰모델 -> Repository -> ApiService Repository 거쳐 참조 + PostRepositoryImpl(apiService) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt b/app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt new file mode 100644 index 0000000..0b045b9 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt @@ -0,0 +1,25 @@ +package com.example.kuit6_android_api.data.repository + +import com.example.kuit6_android_api.data.model.request.PostCreateRequest +import com.example.kuit6_android_api.data.model.response.PostResponse +import okhttp3.MultipartBody + +interface PostRepository { + suspend fun getPosts(): Result> + //레포지토리 패턴을 사용하기 위해 다음 함수들을 추가 + suspend fun getPostDetail(postId: Long): Result + suspend fun createPost( + author: String, + title: String, + content: String, + imageUrl: String? + ): Result + suspend fun updatePost( + postId: Long, + title: String, + content: String, + imageUrl: String? + ): Result + suspend fun deletePost(postId: Long): Result + suspend fun uploadImage(file: MultipartBody.Part): Result +} diff --git a/app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt b/app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt new file mode 100644 index 0000000..b7904fe --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt @@ -0,0 +1,97 @@ +package com.example.kuit6_android_api.data.repository + +import android.util.Log +import com.example.kuit6_android_api.data.api.ApiService +import com.example.kuit6_android_api.data.model.request.PostCreateRequest +import com.example.kuit6_android_api.data.model.response.BaseResponse +import com.example.kuit6_android_api.data.model.response.PostResponse +import okhttp3.MultipartBody + +class PostRepositoryImpl( + private val apiService: ApiService +) : PostRepository { + override suspend fun getPosts(): Result> { + return runCatching { + val response: BaseResponse> = apiService.getPosts() + if (response.success && response.data != null) { + response.data + } else { + throw Exception(response.message ?: "게시글 목록 조회 실패") + } + }.onFailure { error -> + Log.e("PostRepository", error.message.toString()) + } + } + + override suspend fun getPostDetail(postId: Long): Result { + return runCatching { + val response: BaseResponse = apiService.getPostDetail(postId) + if (response.success && response.data != null) { + response.data + } else { + throw Exception(response.message ?: "게시글 상세 조회 실패") + } + } + } + + override suspend fun createPost( + author: String, + title: String, + content: String, + imageUrl: String? + ): Result { + return runCatching { + val request = PostCreateRequest(title, content, imageUrl) + val response: BaseResponse = apiService.createPost(author, request) + if (response.success && response.data != null) { + response.data + } else { + throw Exception(response.message ?: "게시글 생성 실패") + } + } + } + + override suspend fun updatePost( + postId: Long, + title: String, + content: String, + imageUrl: String? + ): Result { + return runCatching { + val request = PostCreateRequest(title, content, imageUrl) + val response: BaseResponse = apiService.updatePost(postId, request) + if (response.success && response.data != null) { + response.data + } else { + throw Exception(response.message ?: "게시글 수정 실패") + } + } + } + + override suspend fun deletePost(postId: Long): Result { + return runCatching { + val response: BaseResponse = apiService.deletePost(postId) + if (response.success) { + Unit + } else { + throw Exception(response.message ?: "게시글 삭제 실패") + } + } + } + + override suspend fun uploadImage(file: MultipartBody.Part): Result { + return runCatching { + val response: BaseResponse> = apiService.uploadImage(file) + if (response.success && response.data != null) { + val imageUrl = response.data["imageUrl"] + if (imageUrl != null) { + imageUrl + } else { + throw Exception("이미지 URL을 받아오지 못했습니다") + } + } else { + throw Exception(response.message ?: "이미지 업로드 실패") + } + } + } +} diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt b/app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt index 975565e..cb02fd1 100644 --- a/app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt @@ -2,6 +2,7 @@ package com.example.kuit6_android_api.ui.navigation import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -10,6 +11,11 @@ import com.example.kuit6_android_api.ui.post.screen.PostCreateScreen import com.example.kuit6_android_api.ui.post.screen.PostDetailScreen import com.example.kuit6_android_api.ui.post.screen.PostEditScreen import com.example.kuit6_android_api.ui.post.screen.PostListScreen +import com.example.kuit6_android_api.ui.post.viewmodel.PostCreateViewModel +import com.example.kuit6_android_api.ui.post.viewmodel.PostDetailViewModel +import com.example.kuit6_android_api.ui.post.viewmodel.PostEditViewModel +import com.example.kuit6_android_api.ui.post.viewmodel.PostListViewModel +import com.example.kuit6_android_api.ui.post.viewmodel.postViewModelFactory @Composable fun NavGraph( @@ -28,7 +34,8 @@ fun NavGraph( }, onCreatePostClick = { navController.navigate(PostCreateRoute) - } + }, + viewModel = viewModel(factory = postViewModelFactory { PostListViewModel(it) }) ) } @@ -43,6 +50,7 @@ fun NavGraph( onEditClick = { postId -> navController.navigate(PostEditRoute(postId)) }, + viewModel = viewModel(factory = postViewModelFactory { PostDetailViewModel(it) }), snackBarState = snackBarState ) } @@ -55,6 +63,7 @@ fun NavGraph( onPostCreated = { navController.popBackStack() }, + viewModel = viewModel(factory = postViewModelFactory { PostCreateViewModel(it) }), snackBarState = snackBarState ) } @@ -70,6 +79,7 @@ fun NavGraph( onPostUpdated = { navController.popBackStack() }, + viewModel = viewModel(factory = postViewModelFactory { PostEditViewModel(it) }), snackBarState = snackBarState ) } diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt index 58cee96..e7cc4c1 100644 --- a/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt @@ -53,7 +53,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage -import com.example.kuit6_android_api.ui.post.viewmodel.PostViewModel +import com.example.kuit6_android_api.ui.post.viewmodel.PostCreateViewModel +import com.example.kuit6_android_api.ui.post.viewmodel.postViewModelFactory import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -61,10 +62,12 @@ import kotlinx.coroutines.launch fun PostCreateScreen( onNavigateBack: () -> Unit, onPostCreated: () -> Unit, - viewModel: PostViewModel = viewModel(), + // Repository는 postViewModelFactory를 통해 수동 주입(App Container) + viewModel: PostCreateViewModel = viewModel(factory = postViewModelFactory { PostCreateViewModel(it) }), snackBarState: SnackbarHostState ) { val context = LocalContext.current + val uiState = viewModel.uiState var author by remember { mutableStateOf("") } var title by remember { mutableStateOf("") } var content by remember { mutableStateOf("") } @@ -198,7 +201,7 @@ fun PostCreateScreen( color = MaterialTheme.colorScheme.onSurface ) - if (selectedImageUri == null && !viewModel.isUploading) { + if (selectedImageUri == null && !uiState.isUploading) { FilledTonalButton( onClick = { imagePickerLauncher.launch("image/*") }, shape = RoundedCornerShape(10.dp) @@ -209,7 +212,7 @@ fun PostCreateScreen( } // 업로드 중 표시 - if (viewModel.isUploading) { + if (uiState.isUploading) { Spacer(modifier = Modifier.height(12.dp)) Row( verticalAlignment = Alignment.CenterVertically, @@ -230,7 +233,7 @@ fun PostCreateScreen( } // 업로드된 이미지 미리보기 - if (selectedImageUri != null && !viewModel.isUploading) { + if (selectedImageUri != null && !uiState.isUploading) { Spacer(modifier = Modifier.height(12.dp)) Box( modifier = Modifier.fillMaxWidth() @@ -270,7 +273,7 @@ fun PostCreateScreen( Button( onClick = { val finalAuthor = author - viewModel.createPost(finalAuthor, title, content, viewModel.uploadedImageUrl) { + viewModel.createPost(finalAuthor, title, content, uiState.uploadedImageUrl) { onPostCreated() scope.launch { snackBarState.showSnackbar("게시글이 작성되었습니다.") } } @@ -278,7 +281,7 @@ fun PostCreateScreen( modifier = Modifier .fillMaxWidth() .height(56.dp), - enabled = title.isNotBlank() && content.isNotBlank() && !viewModel.isUploading, + enabled = title.isNotBlank() && content.isNotBlank() && !uiState.isUploading, shape = RoundedCornerShape(16.dp), elevation = ButtonDefaults.buttonElevation( defaultElevation = 4.dp, diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt index eb617c1..5a83bde 100644 --- a/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt @@ -46,7 +46,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage -import com.example.kuit6_android_api.ui.post.viewmodel.PostViewModel +import com.example.kuit6_android_api.ui.post.viewmodel.PostDetailViewModel +import com.example.kuit6_android_api.ui.post.viewmodel.postViewModelFactory import com.example.kuit6_android_api.util.formatDateTime import kotlinx.coroutines.launch @@ -56,15 +57,17 @@ fun PostDetailScreen( postId: Long, onNavigateBack: () -> Unit, onEditClick: (Long) -> Unit = {}, - viewModel: PostViewModel = viewModel(), + // Repository는 postViewModelFactory를 통해 수동 주입(App Container)됩니다 + viewModel: PostDetailViewModel = viewModel(factory = postViewModelFactory { PostDetailViewModel(it) }), snackBarState: SnackbarHostState ) { - val post = viewModel.postDetail + val uiState = viewModel.uiState + val post = uiState.postDetail var showDeleteDialog by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() LaunchedEffect(postId) { -// viewModel.getPostDetail(postId) + viewModel.getPostDetail(postId) } Scaffold( diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt index 5f8283e..4978458 100644 --- a/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt @@ -52,7 +52,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage -import com.example.kuit6_android_api.ui.post.viewmodel.PostViewModel +import com.example.kuit6_android_api.ui.post.viewmodel.PostEditViewModel +import com.example.kuit6_android_api.ui.post.viewmodel.postViewModelFactory import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -61,11 +62,13 @@ fun PostEditScreen( postId: Long, onNavigateBack: () -> Unit, onPostUpdated: () -> Unit, - viewModel: PostViewModel = viewModel(), + // Repository는 postViewModelFactory를 통해 수동 주입(App Container)됩니다 + viewModel: PostEditViewModel = viewModel(factory = postViewModelFactory { PostEditViewModel(it) }), snackBarState: SnackbarHostState ) { val context = LocalContext.current - val post = viewModel.postDetail + val uiState = viewModel.uiState + val post = uiState.postDetail val scope = rememberCoroutineScope() var title by remember { mutableStateOf("") } @@ -231,7 +234,7 @@ fun PostEditScreen( Button( onClick = { val imageUrl = if (selectedImageUri != null) { - viewModel.uploadedImageUrl + uiState.uploadedImageUrl } else { post?.imageUrl } @@ -245,14 +248,14 @@ fun PostEditScreen( modifier = Modifier .fillMaxWidth() .height(56.dp), - enabled = title.isNotBlank() && content.isNotBlank() && !viewModel.isUploading, + enabled = title.isNotBlank() && content.isNotBlank() && !uiState.isUploading, shape = RoundedCornerShape(12.dp), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary, disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant ) ) { - if (viewModel.isUploading) { + if (uiState.isUploading) { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt index 8522d79..236f8cb 100644 --- a/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -18,23 +19,29 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.example.kuit6_android_api.ui.post.component.PostItem -import com.example.kuit6_android_api.ui.post.viewmodel.PostViewModel +import com.example.kuit6_android_api.ui.post.state.PostListUiState +import com.example.kuit6_android_api.ui.post.viewmodel.PostListViewModel +import com.example.kuit6_android_api.ui.post.viewmodel.postViewModelFactory @OptIn(ExperimentalMaterial3Api::class) @Composable fun PostListScreen( onPostClick: (Long) -> Unit, onCreatePostClick: () -> Unit, - viewModel: PostViewModel = viewModel() + viewModel: PostListViewModel = viewModel(factory = postViewModelFactory { PostListViewModel(it) }) ) { - val posts = viewModel.posts + val uiState by viewModel.uiState.collectAsState() LaunchedEffect(Unit) { - viewModel.getPosts() + // Side Effect:UI 렌더링 외의 작업 + // UI를 직접 그리지 않고 변화하게 했으므로 Side Effect + viewModel.refresh() } Scaffold( @@ -49,19 +56,29 @@ fun PostListScreen( } } ) { paddingValues -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .background(MaterialTheme.colorScheme.background), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - items(posts) { post -> - PostItem( - post = post, - onClick = { onPostClick(post.id) } - ) + when (uiState) { + is PostListUiState.Loading -> { + CircularProgressIndicator() + } + is PostListUiState.Success -> { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(MaterialTheme.colorScheme.background), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items((uiState as PostListUiState.Success).posts) { post -> + PostItem( + post = post, + onClick = { onPostClick(post.id) } + ) + } + } + } + is PostListUiState.Error -> { + // Error 상태 처리 (practice-only와 동일하게 빈 상태) } } } diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt new file mode 100644 index 0000000..f27b93f --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt @@ -0,0 +1,15 @@ +package com.example.kuit6_android_api.ui.post.state + +import com.example.kuit6_android_api.data.model.response.PostResponse + +sealed class PostListUiState { + data object Loading: PostListUiState() + + data class Success( + val posts: List + ):PostListUiState() + + data class Error( + val message: String + ): PostListUiState() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt new file mode 100644 index 0000000..de192e1 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt @@ -0,0 +1,84 @@ +package com.example.kuit6_android_api.ui.post.viewmodel + +import android.content.Context +import android.net.Uri +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.example.kuit6_android_api.data.repository.PostRepository +import com.example.kuit6_android_api.util.UriUtils +import kotlinx.coroutines.launch +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody + +//PostViewModel을 PostListViewModel, PostCreateViewModel, PostDetailViewModel, PostEditViewModel로 분리 +data class PostCreateUiState( //uiState를 통해 상태를 한 번에 모아 처리 + val uploadedImageUrl: String? = null, + val isUploading: Boolean = false +) + +class PostCreateViewModel( + private val repository: PostRepository + // 의존성 주입:뷰모델의 파라미터로 Repository를 전달하는 것 +) : ViewModel() { + //st + var uiState by mutableStateOf(PostCreateUiState()) + private set + + fun createPost( + author: String, + title: String, + content: String, + imageUrl: String? = null, + onSuccess: () -> Unit = {} + ) { + viewModelScope.launch { + repository.createPost(author, title, content, imageUrl) + .onSuccess { + uiState = uiState.copy(uploadedImageUrl = null) + onSuccess() + } + } + } + + fun clearUploadedImageUrl() { + uiState = uiState.copy(uploadedImageUrl = null) + } + + fun uploadImage( + context: Context, + uri: Uri, + onSuccess: (String) -> Unit = {}, + onError: (String) -> Unit = {} + ) { + viewModelScope.launch { + uiState = uiState.copy(isUploading = true) + val file = UriUtils.uriToFile(context, uri) + if (file == null) { + uiState = uiState.copy(isUploading = false) + onError("파일 변환 실패") + return@launch + } + + val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) + val body = MultipartBody.Part.createFormData("file", file.name, requestFile) + + repository.uploadImage(body) + .onSuccess { imageUrl -> + uiState = uiState.copy( + isUploading = false, + uploadedImageUrl = imageUrl + ) + onSuccess(imageUrl) + } + .onFailure { error -> + uiState = uiState.copy(isUploading = false) + onError(error.message ?: "업로드 실패") + } + } + } +} + diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt new file mode 100644 index 0000000..6544df0 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt @@ -0,0 +1,43 @@ +package com.example.kuit6_android_api.ui.post.viewmodel + +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.example.kuit6_android_api.data.repository.PostRepository +import com.example.kuit6_android_api.data.model.response.PostResponse +import kotlinx.coroutines.launch + +data class PostDetailUiState( + val postDetail: PostResponse? = null +) + +class PostDetailViewModel( + private val repository: PostRepository +) : ViewModel() { + var uiState by mutableStateOf(PostDetailUiState()) + private set + + fun getPostDetail(postId: Long) { + viewModelScope.launch { + repository.getPostDetail(postId) + .onSuccess { post -> + uiState = uiState.copy(postDetail = post) + } + .onFailure { + uiState = uiState.copy(postDetail = null) + } + } + } + + fun deletePost(postId: Long, onSuccess: () -> Unit = {}) { + viewModelScope.launch { + repository.deletePost(postId) + .onSuccess { + onSuccess() + } + } + } +} + diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt new file mode 100644 index 0000000..6234394 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt @@ -0,0 +1,95 @@ +package com.example.kuit6_android_api.ui.post.viewmodel + +import android.content.Context +import android.net.Uri +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.example.kuit6_android_api.data.repository.PostRepository +import com.example.kuit6_android_api.data.model.response.PostResponse +import com.example.kuit6_android_api.util.UriUtils +import kotlinx.coroutines.launch +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody + +data class PostEditUiState( + val postDetail: PostResponse? = null, + val uploadedImageUrl: String? = null, + val isUploading: Boolean = false +) + +class PostEditViewModel( + private val repository: PostRepository +) : ViewModel() { + var uiState by mutableStateOf(PostEditUiState()) + private set + + fun getPostDetail(postId: Long) { + viewModelScope.launch { + repository.getPostDetail(postId) + .onSuccess { post -> + uiState = uiState.copy(postDetail = post) + } + .onFailure { + uiState = uiState.copy(postDetail = null) + } + } + } + + fun updatePost( + postId: Long, + title: String, + content: String, + imageUrl: String? = null, + onSuccess: () -> Unit = {} + ) { + viewModelScope.launch { + repository.updatePost(postId, title, content, imageUrl) + .onSuccess { + uiState = uiState.copy(uploadedImageUrl = null) + onSuccess() + } + } + } + + fun clearUploadedImageUrl() { + uiState = uiState.copy(uploadedImageUrl = null) + } + + fun uploadImage( + context: Context, + uri: Uri, + onSuccess: (String) -> Unit = {}, + onError: (String) -> Unit = {} + ) { + viewModelScope.launch { + uiState = uiState.copy(isUploading = true) + val file = UriUtils.uriToFile(context, uri) + if (file == null) { + uiState = uiState.copy(isUploading = false) + onError("파일 변환 실패") + return@launch + } + + val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) + val body = MultipartBody.Part.createFormData("file", file.name, requestFile) + + repository.uploadImage(body) + .onSuccess { imageUrl -> + uiState = uiState.copy( + isUploading = false, + uploadedImageUrl = imageUrl + ) + onSuccess(imageUrl) + } + .onFailure { error -> + uiState = uiState.copy(isUploading = false) + onError(error.message ?: "업로드 실패") + } + } + } +} + diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt new file mode 100644 index 0000000..117499b --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt @@ -0,0 +1,36 @@ +package com.example.kuit6_android_api.ui.post.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.kuit6_android_api.data.repository.PostRepository +import com.example.kuit6_android_api.ui.post.state.PostListUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class PostListViewModel( + private val repository: PostRepository +) : ViewModel() { + private val _uiState = MutableStateFlow(PostListUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private fun loadPosts() { + viewModelScope.launch { + _uiState.value = PostListUiState.Loading + repository.getPosts() + .onSuccess { posts -> + _uiState.value = PostListUiState.Success(posts) + } + .onFailure { error -> + _uiState.value = PostListUiState.Error( + message = error.message ?: "error" + ) + } + } + } + + fun refresh() { + loadPosts() + } +} diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt deleted file mode 100644 index d42740f..0000000 --- a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.example.kuit6_android_api.ui.post.viewmodel - -import android.content.Context -import android.net.Uri -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.example.kuit6_android_api.data.api.RetrofitClient -import com.example.kuit6_android_api.data.model.request.PostCreateRequest -import com.example.kuit6_android_api.data.model.response.PostResponse -import kotlinx.coroutines.launch -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.asRequestBody - -class PostViewModel : ViewModel() { - var posts by mutableStateOf>(emptyList()) - private set - - var postDetail by mutableStateOf(null) - private set - - var uploadedImageUrl by mutableStateOf(null) - private set - - var isUploading by mutableStateOf(false) - private set - - private val apiService = RetrofitClient.apiService - - fun getPosts() { - viewModelScope.launch { - runCatching { - apiService.getPosts() - }.onSuccess { response -> - response.data?.let { - if (response.success) { - posts = response.data - } - } - }.onFailure { error -> - Log.e("getPost", error.message.toString()) - } - } - } - - fun getPostDetail(postId: Long) { - viewModelScope.launch { - runCatching { - apiService.getPostDetail(postId) - }.onSuccess { response -> - if (response.success && response.data != null) { - postDetail = response.data - } - }.onFailure { error -> - // 에러 처리 - postDetail = null - } - } - } - - fun createPost( - author: String, - title: String, - content: String, - imageUrl: String? = null, - onSuccess: () -> Unit = {} - ) { - viewModelScope.launch { - runCatching { - val request = PostCreateRequest(title, content, imageUrl) - apiService.createPost(author, request) - }.onSuccess { response -> - if (response.success) { - // 이미지 업로드 관련 코드 - clearUploadedImageUrl() - onSuccess() - } - } - } - } - - fun updatePost( - postId: Long, - title: String, - content: String, - imageUrl: String? = null, - onSuccess: () -> Unit = {} - ) { - viewModelScope.launch { - runCatching { - val request = PostCreateRequest(title, content, imageUrl) - apiService.updatePost(postId, request) - }.onSuccess { response -> - if (response.success) { - clearUploadedImageUrl() - onSuccess() - } - }.onFailure { error -> - // 에러 처리 - } - } - } - - fun deletePost(postId: Long, onSuccess: () -> Unit = {}) { - viewModelScope.launch { - runCatching { - apiService.deletePost(postId) - }.onSuccess { response -> - if (response.success) { - onSuccess() - } - }.onFailure { error -> - - } - } - } - - fun clearUploadedImageUrl() { - uploadedImageUrl = null - } - - // 이미지 업로드 함수 - fun uploadImage( - context: Context, - uri: Uri, - onSuccess: (String) -> Unit = {}, - onError: (String) -> Unit = {} - ) { - viewModelScope.launch { - isUploading = true - runCatching { - val file = UriUtils.uriToFile(context, uri) - if (file == null) { - throw Exception("파일 변환 실패") - } - - val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) - val body = MultipartBody.Part.createFormData("file", file.name, requestFile) - - apiService.uploadImage(body) - }.onSuccess { response -> - isUploading = false - if (response.success && response.data != null) { - val imageUrl = response.data["imageUrl"] - if (imageUrl != null) { - uploadedImageUrl = imageUrl - onSuccess(imageUrl) - } - } - }.onFailure { error -> - isUploading = false - onError(error.message ?: "업로드 실패") - } - } - } -} diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt new file mode 100644 index 0000000..96bc814 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt @@ -0,0 +1,26 @@ +package com.example.kuit6_android_api.ui.post.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.example.kuit6_android_api.App +import com.example.kuit6_android_api.data.repository.PostRepository + +/** + * Repository 패턴을 위해 Repository를 ViewModel에 파라미터로 전달하는 Factory + * Repository는 수동 주입(App Container)을 통해 가져오며, + * ViewModel에 파라미터를 전달하기 위해 Factory 패턴을 사용합니다. + */ +inline fun postViewModelFactory( + crossinline create: (PostRepository) -> VM +): ViewModelProvider.Factory = viewModelFactory { + initializer { + val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] + as App + + val postRepository = application.container.postRepository + + create(postRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/UriUtils.kt b/app/src/main/java/com/example/kuit6_android_api/util/UriUtils.kt similarity index 95% rename from app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/UriUtils.kt rename to app/src/main/java/com/example/kuit6_android_api/util/UriUtils.kt index 3271431..4418e1a 100644 --- a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/UriUtils.kt +++ b/app/src/main/java/com/example/kuit6_android_api/util/UriUtils.kt @@ -1,4 +1,4 @@ -package com.example.kuit6_android_api.ui.post.viewmodel +package com.example.kuit6_android_api.util import android.content.Context import android.net.Uri @@ -38,4 +38,4 @@ object UriUtils { } return fileName } -} +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 20d5c65..d8f06bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.13.0" +agp = "8.13.1" kotlin = "2.0.21" coreKtx = "1.17.0" junit = "4.13.2"