From ec88cfe260d24c1a00024db3dee3006bd6663895 Mon Sep 17 00:00:00 2001 From: AyoungSe0 Date: Thu, 30 Oct 2025 21:41:46 +0900 Subject: [PATCH 1/5] =?UTF-8?q?5=EC=A3=BC=EC=B0=A8=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/gradle.xml | 4 +- app/build.gradle.kts | 16 +++ .../kuit6_android_api/data/api/ApiService.kt | 42 ++++++ .../data/api/RetrofitClient.kt | 30 ++++ .../kuit6_android_api/data/model/Post.kt | 17 --- .../data/model/request/PostCreateRequest.kt | 9 ++ .../data/model/response/AuthorResponse.kt | 9 ++ .../data/model/response/BaseResponse.kt | 10 ++ .../data/model/response/PostResponse.kt | 13 ++ .../ui/post/component/PostItem.kt | 10 +- .../ui/post/screen/PostCreateScreen.kt | 4 +- .../ui/post/viewmodel/PostViewModel.kt | 130 ++++++++---------- 12 files changed, 192 insertions(+), 102 deletions(-) create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt delete mode 100644 app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/model/response/AuthorResponse.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt create mode 100644 app/src/main/java/com/example/kuit6_android_api/data/model/response/PostResponse.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index a2cf405..639c779 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -9,8 +9,8 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0c6dbbc..54940a1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -18,6 +20,13 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + val properties = Properties() + val localPropertiesFile = rootProject.file("local.properties") + if (localPropertiesFile.exists()) { + properties.load(localPropertiesFile.inputStream()) + } + val baseUrl = properties.getProperty("BASE_URL")// ?: "http://10.0.2.2:8080/" + buildConfigField("String", "BASE_URL", "\"$baseUrl\"") } buildTypes { @@ -38,6 +47,7 @@ android { } buildFeatures { compose = true + buildConfig = true } } @@ -74,4 +84,10 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) + + // Retrofit & OkHttp + implementation(libs.retrofit) + implementation(libs.retrofit.converter.gson) + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) } 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 new file mode 100644 index 0000000..8b517c7 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt @@ -0,0 +1,42 @@ +package com.example.kuit6_android_api.data.api + + +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 retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +interface ApiService { + // http://3.34.136.227:8080/api/posts + @GET(value = "/api/posts") + suspend fun getPosts(): BaseResponse> // suspend fun: 일시 중단 될 수도 있는 함수 + + // http://3.34.136.227:8080/api/posts + @POST(value = "/api/posts") + suspend fun createPost( + @Query(value = "author") author: String = "anonymous", + @Body request: PostCreateRequest + ): BaseResponse + + @GET(value = "/api/posts/{id}") + suspend fun getDetail( + @Path("id") id: Long + ): BaseResponse + + @PUT(value = "/api/posts/{id}") + suspend fun updatePost( + @Path(value = "id") id: Long, + @Body request: PostCreateRequest + ): BaseResponse + + @DELETE(value = "/api/posts/{id}") + suspend fun deletePost( + @Path("id") id: Long + ): BaseResponse // 서버가 data 없으면 Unit/Any? 로 받기 +} \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt b/app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt new file mode 100644 index 0000000..bd61bc5 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt @@ -0,0 +1,30 @@ +package com.example.kuit6_android_api.data.api + +import com.example.kuit6_android_api.BuildConfig +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +object RetrofitClient { // 싱글톤 + + private val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + private val okHttpClient = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + + private val retrofit: Retrofit = Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + val apiService: ApiService = retrofit.create(ApiService::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt b/app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt deleted file mode 100644 index e4c1d08..0000000 --- a/app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.kuit6_android_api.data.model - -data class Author( - val id: Long, - val username: String, - val profileImageUrl: String? -) - -data class Post( - val id: Long, - val title: String, - val content: String, - val imageUrl: String?, - val author: Author, - val createdAt: String, - val updatedAt: String -) diff --git a/app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt b/app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt new file mode 100644 index 0000000..a93554e --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt @@ -0,0 +1,9 @@ +package com.example.kuit6_android_api.data.model.request + +import kotlinx.serialization.SerialName + +data class PostCreateRequest( + @SerialName(value = "title") val title: String, + @SerialName(value = "content") val content: String, + @SerialName(value = "imageUrl") val imageUrl: String? // nullable +) \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/data/model/response/AuthorResponse.kt b/app/src/main/java/com/example/kuit6_android_api/data/model/response/AuthorResponse.kt new file mode 100644 index 0000000..2981cdb --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/model/response/AuthorResponse.kt @@ -0,0 +1,9 @@ +package com.example.kuit6_android_api.data.model.response + +import kotlinx.serialization.SerialName + +data class AuthorResponse( + @SerialName(value = "id") val id: Long, + @SerialName(value = "username") val username: String, + @SerialName(value = "profileImageUrl") val profileImageUrl: String? // nullable +) \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt b/app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt new file mode 100644 index 0000000..4359ea2 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt @@ -0,0 +1,10 @@ +package com.example.kuit6_android_api.data.model.response + +import kotlinx.serialization.SerialName + +data class BaseResponse( + @SerialName(value = "success") val success: Boolean, + @SerialName(value = "message") val message: String?, + @SerialName(value = "data") val data: T?, //nullable + @SerialName(value = "timestamp") val timestamp: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/data/model/response/PostResponse.kt b/app/src/main/java/com/example/kuit6_android_api/data/model/response/PostResponse.kt new file mode 100644 index 0000000..a118c24 --- /dev/null +++ b/app/src/main/java/com/example/kuit6_android_api/data/model/response/PostResponse.kt @@ -0,0 +1,13 @@ +package com.example.kuit6_android_api.data.model.response + +import kotlinx.serialization.SerialName + +data class PostResponse( + @SerialName(value = "id") val id: Long, + @SerialName(value = "title") val title: String, + @SerialName(value = "content") val content: String, + @SerialName(value = "imageUrl") val imageUrl: String?, // nullable + @SerialName(value = "author") val author: AuthorResponse, + @SerialName(value = "createdAt") val createdAt: String, + @SerialName(value = "updatedAt") val updatedAt: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/kuit6_android_api/ui/post/component/PostItem.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/component/PostItem.kt index df2e5fe..b5344a8 100644 --- a/app/src/main/java/com/example/kuit6_android_api/ui/post/component/PostItem.kt +++ b/app/src/main/java/com/example/kuit6_android_api/ui/post/component/PostItem.kt @@ -29,13 +29,13 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import com.example.kuit6_android_api.data.model.Author -import com.example.kuit6_android_api.data.model.Post +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.util.formatDateTime @Composable fun PostItem( - post: Post, + post: PostResponse, onClick: () -> Unit ) { Card( @@ -153,12 +153,12 @@ fun PostItem( fun PostItemPreview() { MaterialTheme { PostItem( - post = Post( + post = PostResponse( id = 1, title = "샘플 게시글 제목", content = "이것은 샘플 게시글 내용입니다. 미리보기에서는 두 줄까지만 표시됩니다.", imageUrl = null, - author = Author(1, "testuser", null), + author = AuthorResponse(1, "testuser", null), createdAt = "2025-10-03T12:00:00", updatedAt = "2025-10-03T12:00:00" ), 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 57ea2ab..9989c29 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 @@ -187,8 +187,8 @@ fun PostCreateScreen( Button( onClick = { - val finalAuthor = author.ifBlank { "anonymous" } - viewModel.createPost(finalAuthor, title, content, null) { + val finalAuthor = author//.ifBlank { "anonymous" } + viewModel.createPost(finalAuthor, title, content, null/*viewModel.uploadedImage*/) { onPostCreated() } }, 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 index 444229c..fe17d5b 100644 --- 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 @@ -1,96 +1,73 @@ package com.example.kuit6_android_api.ui.post.viewmodel import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf 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.model.Author -import com.example.kuit6_android_api.data.model.Post -import kotlinx.coroutines.delay +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 java.time.LocalDateTime class PostViewModel : ViewModel() { - - // 더미 데이터 - private val dummyPosts = mutableStateListOf( - Post( - id = 1, - title = "Jetpack Compose 시작하기", - content = "Jetpack Compose는 Android의 최신 UI 툴킷입니다. 선언형 UI로 더 쉽고 빠르게 UI를 만들 수 있습니다.", - imageUrl = null, - author = Author(1, "개발자A", null), - createdAt = "2025-10-05T10:00:00", - updatedAt = "2025-10-05T10:00:00" - ), - Post( - id = 2, - title = "Kotlin Coroutines 완벽 가이드", - content = "비동기 프로그래밍을 쉽게! Coroutines를 사용하면 복잡한 비동기 코드를 간단하게 작성할 수 있습니다.", - imageUrl = null, - author = Author(2, "개발자B", null), - createdAt = "2025-10-05T11:30:00", - updatedAt = "2025-10-05T11:30:00" - ), - Post( - id = 3, - title = "Android MVVM 아키텍처", - content = "MVVM 패턴으로 코드를 구조화하면 테스트와 유지보수가 쉬워집니다. ViewModel과 LiveData/StateFlow를 활용해봅시다.", - imageUrl = null, - author = Author(1, "개발자A", null), - createdAt = "2025-10-05T14:20:00", - updatedAt = "2025-10-05T14:20:00" - ) - ) - - private var nextId = 4L - - var posts by mutableStateOf>(emptyList()) + var posts by mutableStateOf>(emptyList()) private set - var postDetail by mutableStateOf(null) + var postDetail by mutableStateOf(null) private set var uploadedImageUrl by mutableStateOf(null) private set + private val apiService = RetrofitClient.apiService fun getPosts() { viewModelScope.launch { - delay(500) // 네트워크 시뮬레이션 - posts = dummyPosts.toList() + runCatching { + apiService.getPosts() + }.onSuccess { response -> + response.data?.let { + if (response.success) { + posts = response.data + } + } + } } } fun getPostDetail(postId: Long) { viewModelScope.launch { - delay(300) - postDetail = dummyPosts.find { it.id == postId } + runCatching { + apiService.getDetail(postId) + }.onSuccess { response -> + response.data?.let { + if (response.success){ + postDetail = response.data + } + } + } } } fun createPost( - author: String = "anonymous", + author: String,// = "anonymous", title: String, content: String, imageUrl: String? = null, onSuccess: () -> Unit = {} ) { viewModelScope.launch { - delay(500) - val newPost = Post( - id = nextId++, - title = title, - content = content, - imageUrl = imageUrl, - author = Author(nextId, author, null), - createdAt = getCurrentDateTime(), - updatedAt = getCurrentDateTime() - ) - dummyPosts.add(0, newPost) - posts = dummyPosts.toList() - onSuccess() + runCatching { + val request = PostCreateRequest(title, content, imageUrl) + apiService.createPost(author, request) + }.onSuccess { response -> + if (response.success) { + // 이미지 업로드 관련 코드 + clearUploadedImageUrl() + onSuccess() + } + } } } @@ -101,31 +78,32 @@ class PostViewModel : ViewModel() { imageUrl: String? = null, onSuccess: () -> Unit = {} ) { + // 수정 후 게시글 상세가 나오게 viewModelScope.launch { - delay(500) - val index = dummyPosts.indexOfFirst { it.id == postId } - if (index != -1) { - val oldPost = dummyPosts[index] - val updatedPost = oldPost.copy( - title = title, - content = content, - imageUrl = imageUrl, - updatedAt = getCurrentDateTime() - ) - dummyPosts[index] = updatedPost - postDetail = updatedPost - posts = dummyPosts.toList() - onSuccess() + runCatching { + val request = PostCreateRequest(title, content, imageUrl) + apiService.updatePost(postId, request) + }.onSuccess { response -> + if(response.success && response.data != null){ + postDetail = response.data + onSuccess() + } } } } fun deletePost(postId: Long, onSuccess: () -> Unit = {}) { + // 삭제 후 게시글 목록 나오게 viewModelScope.launch { - delay(300) - dummyPosts.removeIf { it.id == postId } - posts = dummyPosts.toList() - onSuccess() + runCatching { + apiService.deletePost(postId) + }.onSuccess { response -> + if(response.success){ + postDetail = null + getPosts() + onSuccess() + } + } } } From 5166125684b0530b19e4307a19fd8d5c901b320d Mon Sep 17 00:00:00 2001 From: AyoungSe0 Date: Fri, 31 Oct 2025 00:38:42 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EB=B6=80=EB=B6=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kuit6_android_api/data/api/ApiService.kt | 9 ++ .../ui/post/screen/PostCreateScreen.kt | 95 ++++++++++++++++++- .../ui/post/viewmodel/PostViewModel.kt | 87 ++++++++++++++++- 3 files changed, 184 insertions(+), 7 deletions(-) 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 8b517c7..a264c8d 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 @@ -4,11 +4,14 @@ package com.example.kuit6_android_api.data.api 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 import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.PUT +import retrofit2.http.Part import retrofit2.http.Path import retrofit2.http.Query @@ -39,4 +42,10 @@ interface ApiService { suspend fun deletePost( @Path("id") id: Long ): BaseResponse // 서버가 data 없으면 Unit/Any? 로 받기 + + @Multipart + @POST("/api/images/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/ui/post/screen/PostCreateScreen.kt b/app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt index 9989c29..c7ce6ce 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 @@ -5,6 +5,7 @@ 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.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -12,15 +13,18 @@ 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.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.Close import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon @@ -39,10 +43,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 @OptIn(ExperimentalMaterial3Api::class) @@ -52,6 +60,7 @@ fun PostCreateScreen( onPostCreated: () -> Unit, viewModel: PostViewModel = viewModel() ) { + val context = LocalContext.current var author by remember { mutableStateOf("") } var title by remember { mutableStateOf("") } var content by remember { mutableStateOf("") } @@ -60,7 +69,20 @@ fun PostCreateScreen( val imagePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri: Uri? -> - selectedImageUri = uri + uri?.let { + selectedImageUri = it + // 이미지 선택 시 자동으로 업로드 + viewModel.uploadImage( + context = context, + uri = it, + onSuccess = { imageUrl -> + // 업로드 성공 처리는 ViewModel에서 자동으로 됨(viewModel.uploadedImageUrl) + }, + onError = { error -> + // 에러 처리 (필요시 Toast 등으로 표시) + } + ) + } } Scaffold( @@ -171,7 +193,7 @@ fun PostCreateScreen( color = MaterialTheme.colorScheme.onSurface ) - if (selectedImageUri == null) { + if (selectedImageUri == null && !viewModel.isUploading) { FilledTonalButton( onClick = { imagePickerLauncher.launch("image/*") }, shape = RoundedCornerShape(10.dp) @@ -180,6 +202,68 @@ fun PostCreateScreen( } } } + + // 업로드 중 표시 + if (viewModel.isUploading) { + Spacer(modifier = Modifier.height(16.dp)) + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + modifier = Modifier.size(40.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "업로드 중...", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + // 이미지 미리보기 + if (selectedImageUri != null && viewModel.uploadedImageUrl != null) { + Spacer(modifier = Modifier.height(16.dp)) + Box( + modifier = Modifier.fillMaxWidth() + ) { + AsyncImage( + model = selectedImageUri, + contentDescription = "선택된 이미지", + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .clip(RoundedCornerShape(12.dp)), + contentScale = ContentScale.Crop + ) + + // 삭제 버튼 + IconButton( + onClick = { + selectedImageUri = null + viewModel.clearUploadedImageUrl() + }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + .background( + MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), + RoundedCornerShape(20.dp) + ) + ) { + Icon( + Icons.Default.Close, + contentDescription = "이미지 제거", + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + } } } @@ -188,7 +272,12 @@ fun PostCreateScreen( Button( onClick = { val finalAuthor = author//.ifBlank { "anonymous" } - viewModel.createPost(finalAuthor, title, content, null/*viewModel.uploadedImage*/) { + viewModel.createPost( + finalAuthor, + title, + content, + imageUrl = viewModel.uploadedImageUrl + ) { onPostCreated() } }, 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 index fe17d5b..8eb0cea 100644 --- 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 @@ -1,5 +1,8 @@ package com.example.kuit6_android_api.ui.post.viewmodel +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -9,6 +12,11 @@ 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 +import java.io.File +import java.io.FileOutputStream import java.time.LocalDateTime class PostViewModel : ViewModel() { @@ -21,6 +29,9 @@ class PostViewModel : ViewModel() { var uploadedImageUrl by mutableStateOf(null) private set + var isUploading by mutableStateOf(false) + private set + private val apiService = RetrofitClient.apiService fun getPosts() { viewModelScope.launch { @@ -42,7 +53,7 @@ class PostViewModel : ViewModel() { apiService.getDetail(postId) }.onSuccess { response -> response.data?.let { - if (response.success){ + if (response.success) { postDetail = response.data } } @@ -54,7 +65,7 @@ class PostViewModel : ViewModel() { author: String,// = "anonymous", title: String, content: String, - imageUrl: String? = null, + imageUrl: String? = uploadedImageUrl, onSuccess: () -> Unit = {} ) { viewModelScope.launch { @@ -84,7 +95,7 @@ class PostViewModel : ViewModel() { val request = PostCreateRequest(title, content, imageUrl) apiService.updatePost(postId, request) }.onSuccess { response -> - if(response.success && response.data != null){ + if (response.success && response.data != null) { postDetail = response.data onSuccess() } @@ -98,7 +109,7 @@ class PostViewModel : ViewModel() { runCatching { apiService.deletePost(postId) }.onSuccess { response -> - if(response.success){ + if (response.success) { postDetail = null getPosts() onSuccess() @@ -107,6 +118,74 @@ class PostViewModel : ViewModel() { } } + // URI를 File로 변환하는 헬퍼 함수 + private fun uriToFile(context: Context, uri: Uri): File? { + return try { + val contentResolver = context.contentResolver + val fileName = getFileName(context, uri) ?: "image_${System.currentTimeMillis()}.jpg" + val tempFile = File(context.cacheDir, fileName) + + contentResolver.openInputStream(uri)?.use { input -> + FileOutputStream(tempFile).use { output -> + input.copyTo(output) + } + } + tempFile + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + private fun getFileName(context: Context, uri: Uri): String? { + var fileName: String? = null + val cursor = context.contentResolver.query(uri, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (nameIndex != -1) { + fileName = it.getString(nameIndex) + } + } + } + return fileName + } + + // 이미지 업로드 함수 + fun uploadImage( + context: Context, + uri: Uri, + onSuccess: (String) -> Unit = {}, + onError: (String) -> Unit = {} + ) { + viewModelScope.launch { + isUploading = true + runCatching { + val file = 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 ?: "업로드 실패") + } + } + } + fun clearUploadedImageUrl() { uploadedImageUrl = null } From 14b330fe165f6daed5cb2e6d228d860dacbc8835 Mon Sep 17 00:00:00 2001 From: AyoungSe0 Date: Fri, 31 Oct 2025 01:16:49 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EA=B4=80=EB=A0=A8=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/post/screen/PostEditScreen.kt | 35 ++++++++++++++++--- .../ui/post/viewmodel/PostViewModel.kt | 2 +- 2 files changed, 32 insertions(+), 5 deletions(-) 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 0e5eaca..5cf4b00 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 @@ -40,6 +40,7 @@ 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.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -54,17 +55,30 @@ fun PostEditScreen( onPostUpdated: () -> Unit, viewModel: PostViewModel = viewModel() ) { + var removeImage by remember { mutableStateOf(false) } + val post = viewModel.postDetail var title by remember { mutableStateOf("") } var content by remember { mutableStateOf("") } var selectedImageUri by remember { mutableStateOf(null) } var isLoaded by remember { mutableStateOf(false) } + val context = LocalContext.current val imagePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri: Uri? -> - selectedImageUri = uri + if (uri != null) { + selectedImageUri = uri // 미리보기용 + // ★ 선택 즉시 업로드해서 서버에 보낼 URL을 확보 + viewModel.uploadImage( + context = context, + uri = uri, + onSuccess = { /* 필요시 스낵바/토스트 */ }, + onError = { /* 에러 안내 */ } + ) + removeImage = false // 아래 2)에서 추가할 플래그 + } } LaunchedEffect(postId) { @@ -151,7 +165,7 @@ fun PostEditScreen( Spacer(modifier = Modifier.height(12.dp)) - if (selectedImageUri != null || post.imageUrl != null) { + if (!removeImage && (selectedImageUri != null || post.imageUrl != null)) { Box( modifier = Modifier .fillMaxWidth() @@ -171,7 +185,11 @@ fun PostEditScreen( contentScale = ContentScale.Crop ) IconButton( - onClick = { selectedImageUri = null }, + onClick = { + selectedImageUri = null + viewModel.clearUploadedImageUrl() + removeImage = true + }, modifier = Modifier .align(Alignment.TopEnd) .padding(8.dp) @@ -202,7 +220,16 @@ fun PostEditScreen( Button( onClick = { - viewModel.updatePost(postId, title, content, null) { + val finalImageUrl = + when { + removeImage -> null // 삭제 + viewModel.uploadedImageUrl != null -> viewModel.uploadedImageUrl // 교체 + else -> viewModel.postDetail?.imageUrl // 유지 + } + + viewModel.updatePost(postId, title, content, finalImageUrl) { + viewModel.clearUploadedImageUrl() + removeImage = false onPostUpdated() } }, 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 index 8eb0cea..473c05b 100644 --- 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 @@ -86,7 +86,7 @@ class PostViewModel : ViewModel() { postId: Long, title: String, content: String, - imageUrl: String? = null, + imageUrl: String?, onSuccess: () -> Unit = {} ) { // 수정 후 게시글 상세가 나오게 From 4084b6088da9f9e4b697da4f86d4e2aef6d2a4e4 Mon Sep 17 00:00:00 2001 From: AyoungSe0 Date: Fri, 31 Oct 2025 01:29:23 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=EB=AF=B8=EC=85=98=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kuit6_android_api/ui/post/screen/PostEditScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5cf4b00..74e0ad6 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 @@ -70,14 +70,14 @@ fun PostEditScreen( ) { uri: Uri? -> if (uri != null) { selectedImageUri = uri // 미리보기용 - // ★ 선택 즉시 업로드해서 서버에 보낼 URL을 확보 + // 선택 즉시 업로드해서 서버에 보낼 URL을 확보 viewModel.uploadImage( context = context, uri = uri, onSuccess = { /* 필요시 스낵바/토스트 */ }, onError = { /* 에러 안내 */ } ) - removeImage = false // 아래 2)에서 추가할 플래그 + removeImage = false } } From 00583d05b738a7e6ec4210ee26ba3458edeace8c Mon Sep 17 00:00:00 2001 From: AyoungSe0 Date: Wed, 5 Nov 2025 01:26:50 +0900 Subject: [PATCH 5/5] =?UTF-8?q?6=EC=A3=BC=EC=B0=A8=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/kuit6_android_api/MainActivity.kt | 25 ++++++++++++++++--- .../kuit6_android_api/data/api/ApiService.kt | 6 +++++ .../ui/navigation/NavGraph.kt | 13 +++++++--- .../ui/post/screen/PostCreateScreen.kt | 11 +++++++- .../ui/post/screen/PostDetailScreen.kt | 9 ++++++- .../ui/post/screen/PostEditScreen.kt | 9 ++++++- 6 files changed, 62 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/example/kuit6_android_api/MainActivity.kt b/app/src/main/java/com/example/kuit6_android_api/MainActivity.kt index 5129dff..bdb11e1 100644 --- a/app/src/main/java/com/example/kuit6_android_api/MainActivity.kt +++ b/app/src/main/java/com/example/kuit6_android_api/MainActivity.kt @@ -1,6 +1,7 @@ package com.example.kuit6_android_api import android.Manifest +import android.annotation.SuppressLint import android.content.pm.PackageManager import android.os.Build import android.os.Bundle @@ -8,9 +9,14 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize 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.runtime.remember import androidx.compose.ui.Modifier import androidx.core.content.ContextCompat import androidx.navigation.compose.rememberNavController @@ -32,6 +38,7 @@ class MainActivity : ComponentActivity() { } } + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -40,15 +47,23 @@ class MainActivity : ComponentActivity() { setContent { KUIT6_Android_APITheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background + + val snackBarState = remember { SnackbarHostState() } + + Scaffold( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + snackbarHost = { + SnackbarHost(snackBarState) + } ) { val navController = rememberNavController() NavGraph( navController = navController, - startDestination = PostListRoute + startDestination = PostListRoute, + snackBarState = snackBarState ) } } @@ -71,6 +86,7 @@ class MainActivity : ComponentActivity() { ) == PackageManager.PERMISSION_GRANTED -> { // 이미 권한이 있음 } + shouldShowRequestPermissionRationale(permission) -> { // 권한 거부 이력이 있음 - 설명 표시 후 재요청 Toast.makeText( @@ -80,6 +96,7 @@ class MainActivity : ComponentActivity() { ).show() requestPermissionLauncher.launch(permission) } + else -> { // 권한 요청 requestPermissionLauncher.launch(permission) 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 a264c8d..ff80c60 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 @@ -16,10 +16,12 @@ import retrofit2.http.Path import retrofit2.http.Query interface ApiService { + // 게시글 목록 조회 // http://3.34.136.227:8080/api/posts @GET(value = "/api/posts") suspend fun getPosts(): BaseResponse> // suspend fun: 일시 중단 될 수도 있는 함수 + // 게시글 생성 // http://3.34.136.227:8080/api/posts @POST(value = "/api/posts") suspend fun createPost( @@ -27,22 +29,26 @@ interface ApiService { @Body request: PostCreateRequest ): BaseResponse + // 게시글 상세 조회 @GET(value = "/api/posts/{id}") suspend fun getDetail( @Path("id") id: Long ): BaseResponse + // 게시글 수정 [스낵바 - 6주차 미션] @PUT(value = "/api/posts/{id}") suspend fun updatePost( @Path(value = "id") id: Long, @Body request: PostCreateRequest ): BaseResponse + // 게시글 삭제 [스낵바 - 6주차 미션] @DELETE(value = "/api/posts/{id}") suspend fun deletePost( @Path("id") id: Long ): BaseResponse // 서버가 data 없으면 Unit/Any? 로 받기 + // 이미지 업로드 @Multipart @POST("/api/images/upload") suspend fun uploadImage( 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 07aa322..975565e 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,5 +1,6 @@ package com.example.kuit6_android_api.ui.navigation +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -13,7 +14,8 @@ import com.example.kuit6_android_api.ui.post.screen.PostListScreen @Composable fun NavGraph( navController: NavHostController, - startDestination: Any = PostListRoute + startDestination: Any = PostListRoute, + snackBarState: SnackbarHostState ) { NavHost( navController = navController, @@ -40,7 +42,8 @@ fun NavGraph( }, onEditClick = { postId -> navController.navigate(PostEditRoute(postId)) - } + }, + snackBarState = snackBarState ) } @@ -51,7 +54,8 @@ fun NavGraph( }, onPostCreated = { navController.popBackStack() - } + }, + snackBarState = snackBarState ) } @@ -65,7 +69,8 @@ fun NavGraph( }, onPostUpdated = { navController.popBackStack() - } + }, + 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 c7ce6ce..2b53d47 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 @@ -33,6 +33,7 @@ import androidx.compose.material3.MaterialTheme 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.material3.TopAppBarDefaults @@ -40,6 +41,7 @@ import androidx.compose.runtime.Composable 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 @@ -52,12 +54,14 @@ 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 @OptIn(ExperimentalMaterial3Api::class) @Composable fun PostCreateScreen( onNavigateBack: () -> Unit, onPostCreated: () -> Unit, + snackBarState: SnackbarHostState, viewModel: PostViewModel = viewModel() ) { val context = LocalContext.current @@ -65,6 +69,7 @@ fun PostCreateScreen( 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() @@ -279,6 +284,9 @@ fun PostCreateScreen( imageUrl = viewModel.uploadedImageUrl ) { onPostCreated() + // ViewModel 에서 멘트를 받아서 전달하는 식으로 많이 구성. flow 활용 + // 현재: onSuccess 람다로 넣어주는 식으로 구현 + scope.launch { snackBarState.showSnackbar("게시글이 작성되었습니다.") } } }, modifier = Modifier @@ -315,7 +323,8 @@ fun PostCreateScreenPreview() { MaterialTheme { PostCreateScreen( onNavigateBack = {}, - onPostCreated = {} + 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 6635ced..1aecc75 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,6 +25,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -34,6 +35,7 @@ import androidx.compose.runtime.LaunchedEffect 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 @@ -46,6 +48,7 @@ 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.util.formatDateTime +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -53,10 +56,12 @@ fun PostDetailScreen( postId: Long, onNavigateBack: () -> Unit, onEditClick: (Long) -> Unit = {}, + snackBarState: SnackbarHostState, viewModel: PostViewModel = viewModel() ) { val post = viewModel.postDetail var showDeleteDialog by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() LaunchedEffect(postId) { viewModel.getPostDetail(postId) @@ -197,6 +202,7 @@ fun PostDetailScreen( viewModel.deletePost(postId) { showDeleteDialog = false onNavigateBack() + scope.launch { snackBarState.showSnackbar("게시글이 삭제되었습니다.") } } }) { Text("삭제") @@ -219,7 +225,8 @@ fun PostDetailScreenPreview() { PostDetailScreen( postId = 1L, onNavigateBack = {}, - onEditClick = {} + 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 74e0ad6..bd8769d 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 @@ -28,6 +28,7 @@ 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 @@ -36,6 +37,7 @@ import androidx.compose.runtime.LaunchedEffect 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 @@ -46,6 +48,7 @@ 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 @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -53,6 +56,7 @@ fun PostEditScreen( postId: Long, onNavigateBack: () -> Unit, onPostUpdated: () -> Unit, + snackBarState: SnackbarHostState, viewModel: PostViewModel = viewModel() ) { var removeImage by remember { mutableStateOf(false) } @@ -64,6 +68,7 @@ fun PostEditScreen( var selectedImageUri by remember { mutableStateOf(null) } var isLoaded by remember { mutableStateOf(false) } val context = LocalContext.current + val scope = rememberCoroutineScope() val imagePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() @@ -231,6 +236,7 @@ fun PostEditScreen( viewModel.clearUploadedImageUrl() removeImage = false onPostUpdated() + scope.launch { snackBarState.showSnackbar("게시글이 수정되었습니다.") } } }, modifier = Modifier @@ -262,7 +268,8 @@ fun PostEditScreenPreview() { PostEditScreen( postId = 1L, onNavigateBack = {}, - onPostUpdated = {} + onPostUpdated = {}, + snackBarState = remember { SnackbarHostState() } ) } }