From d3738088fdb871418652472d1f9b5e4f32b33ec7 Mon Sep 17 00:00:00 2001 From: kyungmin Date: Thu, 20 Nov 2025 04:17:52 +0900 Subject: [PATCH 1/5] =?UTF-8?q?7=EC=A3=BC=EC=B0=A8=EA=B9=8C=EC=A7=80=20?= =?UTF-8?q?=EB=B3=B5=EA=B5=AC=EC=A4=91...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + .../java/com/example/kuit6_android_api/App.kt | 13 + .../example/kuit6_android_api/MainActivity.kt | 3 +- .../kuit6_android_api/data/api/ApiService.kt | 9 + .../kuit6_android_api/data/di/AppContainer.kt | 16 ++ .../data/repository/PostRepository.kt | 28 +++ .../data/repository/PostRepositoryImpl.kt | 90 +++++++ .../ui/navigation/NavGraph.kt | 59 +++-- .../ui/post/screen/PostCreateScreen.kt | 92 +------ .../ui/post/screen/PostDetailScreen.kt | 68 ++---- .../ui/post/screen/PostEditScreen.kt | 230 ++++++------------ .../ui/post/screen/PostListScreen.kt | 55 +++-- .../ui/post/state/DeletePostUiState.kt | 13 + .../ui/post/state/PostCreateUiState.kt | 15 ++ .../ui/post/state/PostDetailUiState.kt | 15 ++ .../ui/post/state/PostEditUiState.kt | 15 ++ .../ui/post/state/PostListUiState.kt | 15 ++ .../ui/post/state/UploadImageUiState.kt | 16 ++ .../ui/post/viewmodel/PostCreateViewModel.kt | 38 +++ .../ui/post/viewmodel/PostDetailViewModel.kt | 53 ++++ .../ui/post/viewmodel/PostEditViewModel.kt | 51 ++++ .../ui/post/viewmodel/PostListViewModel.kt | 40 +++ .../ui/post/viewmodel/PostViewModel.kt | 109 --------- .../ui/post/viewmodel/PostViewModelFactory.kt | 20 ++ 24 files changed, 633 insertions(+), 431 deletions(-) create mode 100644 app/src/main/java/com/example/kuit6_android_api/App.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/state/DeletePostUiState.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostDetailUiState.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/state/UploadImageUiState.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt delete mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt 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" /> + + + @POST("/api/upload") + suspend fun uploadImage( + @Part file: MultipartBody.Part + ): BaseResponse> } \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt b/app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt new file mode 100644 index 0000000..e0b797f --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt @@ -0,0 +1,16 @@ +package com.example.kuit6_android_api.data.di + +import com.example.kuit6_android_api.data.api.ApiService +import com.example.kuit6_android_api.data.api.RetrofitClient +import com.example.kuit6_android_api.data.repository.PostRepository +import com.example.kuit6_android_api.data.repository.PostRepositoryImpl + +class AppContainer { + private val apiService: ApiService by lazy { + RetrofitClient.apiService + } + + val postRepository: PostRepository by lazy{ + PostRepositoryImpl(apiService) + } +} 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..c826423 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt @@ -0,0 +1,28 @@ +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 +import retrofit2.http.Body + +interface PostRepository{ + suspend fun getPosts(): Result> + suspend fun createPost( + author: String, + request: PostCreateRequest + ): Result + suspend fun getPostDetail( + id: Long + ): Result + suspend fun editPost( + id: Long, + request: PostCreateRequest + ): Result + suspend fun deletePost( + id: Long + ): Result + suspend fun uploadImage( + file: MultipartBody.Part + ): Result> + +} \ No newline at end of file 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..9bc6535 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt @@ -0,0 +1,90 @@ +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.PostResponse +import okhttp3.MultipartBody + +class PostRepositoryImpl( + private val apiService: ApiService +) : PostRepository{ + override suspend fun getPosts(): Result> { + return runCatching { + val response = 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 createPost( + author: String, + request: PostCreateRequest + ): Result { + return runCatching { + val response = apiService.createPost(author, request) + + 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(id: Long): Result { + return runCatching { + val response = apiService.getPostDetail(id) + + if (response.success && response.data != null) { + response.data + } else { + throw Exception(response.message ?: "게시글 불러오기 실패") + } + }.onFailure { error -> + Log.e("PostRepository", error.message.toString()) + } + } + override suspend fun editPost(id: Long, request: PostCreateRequest): Result { + return runCatching { + val response = apiService.editPost(id, request) + if(response.success && response.data != null){ + response.data + }else{ + throw Exception(response.message ?: "게시글 수정 실패") + } + }.onFailure { error-> + Log.e("PostRepository", error.message.toString()) + } + } + override suspend fun deletePost(id: Long): Result { + return runCatching { + val response = apiService.deletePost(id) + if (response.success) Unit + else throw Exception(response.message ?: "게시글 삭제 실패") + }.onFailure { error-> + Log.e("PostRepository", error.message.toString()) + } + } + + override suspend fun uploadImage(file: MultipartBody.Part): Result> { + return runCatching { + val response = apiService.uploadImage(file) + if (response.success && response.data != null) { + response.data + } else { + throw Exception(response.message ?: "이미지 업로드 실패") + } + }.onFailure { error -> + Log.e("PostRepository", error.message.toString()) + } + } + +} \ No newline at end of file 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 e1260fc..3bb3fe0 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 @@ -1,8 +1,7 @@ package com.example.kuit6_android_api.ui.navigation -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -11,18 +10,22 @@ 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.google.android.material.snackbar.Snackbar +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( navController: NavHostController, - startDestination: Any = PostListRoute, - snackBarState: SnackbarHostState + startDestination: Any = PostListRoute ) { NavHost( navController = navController, startDestination = startDestination ) { + composable { PostListScreen( onPostClick = { postId -> @@ -30,49 +33,61 @@ fun NavGraph( }, onCreatePostClick = { navController.navigate(PostCreateRoute) - } + }, + viewModel = viewModel( + factory = postViewModelFactory { PostListViewModel(postRepository = it) } + ) ) } composable { backStackEntry -> val route = backStackEntry.toRoute() - PostDetailScreen( postId = route.postId, - onNavigateBack = { - navController.popBackStack() - }, + onNavigateBack = { navController.popBackStack() }, onEditClick = { postId -> navController.navigate(PostEditRoute(postId)) }, - snackBarState= snackBarState + viewModel = viewModel(factory = postViewModelFactory { + PostDetailViewModel( + postRepository = it + ) + }) ) } composable { PostCreateScreen( - onNavigateBack = { - navController.popBackStack() - }, + onNavigateBack = { navController.popBackStack() }, onPostCreated = { + navController.previousBackStackEntry + ?.savedStateHandle + ?.set("refreshNeeded", true) navController.popBackStack() }, - snackBarState = snackBarState + viewModel = viewModel(factory = postViewModelFactory { + PostCreateViewModel( + postRepository = it + ) + }) ) } composable { backStackEntry -> val route = backStackEntry.toRoute() - PostEditScreen( postId = route.postId, - onNavigateBack = { - navController.popBackStack() - }, + onNavigateBack = { navController.popBackStack() }, onPostUpdated = { - navController.popBackStack() - }, - snackBarState = snackBarState + navController.previousBackStackEntry + ?.savedStateHandle + ?.set("refreshNeeded", true) + navController.popBackStack() }, + viewModel = viewModel(factory = postViewModelFactory { + PostEditViewModel( + postRepository = it + ) + }) ) } } 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 3cd7e39..45c3707 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 @@ -1,8 +1,5 @@ package com.example.kuit6_android_api.ui.post.screen -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -19,36 +16,28 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold -import androidx.compose.material3.Snackbar -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.kuit6_android_api.ui.post.viewmodel.PostViewModel -import kotlinx.coroutines.launch +import com.example.kuit6_android_api.data.model.request.PostCreateRequest +import com.example.kuit6_android_api.ui.post.viewmodel.PostCreateViewModel @OptIn(ExperimentalMaterial3Api::class) @@ -56,21 +45,12 @@ import kotlinx.coroutines.launch fun PostCreateScreen( onNavigateBack: () -> Unit, onPostCreated: () -> Unit, - snackBarState : SnackbarHostState, - viewModel: PostViewModel = viewModel() + viewModel: PostCreateViewModel ) { - val context = LocalContext.current + val uiState by viewModel.uiState.collectAsState() var author by remember { mutableStateOf("") } var title by remember { mutableStateOf("") } var content by remember { mutableStateOf("") } - var selectedImageUri by remember { mutableStateOf(null) } - val scope = rememberCoroutineScope() - - val imagePickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() - ) { uri: Uri? -> - selectedImageUri = uri - } Scaffold( topBar = { @@ -154,55 +134,18 @@ fun PostCreateScreen( ) ) - Spacer(modifier = Modifier.height(24.dp)) - - // 이미지 섹션 - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - ), - shape = RoundedCornerShape(16.dp) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = "이미지 첨부", - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold - ), - color = MaterialTheme.colorScheme.onSurface - ) - - if (selectedImageUri == null) { - FilledTonalButton( - onClick = { imagePickerLauncher.launch("image/*") }, - shape = RoundedCornerShape(10.dp) - ) { - Text("선택") - } - } - } - } - } - Spacer(modifier = Modifier.height(32.dp)) Button( onClick = { val finalAuthor = author - viewModel.createPost(finalAuthor, title, content,null) { - onPostCreated() - scope.launch{ - snackBarState.showSnackbar("게시글이 작성되었습니다.") - } - } + val request = PostCreateRequest( + title = title, + content = content, + imageUrl = null + ) + viewModel.createPost(finalAuthor, request) + onPostCreated() }, modifier = Modifier .fillMaxWidth() @@ -232,14 +175,3 @@ fun PostCreateScreen( } } -@Preview(showBackground = true) -@Composable -fun PostCreateScreenPreview() { - MaterialTheme { - PostCreateScreen( - onNavigateBack = {}, - onPostCreated = {}, - snackBarState = remember {SnackbarHostState()} - ) - } -} 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 78431ff..159b6c1 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 @@ -25,14 +25,13 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton 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.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -43,13 +42,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight -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.state.PostDetailUiState +import com.example.kuit6_android_api.ui.post.viewmodel.PostDetailViewModel import com.example.kuit6_android_api.util.formatDateTime -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -57,15 +54,14 @@ fun PostDetailScreen( postId: Long, onNavigateBack: () -> Unit, onEditClick: (Long) -> Unit = {}, - snackBarState : SnackbarHostState, - viewModel: PostViewModel = viewModel() + viewModel: PostDetailViewModel ) { - val post = viewModel.postDetail + val uiState by viewModel.uiState.collectAsState() var showDeleteDialog by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() LaunchedEffect(postId) { - viewModel.getPostDetail(postId) + viewModel.loadDetail(postId) } Scaffold( @@ -88,9 +84,13 @@ fun PostDetailScreen( ) } ) { paddingValues -> - post?.let { - - Column( + when (uiState) { + is PostDetailUiState.Loading -> { + // 로딩 표시는 필요시 추가 + } + is PostDetailUiState.Success -> { + val post = (uiState as PostDetailUiState.Success).post + Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) @@ -110,9 +110,9 @@ fun PostDetailScreen( .padding(16.dp) ) { // 프로필 이미지 - if (it.author.profileImageUrl != null) { + if (post.author.profileImageUrl != null) { AsyncImage( - model = it.author.profileImageUrl, + model = post.author.profileImageUrl, contentDescription = "프로필 이미지", modifier = Modifier .size(44.dp) @@ -138,14 +138,14 @@ fun PostDetailScreen( Column { Text( - text = it.author.username, + text = post.author.username, style = MaterialTheme.typography.bodyLarge.copy( fontWeight = FontWeight.SemiBold ), color = MaterialTheme.colorScheme.onSurface ) Text( - text = formatDateTime(it.createdAt), + text = formatDateTime(post.createdAt), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -154,7 +154,7 @@ fun PostDetailScreen( } // 이미지 - it.imageUrl?.let { imageUrl -> + post.imageUrl?.let { imageUrl -> AsyncImage( model = imageUrl, contentDescription = "게시글 이미지", @@ -170,7 +170,7 @@ fun PostDetailScreen( modifier = Modifier.padding(20.dp) ) { Text( - text = it.title, + text = post.title, style = MaterialTheme.typography.headlineSmall.copy( fontWeight = FontWeight.Bold ), @@ -180,7 +180,7 @@ fun PostDetailScreen( Spacer(modifier = Modifier.height(16.dp)) Text( - text = it.content, + text = post.content, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onBackground, lineHeight = MaterialTheme.typography.bodyLarge.lineHeight.times(1.5f) @@ -189,6 +189,10 @@ fun PostDetailScreen( Spacer(modifier = Modifier.height(40.dp)) } } + } + is PostDetailUiState.Error -> { + // 에러 표시는 필요시 추가 + } } } @@ -200,13 +204,9 @@ fun PostDetailScreen( text = { Text("정말로 이 게시글을 삭제하시겠습니까?") }, confirmButton = { TextButton(onClick = { - viewModel.deletePost(postId) { - showDeleteDialog = false - onNavigateBack() - scope.launch{ - snackBarState.showSnackbar("게시글이 삭제되었습니다. ") - } - } + viewModel.deletePost(postId) + showDeleteDialog = false + onNavigateBack() }) { Text("삭제") } @@ -219,17 +219,3 @@ fun PostDetailScreen( ) } } - - -@Preview(showBackground = true) -@Composable -fun PostDetailScreenPreview() { - MaterialTheme { - PostDetailScreen( - postId = 1L, - onNavigateBack = {}, - onEditClick = {}, - snackBarState = remember{ SnackbarHostState() } - ) - } -} 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 3418078..6e637d7 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 @@ -1,53 +1,52 @@ package com.example.kuit6_android_api.ui.post.screen -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts +import android.widget.Toast import androidx.compose.foundation.background -import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Clear import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight +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 kotlinx.coroutines.launch +import com.example.kuit6_android_api.data.model.request.PostCreateRequest +import com.example.kuit6_android_api.ui.post.state.PostEditUiState +import com.example.kuit6_android_api.ui.post.viewmodel.PostEditViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -55,34 +54,18 @@ fun PostEditScreen( postId: Long, onNavigateBack: () -> Unit, onPostUpdated: () -> Unit, - snackBarState: SnackbarHostState, - viewModel: PostViewModel = viewModel() + viewModel: PostEditViewModel ) { - val post = viewModel.postDetail - + val uiState by viewModel.uiState.collectAsState() + val context = LocalContext.current var title by remember { mutableStateOf("") } var content by remember { mutableStateOf("") } - var selectedImageUri by remember { mutableStateOf(null) } var isLoaded by remember { mutableStateOf(false) } - val scope = rememberCoroutineScope() - val imagePickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() - ) { uri: Uri? -> - selectedImageUri = uri - } LaunchedEffect(postId) { viewModel.getPostDetail(postId) } - LaunchedEffect(post) { - if (post != null && !isLoaded) { - title = post.title - content = post.content - isLoaded = true - } - } - Scaffold( topBar = { TopAppBar( @@ -102,148 +85,89 @@ fun PostEditScreen( ) } ) { paddingValues -> - post?.let { - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .verticalScroll(rememberScrollState()) - .padding(paddingValues) - .padding(20.dp) - ) { - OutlinedTextField( - value = title, - onValueChange = { title = it }, - label = { Text("제목") }, - placeholder = { Text("제목을 입력하세요 (최대 200자)") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - shape = RoundedCornerShape(12.dp), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outline - ) - ) + when (uiState) { + is PostEditUiState.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } - Spacer(modifier = Modifier.height(16.dp)) + is PostEditUiState.Success -> { + val post = (uiState as PostEditUiState.Success).post + if (!isLoaded) { + title = post.title + content = post.content + isLoaded = true + } - OutlinedTextField( - value = content, - onValueChange = { content = it }, - label = { Text("내용") }, - placeholder = { Text("내용을 입력하세요") }, + Column( modifier = Modifier - .fillMaxWidth() - .height(200.dp), - maxLines = 10, - shape = RoundedCornerShape(12.dp), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outline + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .verticalScroll(rememberScrollState()) + .padding(paddingValues) + .padding(20.dp) + ) { + // 제목 입력 + OutlinedTextField( + value = title, + onValueChange = { title = it }, + label = { Text("제목") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true ) - ) - Spacer(modifier = Modifier.height(20.dp)) - - Text( - text = "이미지 첨부", - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold - ), - color = MaterialTheme.colorScheme.onBackground - ) - - Spacer(modifier = Modifier.height(12.dp)) - - if (selectedImageUri != null || post.imageUrl != null) { - Box( + Spacer(modifier = Modifier.height(16.dp)) + // 내용 입력 + OutlinedTextField( + value = content, + onValueChange = { content = it }, + label = { Text("내용") }, modifier = Modifier .fillMaxWidth() - .height(200.dp) - ) { - AsyncImage( - model = selectedImageUri ?: post.imageUrl, - contentDescription = "선택된 이미지", - modifier = Modifier - .fillMaxWidth() - .height(200.dp) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.outline, - shape = RoundedCornerShape(8.dp) - ), - contentScale = ContentScale.Crop - ) - IconButton( - onClick = { selectedImageUri = null }, - modifier = Modifier - .align(Alignment.TopEnd) - .padding(8.dp) - ) { - Icon( - Icons.Default.Clear, - contentDescription = "이미지 제거", - tint = MaterialTheme.colorScheme.error + .height(200.dp), + maxLines = 10 + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // 수정 버튼 + Button( + onClick = { + val request = PostCreateRequest( + title = title, + content = content, + imageUrl = null ) - } - } - } else { - OutlinedButton( - onClick = { imagePickerLauncher.launch("image/*") }, + viewModel.editPost(postId, request) + onPostUpdated() + }, modifier = Modifier .fillMaxWidth() .height(56.dp), + enabled = title.isNotBlank() && content.isNotBlank(), shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.primary + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary ) ) { - Text("갤러리에서 이미지 선택") + Text("수정하기", fontWeight = FontWeight.Bold) } } + } - Spacer(modifier = Modifier.height(24.dp)) - - Button( - onClick = { - viewModel.updatePost(postId, title, content, null) { - onPostUpdated() - scope.launch{ - snackBarState.showSnackbar("게시글이 수정되었습니다.") - } - } - }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - enabled = title.isNotBlank() && content.isNotBlank(), - shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant - ) + is PostEditUiState.Error -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center ) { - Text( - "수정하기", - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold - ) - ) + Text("게시글 로딩 실패") } } } } } -@Preview(showBackground = true) -@Composable -fun PostEditScreenPreview() { - MaterialTheme { - PostEditScreen( - postId = 1L, - onNavigateBack = {}, - onPostUpdated = {}, - snackBarState = remember { SnackbarHostState()} - ) - } -} 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 e48ac6e..64574c1 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 @@ -17,27 +18,22 @@ import androidx.compose.material3.Scaffold 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.data.model.response.AuthorResponse -import com.example.kuit6_android_api.data.model.response.PostResponse +import com.example.kuit6_android_api.ui.post.state.PostListUiState +import com.example.kuit6_android_api.ui.post.viewmodel.PostListViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun PostListScreen( onPostClick: (Long) -> Unit, onCreatePostClick: () -> Unit, - viewModel: PostViewModel = viewModel() + viewModel: PostListViewModel ) { - val posts = viewModel.posts - - LaunchedEffect(Unit) { - viewModel.getPosts() - } + val uiState by viewModel.postListUiState.collectAsState() Scaffold( topBar = { @@ -51,19 +47,30 @@ 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 -> { + Text("로딩 실패") } } } diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/state/DeletePostUiState.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/DeletePostUiState.kt new file mode 100644 index 0000000..0a42be2 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/DeletePostUiState.kt @@ -0,0 +1,13 @@ +package com.example.kuit6_android_api.ui.post.state + +sealed class DeletePostUiState { + data object Loading : DeletePostUiState() + + data class Success( + val deletePost : Unit + ) : DeletePostUiState() + + data class Error( + val message: String + ) : DeletePostUiState() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.kt new file mode 100644 index 0000000..dacb499 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.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 PostCreateUiState { + data object Loading : PostCreateUiState() + + data class Success( + val post: PostResponse + ) : PostCreateUiState() + + data class Error( + val message: String + ) : PostCreateUiState() +} diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostDetailUiState.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostDetailUiState.kt new file mode 100644 index 0000000..bac738e --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostDetailUiState.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 PostDetailUiState { + data object Loading : PostDetailUiState() + + data class Success( + val post: PostResponse + ) : PostDetailUiState() + + data class Error( + val message: String + ) : PostDetailUiState() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.kt new file mode 100644 index 0000000..edc4df8 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.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 PostEditUiState { + data object Loading : PostEditUiState() + + data class Success( + val post: PostResponse + ) : PostEditUiState() + + data class Error( + val message: String + ) : PostEditUiState() +} \ No newline at end of file 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..b4ab364 --- /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/state/UploadImageUiState.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/UploadImageUiState.kt new file mode 100644 index 0000000..8263f2c --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/UploadImageUiState.kt @@ -0,0 +1,16 @@ +package com.example.kuit6_android_api.ui.post.state + +import com.example.kuit6_android_api.data.model.response.PostResponse + +sealed class UploadImageUiState { + data object Idle : UploadImageUiState() + data object Loading : UploadImageUiState() + + data class Success( + val imgUrl: Map + ) : UploadImageUiState() + + data class Error( + val message: String + ) : UploadImageUiState() +} 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..1115590 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt @@ -0,0 +1,38 @@ +package com.example.kuit6_android_api.ui.post.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.kuit6_android_api.data.model.request.PostCreateRequest +import com.example.kuit6_android_api.data.repository.PostRepository +import com.example.kuit6_android_api.ui.post.state.PostCreateUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class PostCreateViewModel ( + private val postRepository : PostRepository +) : ViewModel(){ + private val _uiState = MutableStateFlow(PostCreateUiState.Loading) // 변경 가능 상태 + val uiState: StateFlow = _uiState.asStateFlow() + fun createPost( + author: String, + request: PostCreateRequest + ){ + viewModelScope.launch { + _uiState.value = PostCreateUiState.Loading + + postRepository.createPost(author, request) + .onSuccess { post -> + _uiState.value = PostCreateUiState.Success(post) + } + .onFailure { error -> + _uiState.value = PostCreateUiState.Error( + error.message ?: "error" + ) + } + } + } +} + + 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..195f9d6 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt @@ -0,0 +1,53 @@ +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.DeletePostUiState +import com.example.kuit6_android_api.ui.post.state.PostDetailUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class PostDetailViewModel ( + private val postRepository : PostRepository +) : ViewModel() { + private val _uiState = MutableStateFlow(PostDetailUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _deleteUiState = MutableStateFlow(DeletePostUiState.Loading) + val deleteUiState: StateFlow = _deleteUiState.asStateFlow() + + fun loadDetail(postId: Long) { + viewModelScope.launch { + _uiState.value = PostDetailUiState.Loading + + postRepository.getPostDetail(postId) + .onSuccess { post -> + _uiState.value = PostDetailUiState.Success(post) + } + .onFailure { exception -> + _uiState.value = PostDetailUiState.Error( + exception.message ?: "error" + ) + } + } + } + + fun deletePost(postId: Long) { + viewModelScope.launch { + _deleteUiState.value = DeletePostUiState.Loading + + postRepository.deletePost(postId) + .onSuccess { deletePost -> + _deleteUiState.value = DeletePostUiState.Success(deletePost) + } + .onFailure { error -> + _deleteUiState.value = DeletePostUiState.Error( + error.message ?: "error" + ) + } + } + } +} \ No newline at end of file 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..cf5407a --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt @@ -0,0 +1,51 @@ +package com.example.kuit6_android_api.ui.post.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.kuit6_android_api.data.model.request.PostCreateRequest +import com.example.kuit6_android_api.data.repository.PostRepository +import com.example.kuit6_android_api.ui.post.state.PostEditUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class PostEditViewModel ( + private val postRepository : PostRepository +) : ViewModel(){ + private val _uiState = MutableStateFlow(PostEditUiState.Loading) // 변경 가능 상태 + val uiState: StateFlow = _uiState.asStateFlow() + + fun editPost( + postId: Long, + request: PostCreateRequest + ){ + viewModelScope.launch { + _uiState.value = PostEditUiState.Loading + + postRepository.editPost(postId, request) + .onSuccess { post -> + _uiState.value = PostEditUiState.Success(post) + } + .onFailure { error -> + _uiState.value = PostEditUiState.Error( + error.message ?: "error" + ) + } + } + } + + fun getPostDetail(postId: Long) { + viewModelScope.launch { + _uiState.value = PostEditUiState.Loading + + postRepository.getPostDetail(postId) + .onSuccess { post -> + _uiState.value = PostEditUiState.Success(post) + } + .onFailure { error -> + _uiState.value = PostEditUiState.Error(error.message ?: "게시글 불러오기 실패") + } + } + } +} \ No newline at end of file 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..30104c8 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt @@ -0,0 +1,40 @@ +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 postRepository : PostRepository +) : ViewModel() { + private val _PostList_uiState = MutableStateFlow(PostListUiState.Loading) // 변경 가능 상태 + val postListUiState: StateFlow = _PostList_uiState.asStateFlow() // 읽기 전용 + init { + loadPosts() + } + + private fun loadPosts() { + viewModelScope.launch { + _PostList_uiState.value = PostListUiState.Loading + + postRepository.getPosts() + .onSuccess { posts-> + _PostList_uiState.value = PostListUiState.Success(posts) + } + .onFailure { error-> + _PostList_uiState.value = PostListUiState.Error( + error.message ?: "error" + ) + } + } + } + + fun refresh() { + loadPosts() + } +} \ No newline at end of file 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 8601526..0000000 --- a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt +++ /dev/null @@ -1,109 +0,0 @@ -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 kotlinx.coroutines.launch -import com.example.kuit6_android_api.data.api.RetrofitClient -import com.example.kuit6_android_api.data.model.response.PostResponse -import com.example.kuit6_android_api.data.model.request.PostCreateRequest - - -class PostViewModel : ViewModel() { - - var posts by mutableStateOf>(emptyList()) - private set - - var postDetail by mutableStateOf(null) - private set - - private val apiService = RetrofitClient.apiService - var uploadedImageUrl by mutableStateOf(null) - private set - - fun getPosts() { - viewModelScope.launch { - runCatching { - apiService.getPosts() - }.onSuccess { response -> - response.data?.let { - if (response.success) { - posts = response.data - } - } - } - } - } - - fun getPostDetail(postId: Long) { - viewModelScope.launch { - runCatching { - apiService.getPostDetail(postId) - }.onSuccess { response -> - response.data?.let{ - if(response.success){ - postDetail = response.data - } - } - } - } - } - - 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 { - apiService.editPost(postId, PostCreateRequest(title, content, imageUrl)) - }.onSuccess{ response-> - if(response.success){ - postDetail = response.data - onSuccess() - } - } - } - } - - fun deletePost(postId: Long, onSuccess: () -> Unit = {}) { - viewModelScope.launch { - runCatching { - apiService.deletePost(postId) - }.onSuccess { response -> - if(response.success){ - onSuccess() - } - } - } - } - - fun clearUploadedImageUrl() { - uploadedImageUrl = null - } - -} 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..a207ea1 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt @@ -0,0 +1,20 @@ +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 + +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 From 215810ed5fc2ac32347181146596e058b3e21460 Mon Sep 17 00:00:00 2001 From: kyungmin Date: Thu, 20 Nov 2025 04:28:04 +0900 Subject: [PATCH 2/5] dd --- .../kuit6_android_api/ui/post/screen/PostListScreen.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 64574c1..fb95faf 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 @@ -18,6 +18,7 @@ import androidx.compose.material3.Scaffold 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 @@ -35,6 +36,10 @@ fun PostListScreen( ) { val uiState by viewModel.postListUiState.collectAsState() + LaunchedEffect(Unit) { + viewModel.refresh() + } + Scaffold( topBar = { TopAppBar( @@ -53,6 +58,7 @@ fun PostListScreen( } is PostListUiState.Success -> { + val posts = (uiState as PostListUiState.Success).posts LazyColumn( modifier = Modifier .fillMaxSize() @@ -61,7 +67,7 @@ fun PostListScreen( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - items((uiState as PostListUiState.Success).posts) { post -> + items(posts) { post -> PostItem( post = post, onClick = { onPostClick(post.id) } From 0da5c309a049b6d9016b4cb07d903e0036879a15 Mon Sep 17 00:00:00 2001 From: kyungmin Date: Thu, 20 Nov 2025 17:00:54 +0900 Subject: [PATCH 3/5] =?UTF-8?q?8=EC=A3=BC=EC=B0=A8=20=EC=8B=A4=EC=8A=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + .../kuit6_android_api/data/api/ApiService.kt | 13 +++ .../kuit6_android_api/data/di/AppContainer.kt | 12 +++ .../data/model/request/LoginRequest.kt | 11 +++ .../data/model/response/LoginResponse.kt | 10 ++ .../data/repository/LoginRepository.kt | 8 ++ .../data/repository/LoginRepositoryImpl.kt | 40 ++++++++ .../data/repository/PostRepository.kt | 1 - .../data/repository/TokenRepository.kt | 8 ++ .../data/repository/TokenRepositoryImpl.kt | 25 +++++ .../ui/navigation/NavGraph.kt | 21 +++++ .../kuit6_android_api/ui/navigation/Routes.kt | 5 +- .../ui/post/screen/LoginScreen.kt | 93 +++++++++++++++++++ .../ui/post/screen/PostListScreen.kt | 24 ++++- .../ui/post/state/LoginUiState.kt | 9 ++ .../ui/post/viewmodel/LoginViewModel.kt | 64 +++++++++++++ .../post/viewmodel/LoginViewModelFactory.kt | 22 +++++ gradle/libs.versions.toml | 2 + 18 files changed, 364 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/model/request/LoginRequest.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/model/response/LoginResponse.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepository.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepository.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/state/LoginUiState.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModelFactory.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 72431d6..cdcc288 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -78,6 +78,7 @@ dependencies { // Coroutines implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.datastore.preferences) //implementation(libs.adapter.guava) testImplementation(libs.junit) diff --git a/app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt b/app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt index 2279732..8b8952d 100644 --- a/app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt +++ b/app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt @@ -1,8 +1,10 @@ package com.example.kuit6_android_api.data.api import android.R.attr.value +import com.example.kuit6_android_api.data.model.request.LoginRequest 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.LoginResponse import com.example.kuit6_android_api.data.model.response.PostResponse import okhttp3.MultipartBody import retrofit2.http.Body @@ -46,4 +48,15 @@ interface ApiService { suspend fun uploadImage( @Part file: MultipartBody.Part ): BaseResponse> + + @POST("/api/auth/signup") + suspend fun signup( + @Body request: LoginRequest + ): BaseResponse + + @POST("/api/auth/login") + suspend fun login( + @Body request: LoginRequest + ): BaseResponse + } \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt b/app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt index e0b797f..7860be4 100644 --- a/app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt +++ b/app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt @@ -2,8 +2,12 @@ package com.example.kuit6_android_api.data.di import com.example.kuit6_android_api.data.api.ApiService import com.example.kuit6_android_api.data.api.RetrofitClient +import com.example.kuit6_android_api.data.repository.LoginRepository +import com.example.kuit6_android_api.data.repository.LoginRepositoryImpl import com.example.kuit6_android_api.data.repository.PostRepository import com.example.kuit6_android_api.data.repository.PostRepositoryImpl +import com.example.kuit6_android_api.data.repository.TokenRepository +import com.example.kuit6_android_api.data.repository.TokenRepositoryImpl class AppContainer { private val apiService: ApiService by lazy { @@ -13,4 +17,12 @@ class AppContainer { val postRepository: PostRepository by lazy{ PostRepositoryImpl(apiService) } + + val loginRepository: LoginRepository by lazy{ + LoginRepositoryImpl(apiService) + } + + val tokenRepository: TokenRepository by lazy{ + TokenRepositoryImpl() + } } diff --git a/app/src/main/java/com/example/kuit6_android_api/data/model/request/LoginRequest.kt b/app/src/main/java/com/example/kuit6_android_api/data/model/request/LoginRequest.kt new file mode 100644 index 0000000..479b213 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/model/request/LoginRequest.kt @@ -0,0 +1,11 @@ +package com.example.kuit6_android_api.data.model.request + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginRequest( + val username:String, + val password: String +) + + diff --git a/app/src/main/java/com/example/kuit6_android_api/data/model/response/LoginResponse.kt b/app/src/main/java/com/example/kuit6_android_api/data/model/response/LoginResponse.kt new file mode 100644 index 0000000..faaf556 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/model/response/LoginResponse.kt @@ -0,0 +1,10 @@ +package com.example.kuit6_android_api.data.model.response + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginResponse( + val token: String, + val userId: Long, + val username: String +) diff --git a/app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepository.kt b/app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepository.kt new file mode 100644 index 0000000..c8c3a86 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepository.kt @@ -0,0 +1,8 @@ +package com.example.kuit6_android_api.data.repository + +import com.example.kuit6_android_api.data.model.response.LoginResponse + +interface LoginRepository { + suspend fun signup(id:String, password: String): Result + suspend fun login(id:String, password: String): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt b/app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt new file mode 100644 index 0000000..927a035 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt @@ -0,0 +1,40 @@ +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.LoginRequest +import com.example.kuit6_android_api.data.model.response.LoginResponse + +class LoginRepositoryImpl( + private val apiService: ApiService +): LoginRepository { + override suspend fun signup(id: String, password: String): Result { + return runCatching { + val response = apiService.signup(LoginRequest(username = id, password = password)) + + if (response.success && response.data != null) { + response.data + } else { + throw Exception(response.message ?: "회원가입 실패") + } + }.onFailure { error -> + Log.e("LoginRepository", error.message.toString()) + } + } + + override suspend fun login(id: String, password: String): Result { + return runCatching { + val response = apiService.login(LoginRequest(username = id, password = password)) + + if (response.success && response.data != null) { + response.data + } else { + throw Exception(response.message ?: "로그인 실패") + } + }.onFailure { error -> + Log.e("LoginRepository", error.message.toString()) + } + } + + } + 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 index c826423..9d57e7e 100644 --- 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 @@ -3,7 +3,6 @@ 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 -import retrofit2.http.Body interface PostRepository{ suspend fun getPosts(): Result> diff --git a/app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepository.kt b/app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepository.kt new file mode 100644 index 0000000..3cbafa1 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepository.kt @@ -0,0 +1,8 @@ +package com.example.kuit6_android_api.data.repository + +import android.content.Context + +interface TokenRepository{ + suspend fun saveToken(context: Context, token: String) + suspend fun getToken(context: Context): String? +} \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt b/app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt new file mode 100644 index 0000000..3328cf8 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt @@ -0,0 +1,25 @@ +package com.example.kuit6_android_api.data.repository + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.first + +val Context.datastore: DataStore by preferencesDataStore("Token") +val KEY = stringPreferencesKey("token") + +class TokenRepositoryImpl(): TokenRepository { + override suspend fun saveToken(context: Context, token: String) { + context.datastore.edit{ + it[KEY] = token + } + } + + override suspend fun getToken(context: Context): String? { + val prefs = context.datastore.data.first() + return prefs[KEY] + } +} \ No newline at end of file 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 3bb3fe0..47a3189 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 @@ -6,14 +6,17 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.toRoute +import com.example.kuit6_android_api.ui.post.screen.LoginScreen 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.LoginViewModel 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.loginViewModelFactory import com.example.kuit6_android_api.ui.post.viewmodel.postViewModelFactory @Composable @@ -34,6 +37,9 @@ fun NavGraph( onCreatePostClick = { navController.navigate(PostCreateRoute) }, + onLoginClick = { + navController.navigate(LoginRoute) + }, viewModel = viewModel( factory = postViewModelFactory { PostListViewModel(postRepository = it) } ) @@ -90,5 +96,20 @@ fun NavGraph( }) ) } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + + LoginScreen( + onNavigateBack = { + navController.popBackStack() + }, + viewModel = viewModel(factory = loginViewModelFactory { loginRepo, tokenRepo -> + LoginViewModel( + loginRepository = loginRepo, + tokenRepository = tokenRepo + ) + }) + ) + } } } diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/navigation/Routes.kt b/app/src/main/java/com/example/kuit6_android_api/ui/navigation/Routes.kt index b2525d3..45438b5 100644 --- a/app/src/main/java/com/example/kuit6_android_api/ui/navigation/Routes.kt +++ b/app/src/main/java/com/example/kuit6_android_api/ui/navigation/Routes.kt @@ -12,4 +12,7 @@ data class PostDetailRoute(val postId: Long) object PostCreateRoute @Serializable -data class PostEditRoute(val postId: Long) \ No newline at end of file +data class PostEditRoute(val postId: Long) + +@Serializable +object LoginRoute \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt new file mode 100644 index 0000000..8e8aacc --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt @@ -0,0 +1,93 @@ +package com.example.kuit6_android_api.ui.post.screen + +import androidx.compose.foundation.layout.Arrangement +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.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.kuit6_android_api.ui.post.viewmodel.LoginViewModel + +@Composable +fun LoginScreen( + modifier: Modifier = Modifier, + onNavigateBack: ()-> Unit, + viewModel: LoginViewModel= viewModel() + ) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current + + Scaffold(Modifier.fillMaxSize()){ innerPadding-> + Column( + Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 20.dp), + Arrangement.Center, + Alignment.CenterHorizontally + + ){ + TextField( + value = uiState.id, + onValueChange = { + viewModel.onIdChanged(it) + }, + Modifier.fillMaxWidth(), + placeholder = {Text("아이디")} + ) + TextField( + value = uiState.password, + onValueChange = { + viewModel.onPasswordChanged(it) + }, + Modifier.fillMaxWidth(), + placeholder = {Text ("비밀번호")} + ) + Row( + Modifier.align(Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + ){ + Checkbox( + checked = uiState.isAutoLogin, + onCheckedChange = { + viewModel.onAutoLoginChanged(it) + } + ) + Text("자동 로그인") + } + Row() { + val context = LocalContext.current + Button(onClick = { + viewModel.login(context) + }) { + Text("로그인") + } + Button(onClick = { + viewModel.signup(context) + }) { + Text("회원가입") + } + } + Text("토큰:${uiState.token} ") + Button(onClick = { + viewModel.getToken(context= context) + }){ + Text("토큰 조회") + } + } + } +} 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 fb95faf..e1b70e8 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 @@ -3,14 +3,18 @@ package com.example.kuit6_android_api.ui.post.screen import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding 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.material.icons.filled.Person import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -23,6 +27,8 @@ 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.navigation.LoginRoute import com.example.kuit6_android_api.ui.post.component.PostItem import com.example.kuit6_android_api.ui.post.state.PostListUiState import com.example.kuit6_android_api.ui.post.viewmodel.PostListViewModel @@ -32,6 +38,7 @@ import com.example.kuit6_android_api.ui.post.viewmodel.PostListViewModel fun PostListScreen( onPostClick: (Long) -> Unit, onCreatePostClick: () -> Unit, + onLoginClick: () -> Unit, viewModel: PostListViewModel ) { val uiState by viewModel.postListUiState.collectAsState() @@ -47,10 +54,21 @@ fun PostListScreen( ) }, floatingActionButton = { - FloatingActionButton(onClick = onCreatePostClick) { - Icon(Icons.Default.Add, contentDescription = "게시글 작성") + Row(modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + Arrangement.SpaceBetween + ) { + //로그인 버튼 + FloatingActionButton(onClick = onLoginClick) { + Icon(Icons.Default.Person, contentDescription = "로그인") + } + FloatingActionButton(onClick = onCreatePostClick) { + Icon(Icons.Default.Add, contentDescription = "게시글 작성") + } } - } + }, + floatingActionButtonPosition = FabPosition.Center //position 잡아주기 ) { paddingValues -> when (uiState) { is PostListUiState.Loading -> { diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/state/LoginUiState.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/LoginUiState.kt new file mode 100644 index 0000000..249753c --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/state/LoginUiState.kt @@ -0,0 +1,9 @@ +package com.example.kuit6_android_api.ui.post.state + +//uiState를 이번에는 data class 로 만들어보자. +data class LoginUiState( + val id: String = "", + val password: String= "", + val isAutoLogin: Boolean= false, + val token: String = "", +) diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt new file mode 100644 index 0000000..d9cd91b --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt @@ -0,0 +1,64 @@ +package com.example.kuit6_android_api.ui.post.viewmodel + +import android.R.attr.password +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.kuit6_android_api.data.repository.LoginRepository +import com.example.kuit6_android_api.data.repository.TokenRepository +import com.example.kuit6_android_api.ui.post.state.LoginUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class LoginViewModel( + private val loginRepository: LoginRepository, + private val tokenRepository: TokenRepository +) : ViewModel() { + private val _uiState = MutableStateFlow(LoginUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun onIdChanged(id: String) { + _uiState.update { + it.copy(id = id) + } + } + + fun onPasswordChanged(password: String) { + _uiState.update { it.copy(password = password) } + } + + fun onAutoLoginChanged(isAutoLogin: Boolean) { + _uiState.update { it.copy(isAutoLogin = isAutoLogin) } + } + + fun signup(context: Context) { + viewModelScope.launch { + loginRepository.signup( + id = uiState.value.id, + password = uiState.value.password + ).onSuccess { + tokenRepository.saveToken(context, it.token) + } + } + } + + fun login(context: Context) { + viewModelScope.launch { + loginRepository.login( + id = uiState.value.id, + password = uiState.value.password + ).onSuccess { + tokenRepository.saveToken(context, it.token) + } + } + } + fun getToken(context: Context){ + viewModelScope.launch{ + val token = tokenRepository.getToken(context) + _uiState.update{it.copy(token = token ?: "")} + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModelFactory.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModelFactory.kt new file mode 100644 index 0000000..67477d7 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModelFactory.kt @@ -0,0 +1,22 @@ +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.LoginRepository +import com.example.kuit6_android_api.data.repository.TokenRepository + +inline fun loginViewModelFactory( + crossinline create: (LoginRepository, TokenRepository) -> VM +) : ViewModelProvider.Factory = viewModelFactory { + initializer { + val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] + as App + + val loginRepository = application.container.loginRepository + val tokenRepository = application.container.tokenRepository + create(loginRepository, tokenRepository) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10c96d0..44a00a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ coil = "2.7.0" # Coroutines coroutines = "1.9.0" adapterGuava = "2.11.0" +datastorePreferences = "1.3.0-alpha01" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -69,6 +70,7 @@ coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coi # Coroutines kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } adapter-guava = { group = "com.squareup.retrofit2", name = "adapter-guava", version.ref = "adapterGuava" } +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From dc9aa90e1e35e7b4860632b76e46c1db70da8812 Mon Sep 17 00:00:00 2001 From: kyungmin Date: Fri, 21 Nov 2025 15:38:20 +0900 Subject: [PATCH 4/5] =?UTF-8?q?AuthInterceptor=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/deploymentTargetSelector.xml | 4 +-- .../java/com/example/kuit6_android_api/App.kt | 2 +- .../kuit6_android_api/data/api/ApiService.kt | 2 ++ .../data/api/AuthInterceptor.kt | 33 +++++++++++++++++++ .../data/api/RetrofitClient.kt | 9 ++++- .../kuit6_android_api/data/di/AppContainer.kt | 18 ++++++---- .../ui/post/screen/LoginScreen.kt | 24 ++++++++++---- 7 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/api/AuthInterceptor.kt diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 086c00d..04f3284 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,10 +4,10 @@