-
Notifications
You must be signed in to change notification settings - Fork 10
6주차 미션 / 안드로이드 2조 서아영 #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
📋 개요API 클라이언트 인프라(Retrofit)를 통합하고, 하드코딩된 게시물 데이터 모델을 API 응답 모델로 교체하며, UI 화면 전체에 걸쳐 스낵바 상태 관리를 추가하고, 멀티파트 지원을 포함한 이미지 업로드 기능을 구현했습니다. 📝 변경 사항
🔄 시퀀스 다이어그램sequenceDiagram
participant User
participant PostCreateScreen as PostCreateScreen
participant PostViewModel as PostViewModel
participant ApiService as ApiService
rect rgb(200, 220, 255)
Note over PostCreateScreen,ApiService: 이미지 선택 및 업로드
User->>PostCreateScreen: 이미지 선택
PostCreateScreen->>PostViewModel: uploadImage(context, uri)
PostViewModel->>PostViewModel: uriToFile() 변환
PostViewModel->>ApiService: uploadImage(multipart)
ApiService-->>PostViewModel: 업로드된 imageUrl 반환
PostViewModel->>PostViewModel: uploadedImageUrl 저장
PostCreateScreen->>PostCreateScreen: 이미지 미리보기 표시
end
rect rgb(220, 255, 220)
Note over PostCreateScreen,ApiService: 게시물 생성
User->>PostCreateScreen: 게시물 생성 버튼 클릭
PostCreateScreen->>PostViewModel: createPost(title, content, uploadedImageUrl)
PostViewModel->>ApiService: createPost(author, request)
ApiService-->>PostViewModel: BaseResponse<PostResponse>
PostViewModel->>PostCreateScreen: 콜백 실행
PostCreateScreen->>PostCreateScreen: 스낵바 표시 성공 메시지
end
sequenceDiagram
participant User
participant PostDetailScreen as PostDetailScreen
participant PostViewModel as PostViewModel
participant ApiService as ApiService
participant MainActivity as MainActivity
rect rgb(255, 200, 200)
Note over PostDetailScreen,MainActivity: 게시물 삭제 흐름
User->>PostDetailScreen: 삭제 버튼 클릭
PostDetailScreen->>PostViewModel: deletePost(postId)
PostViewModel->>ApiService: deletePost(id)
ApiService-->>PostViewModel: 성공 응답
PostViewModel->>PostViewModel: postDetail 초기화
PostViewModel->>PostViewModel: getPosts() 재호출
PostViewModel->>PostDetailScreen: 콜백 실행
PostDetailScreen->>MainActivity: SnackbarHostState.showSnackbar()
MainActivity->>User: 삭제 성공 메시지 표시
end
🎯 코드 리뷰 예상 난이도🎯 4 (복잡함) | ⏱️ ~50분 세부 검토 항목:
📜 축하 시
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
@alswlekk: I'll review the changes in this PR now. ✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 12
♻️ Duplicate comments (2)
app/src/main/java/com/example/kuit6_android_api/data/model/response/PostResponse.kt (1)
5-13: @serializable 어노테이션이 누락되었습니다.다른 응답 모델들과 동일한 직렬화 불일치 문제입니다.
AuthorResponse및BaseResponse와 함께 수정이 필요합니다.kotlinx-serialization 사용 시:
package com.example.kuit6_android_api.data.model.response import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +@Serializable 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?, @SerialName(value = "author") val author: AuthorResponse, @SerialName(value = "createdAt") val createdAt: String, @SerialName(value = "updatedAt") val updatedAt: String )app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt (1)
5-9: @serializable 어노테이션이 누락되었습니다.요청 모델도 응답 모델들과 동일한 직렬화 불일치 문제가 있습니다. Retrofit의 Gson 컨버터가
@SerialName어노테이션을 인식하지 못하므로 API 요청이 제대로 작동하지 않을 수 있습니다.kotlinx-serialization 사용 시:
package com.example.kuit6_android_api.data.model.request import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +@Serializable data class PostCreateRequest( @SerialName(value = "title") val title: String, @SerialName(value = "content") val content: String, @SerialName(value = "imageUrl") val imageUrl: String? )Gson 사용 시:
package com.example.kuit6_android_api.data.model.request -import kotlinx.serialization.SerialName +import com.google.gson.annotations.SerializedName data class PostCreateRequest( - @SerialName(value = "title") val title: String, - @SerialName(value = "content") val content: String, - @SerialName(value = "imageUrl") val imageUrl: String? + @SerializedName("title") val title: String, + @SerializedName("content") val content: String, + @SerializedName("imageUrl") val imageUrl: String? )
🧹 Nitpick comments (4)
app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt (1)
12-14: 로깅 레벨을 빌드 타입에 따라 조정하는 것을 고려하세요.현재
HttpLoggingInterceptor가BODY레벨로 설정되어 있어 개발 중에는 유용하지만, 프로덕션 빌드에서는 민감한 정보가 로그에 노출될 수 있습니다.빌드 타입에 따라 로깅 레벨을 조정하는 것을 권장합니다:
private val loggingInterceptor = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY + level = if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + } }app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt (1)
21-22: 코드 스타일 개선: 어노테이션에서 불필요한value파라미터 제거Retrofit 어노테이션에서
value파라미터명은 생략 가능합니다. 더 간결한 코드를 위해 제거를 권장합니다.다음과 같이 수정할 수 있습니다:
- @GET(value = "/api/posts") + @GET("/api/posts") suspend fun getPosts(): BaseResponse<List<PostResponse>> - @POST(value = "/api/posts") + @POST("/api/posts") suspend fun createPost( - @Query(value = "author") author: String = "anonymous", + @Query("author") author: String = "anonymous", @Body request: PostCreateRequest ): BaseResponse<PostResponse> - @GET(value = "/api/posts/{id}") + @GET("/api/posts/{id}") suspend fun getDetail( @Path("id") id: Long ): BaseResponse<PostResponse> - @PUT(value = "/api/posts/{id}") + @PUT("/api/posts/{id}") suspend fun updatePost( - @Path(value = "id") id: Long, + @Path("id") id: Long, @Body request: PostCreateRequest ): BaseResponse<PostResponse> - @DELETE(value = "/api/posts/{id}") + @DELETE("/api/posts/{id}") suspend fun deletePost( @Path("id") id: Long ): BaseResponse<Unit>Also applies to: 26-26, 33-33, 39-39, 46-46
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt (2)
122-152: 파일 변환 로직의 에러 처리 개선 가능
uriToFile()함수는 예외 발생 시null을 반환하지만, 구체적인 에러 원인이 로그에만 출력됩니다. 더 나은 에러 전파를 위해 예외를 다시 던지는 것을 고려해보세요.현재 구현은 동작하지만, 다음과 같이 개선할 수 있습니다:
- private fun uriToFile(context: Context, uri: Uri): File? { - return try { + private fun uriToFile(context: Context, uri: Uri): File { 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) } - } + } ?: throw IllegalStateException("URI로부터 InputStream을 열 수 없습니다") + tempFile - } catch (e: Exception) { - e.printStackTrace() - null - } }그리고
uploadImage()함수의 Line 165에서:- val file = uriToFile(context, uri) - if (file == null) { - throw Exception("파일 변환 실패") - } + val file = uriToFile(context, uri)
32-33: 로딩 상태 관리 추가
isUploading상태를 이미지 업로드에만 사용하고 있습니다. 일관성을 위해 다른 API 작업(게시글 생성, 수정, 삭제)에도 유사한 로딩 상태 관리를 고려해보세요.예시:
var isLoading by mutableStateOf(false) private set그리고 각 API 호출 전후로 상태를 업데이트하면 UI에서 통합된 로딩 표시가 가능합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
.idea/gradle.xml(1 hunks)app/build.gradle.kts(4 hunks)app/src/main/java/com/example/kuit6_android_api/MainActivity.kt(5 hunks)app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt(0 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/response/AuthorResponse.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/response/PostResponse.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt(5 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/component/PostItem.kt(2 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt(5 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt(5 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt(6 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt(1 hunks)
💤 Files with no reviewable changes (1)
- app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt
🔇 Additional comments (7)
.idea/gradle.xml (1)
12-13: LGTM!프로젝트 루트와 app 모듈에 대한 Gradle 설정이 올바르게 업데이트되었습니다.
app/build.gradle.kts (1)
88-92: LGTM!Retrofit 및 OkHttp 관련 의존성이 올바르게 추가되었습니다. 다만
RetrofitClient.kt에서 사용 중인 Gson 컨버터와 데이터 모델의 kotlinx.serialization 어노테이션 간 불일치 문제는 별도 코멘트를 참고해주세요.app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (2)
3-3: LGTM!Snackbar 상태 관리를 위한
SnackbarHostState파라미터가 올바르게 추가되었습니다. PR 목표(삭제 성공, 수정 성공 시 Snackbar 표시)를 달성하기 위한 적절한 구현입니다.Also applies to: 18-18
45-46: LGTM!
snackBarState가 필요한 세 개의 화면(PostDetailScreen, PostCreateScreen, PostEditScreen)에 올바르게 전달되고 있습니다.Also applies to: 57-58, 72-73
app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt (1)
16-21: LGTM!OkHttpClient 설정이 적절합니다. 로깅 인터셉터와 타임아웃 설정이 합리적입니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt (1)
155-187: 이미지 업로드 함수가 잘 구현되었습니다이 함수는 다른 API 호출과 달리
onFailure블록을 포함하여 에러를 적절히 처리하고 있으며,isUploading상태를 통해 로딩 상태도 관리하고 있습니다. 좋은 구현입니다.app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt (1)
27-29: 원본 리뷰 댓글이 부정확합니다.Retrofit 인터페이스의 기본 파라미터 동작에 대한 우려는 타당한 지적이지만, 이 코드베이스에서는 실제 문제가 되지 않습니다. 모든 호출 지점에서
author파라미터가 명시적으로 전달되므로 기본값이 필요하지 않습니다:
- ApiService.kt:74 -
apiService.createPost(author, request): author 항상 명시적 전달- PostViewModel.kt:65 -
author: String: 필수 파라미터 (기본값 없음)- PostCreateScreen.kt:279-280 -
finalAuthor생성 후 명시적 전달ApiService의 기본값
= "anonymous"는 방어적 조치일 뿐 실제로 사용되지 않으며, 코드는 정상 동작합니다.Likely an incorrect or invalid review comment.
| 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\"") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BASE_URL null 안전성 문제를 해결해야 합니다.
local.properties에 BASE_URL 속성이 없으면 baseUrl이 null이 되어 빌드 에러가 발생합니다. 주석 처리된 기본값을 보아 fallback 값을 고려했던 것으로 보입니다.
다음과 같이 수정하여 null 안전성을 확보하세요:
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/"
+ val baseUrl = properties.getProperty("BASE_URL") ?: "http://10.0.2.2:8080/"
buildConfigField("String", "BASE_URL", "\"$baseUrl\"")또는 더 명확하게 에러를 발생시키려면:
- val baseUrl = properties.getProperty("BASE_URL")// ?: "http://10.0.2.2:8080/"
+ val baseUrl = properties.getProperty("BASE_URL")
+ ?: throw GradleException("BASE_URL not found in local.properties")
buildConfigField("String", "BASE_URL", "\"$baseUrl\"")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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\"") | |
| 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\"") |
🤖 Prompt for AI Agents
In app/build.gradle.kts around lines 23 to 29, BASE_URL may be null when
local.properties lacks that key; update the code to ensure null-safety by
reading the property with a fallback or explicit error: obtain baseUrl =
properties.getProperty("BASE_URL") ?: "http://10.0.2.2:8080/" (or throw
IllegalStateException("BASE_URL not set in local.properties") if you prefer
failing fast) and then pass that non-null value into buildConfigField so the
build never receives a null BASE_URL.
| private val retrofit: Retrofit = Retrofit.Builder() | ||
| .baseUrl(BuildConfig.BASE_URL) | ||
| .client(okHttpClient) | ||
| .addConverterFactory(GsonConverterFactory.create()) | ||
| .build() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
직렬화 라이브러리 불일치 문제를 해결해야 합니다.
GsonConverterFactory를 사용하고 있지만, 모든 데이터 모델(AuthorResponse, PostResponse, BaseResponse, PostCreateRequest)은 kotlinx.serialization의 @SerialName 어노테이션을 사용합니다. Gson은 @SerialName을 인식하지 못하므로 직렬화/역직렬화가 제대로 작동하지 않습니다.
해결 방법 1 (권장): kotlinx-serialization 컨버터 사용
build.gradle.kts에 의존성 추가:
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")RetrofitClient 수정:
package com.example.kuit6_android_api.data.api
import com.example.kuit6_android_api.BuildConfig
+import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
+import kotlinx.serialization.json.Json
+import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
-import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object RetrofitClient {
+ private val json = Json {
+ ignoreUnknownKeys = true
+ coerceInputValues = true
+ }
+
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())
+ .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
val apiService: ApiService = retrofit.create(ApiService::class.java)
}그리고 모든 데이터 클래스에 @Serializable 어노테이션 추가 필요 (다른 파일 코멘트 참조)
해결 방법 2: 모든 데이터 모델을 Gson 어노테이션으로 변경
모든 데이터 모델의 @SerialName을 @SerializedName으로 변경 (다른 파일 코멘트 참조)
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt
around lines 23 to 27, the Retrofit instance uses GsonConverterFactory while
your models use kotlinx.serialization @SerialName, causing incorrect
serialization; switch to the kotlinx-serialization converter by adding the
retrofit2-kotlinx-serialization-converter dependency to build.gradle.kts and
replace GsonConverterFactory.create() with the kotlinx serialization converter
configured with your Json instance, and ensure all data classes are annotated
with @Serializable; alternatively (less recommended) convert all @SerialName
annotations to Gson’s @SerializedName and keep GsonConverterFactory.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@serializable 어노테이션이 누락되었습니다.
kotlinx.serialization의 @SerialName을 사용하고 있지만, 데이터 클래스에 @Serializable 어노테이션이 없습니다. 그런데 RetrofitClient.kt에서 GsonConverterFactory를 사용하고 있어서, 현재 설정으로는 @SerialName 어노테이션이 무시됩니다.
다음 중 하나를 선택하여 수정해야 합니다:
- 권장: Retrofit 컨버터를
kotlinx-serialization-converter로 변경하고 모든 데이터 클래스에@Serializable추가 @SerialName대신 Gson의@SerializedName사용
해결 방법 1 (권장): kotlinx-serialization 사용
build.gradle.kts에 의존성 추가:
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")데이터 클래스 수정:
package com.example.kuit6_android_api.data.model.response
import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+@Serializable
data class AuthorResponse(
@SerialName(value = "id") val id: Long,
@SerialName(value = "username") val username: String,
@SerialName(value = "profileImageUrl") val profileImageUrl: String?
)그리고 RetrofitClient.kt도 수정 필요 (별도 코멘트 참조)
해결 방법 2: Gson 사용
package com.example.kuit6_android_api.data.model.response
-import kotlinx.serialization.SerialName
+import com.google.gson.annotations.SerializedName
data class AuthorResponse(
- @SerialName(value = "id") val id: Long,
- @SerialName(value = "username") val username: String,
- @SerialName(value = "profileImageUrl") val profileImageUrl: String?
+ @SerializedName("id") val id: Long,
+ @SerializedName("username") val username: String,
+ @SerializedName("profileImageUrl") val profileImageUrl: String?
)🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/data/model/response/AuthorResponse.kt
around lines 5 to 9, the data class uses kotlinx.serialization's @SerialName but
is missing the required @Serializable annotation and the project currently uses
Gson via Retrofit, so @SerialName is being ignored; fix by either (recommended)
adding @Serializable to this data class and switching Retrofit to the
kotlinx-serialization converter (add the kotlin serialization converter and
kotlinx-serialization-json dependencies, update RetrofitClient to use the
KotlinxSerializationConverterFactory) or alternatively replace @SerialName with
Gson's @SerializedName and keep the current Gson converter; apply the same
choice consistently across all models and update imports accordingly.
| data class BaseResponse<T>( | ||
| @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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@serializable 어노테이션이 누락되었습니다.
AuthorResponse.kt와 동일한 문제입니다. @SerialName을 사용하고 있지만 @Serializable 어노테이션이 없고, Retrofit에서는 Gson을 사용하고 있어 직렬화가 제대로 작동하지 않습니다.
kotlinx-serialization 사용 시:
package com.example.kuit6_android_api.data.model.response
import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+@Serializable
data class BaseResponse<T>(
@SerialName(value = "success") val success: Boolean,
@SerialName(value = "message") val message: String?,
@SerialName(value = "data") val data: T?,
@SerialName(value = "timestamp") val timestamp: String
)Gson 사용 시:
package com.example.kuit6_android_api.data.model.response
-import kotlinx.serialization.SerialName
+import com.google.gson.annotations.SerializedName
data class BaseResponse<T>(
- @SerialName(value = "success") val success: Boolean,
- @SerialName(value = "message") val message: String?,
- @SerialName(value = "data") val data: T?,
- @SerialName(value = "timestamp") val timestamp: String
+ @SerializedName("success") val success: Boolean,
+ @SerializedName("message") val message: String?,
+ @SerializedName("data") val data: T?,
+ @SerializedName("timestamp") val timestamp: String
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| data class BaseResponse<T>( | |
| @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 | |
| ) | |
| package com.example.kuit6_android_api.data.model.response | |
| import com.google.gson.annotations.SerializedName | |
| data class BaseResponse<T>( | |
| @SerializedName("success") val success: Boolean, | |
| @SerializedName("message") val message: String?, | |
| @SerializedName("data") val data: T?, //nullable | |
| @SerializedName("timestamp") val timestamp: String | |
| ) |
| data class BaseResponse<T>( | |
| @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 | |
| ) | |
| package com.example.kuit6_android_api.data.model.response | |
| import kotlinx.serialization.SerialName | |
| import kotlinx.serialization.Serializable | |
| @Serializable | |
| data class BaseResponse<T>( | |
| @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 | |
| ) |
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt
around lines 5-10, the data class uses @SerialName but is missing the
@Serializable annotation; add @Serializable above the data class and import
kotlinx.serialization.Serializable so kotlinx-serialization can process the
SerialName annotations (alternatively, if the project uses Gson/Retrofit,
replace @SerialName with Gson's @SerializedName and remove @Serializable to
match the Gson serializer).
| val finalAuthor = author//.ifBlank { "anonymous" } | ||
| viewModel.createPost( | ||
| finalAuthor, | ||
| title, | ||
| content, | ||
| imageUrl = viewModel.uploadedImageUrl | ||
| ) { | ||
| onPostCreated() | ||
| // ViewModel 에서 멘트를 받아서 전달하는 식으로 많이 구성. flow 활용 | ||
| // 현재: onSuccess 람다로 넣어주는 식으로 구현 | ||
| scope.launch { snackBarState.showSnackbar("게시글이 작성되었습니다.") } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
성공 스낵바가 네비게이션과 함께 사라집니다.
Line 286에서 onPostCreated()로 화면을 종료한 뒤, 동일 컴포저블의 rememberCoroutineScope()로 스낵바를 띄우고 있습니다. 화면이 pop되면 해당 scope가 즉시 cancel되어 스낵바가 표시되지 않으므로 미션 요구사항을 충족하지 못합니다. MainActivity 수준에서 유지되는 scope나 suspend (String) -> Unit 콜백을 내려받아 네비게이션 전에 호출하거나, 이전 화면(savedStateHandle 등)에서 스낵바를 처리하도록 리팩터링해 주세요.
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt
around lines 279 to 290, the success snackbar is launched from the composable's
rememberCoroutineScope after calling onPostCreated(), but the composable's scope
is cancelled when the screen is popped so the snackbar never appears; fix by
moving snackbar emission out of the composable-local scope or firing it before
navigation: either (a) call snackBarState.showSnackbar(...) (suspending) before
invoking onPostCreated(), (b) accept a suspend (String) -> Unit successSnackbar
callback from the caller (MainActivity/host) and invoke that before popping, or
(c) have the destination write a message into savedStateHandle
(previousBackStackEntry) / ViewModel which the previous screen or MainActivity
observes and shows the snackbar from a lifecycle-safe, long-lived scope.
| fun getPosts() { | ||
| viewModelScope.launch { | ||
| delay(500) // 네트워크 시뮬레이션 | ||
| posts = dummyPosts.toList() | ||
| runCatching { | ||
| apiService.getPosts() | ||
| }.onSuccess { response -> | ||
| response.data?.let { | ||
| if (response.success) { | ||
| posts = response.data | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 처리 누락으로 인한 사용자 경험 저하
getPosts() 함수에서 API 호출 실패 시 에러를 처리하지 않아 사용자에게 피드백이 제공되지 않습니다. 네트워크 오류나 서버 에러 발생 시 사용자는 아무런 알림 없이 빈 목록만 보게 됩니다.
다음과 같이 에러 처리를 추가하세요:
fun getPosts() {
viewModelScope.launch {
runCatching {
apiService.getPosts()
}.onSuccess { response ->
response.data?.let {
if (response.success) {
posts = response.data
}
}
+ }.onFailure { error ->
+ // 에러 상태를 UI에 전달하거나 로깅
+ error.printStackTrace()
}
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun getPosts() { | |
| viewModelScope.launch { | |
| delay(500) // 네트워크 시뮬레이션 | |
| posts = dummyPosts.toList() | |
| runCatching { | |
| apiService.getPosts() | |
| }.onSuccess { response -> | |
| response.data?.let { | |
| if (response.success) { | |
| posts = response.data | |
| } | |
| } | |
| } | |
| } | |
| } | |
| fun getPosts() { | |
| viewModelScope.launch { | |
| runCatching { | |
| apiService.getPosts() | |
| }.onSuccess { response -> | |
| response.data?.let { | |
| if (response.success) { | |
| posts = response.data | |
| } | |
| } | |
| }.onFailure { error -> | |
| // 에러 상태를 UI에 전달하거나 로깅 | |
| error.printStackTrace() | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
around lines 36 to 48, getPosts() currently ignores failures from the API call;
add error handling to improve UX by handling runCatching failures and updating
observable state. Specifically, add an onFailure block to runCatching to capture
the exception, set an error LiveData/StateFlow (or update a UI-visible
error/message property) and/or clear/loading state appropriately, and optionally
log the exception; ensure you also handle non-success responses inside onSuccess
by setting an error state when response.success is false so the UI can show a
message or retry option.
| 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 | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 처리 누락으로 인한 사용자 경험 저하
getPostDetail() 함수에서도 API 호출 실패 시 에러를 처리하지 않아 사용자가 문제를 인지할 수 없습니다.
다음과 같이 에러 처리를 추가하세요:
fun getPostDetail(postId: Long) {
viewModelScope.launch {
runCatching {
apiService.getDetail(postId)
}.onSuccess { response ->
response.data?.let {
if (response.success) {
postDetail = response.data
}
}
+ }.onFailure { error ->
+ error.printStackTrace()
}
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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 getPostDetail(postId: Long) { | |
| viewModelScope.launch { | |
| runCatching { | |
| apiService.getDetail(postId) | |
| }.onSuccess { response -> | |
| response.data?.let { | |
| if (response.success) { | |
| postDetail = response.data | |
| } | |
| } | |
| }.onFailure { error -> | |
| error.printStackTrace() | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
around lines 50-62, the getPostDetail() call uses runCatching but lacks error
handling; add an onFailure block to runCatching to capture the throwable and
handle API failures, and also handle the case when response.success is false by
setting an error state. Specifically, in onFailure { throwable -> ... } log the
error (e.g., Timber/Log) and update a ViewModel-exposed observable error holder
(LiveData/StateFlow/Event) so the UI can show an error message; also change the
onSuccess branch to set the error state when response.success is false (with an
appropriate message) instead of silently ignoring it.
| fun createPost( | ||
| author: String = "anonymous", | ||
| author: String,// = "anonymous", | ||
| title: String, | ||
| content: String, | ||
| imageUrl: String? = null, | ||
| imageUrl: String? = uploadedImageUrl, | ||
| 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() | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
기본 파라미터 값이 예상과 다르게 동작할 수 있음
Line 68의 imageUrl: String? = uploadedImageUrl는 함수 호출 시점이 아닌 파라미터 선언 시점의 uploadedImageUrl 값을 캡처합니다. 이는 의도한 동작이 아닐 가능성이 높습니다. 또한 에러 처리가 누락되어 있습니다.
다음과 같이 수정하세요:
fun createPost(
author: String,
title: String,
content: String,
- imageUrl: String? = uploadedImageUrl,
+ imageUrl: String? = null,
onSuccess: () -> Unit = {}
) {
viewModelScope.launch {
runCatching {
- val request = PostCreateRequest(title, content, imageUrl)
+ val request = PostCreateRequest(title, content, imageUrl ?: uploadedImageUrl)
apiService.createPost(author, request)
}.onSuccess { response ->
if (response.success) {
clearUploadedImageUrl()
onSuccess()
}
+ }.onFailure { error ->
+ error.printStackTrace()
}
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun createPost( | |
| author: String = "anonymous", | |
| author: String,// = "anonymous", | |
| title: String, | |
| content: String, | |
| imageUrl: String? = null, | |
| imageUrl: String? = uploadedImageUrl, | |
| 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() | |
| } | |
| } | |
| } | |
| } | |
| fun createPost( | |
| author: String,// = "anonymous", | |
| title: String, | |
| content: String, | |
| imageUrl: String? = null, | |
| onSuccess: () -> Unit = {} | |
| ) { | |
| viewModelScope.launch { | |
| runCatching { | |
| val request = PostCreateRequest(title, content, imageUrl ?: uploadedImageUrl) | |
| apiService.createPost(author, request) | |
| }.onSuccess { response -> | |
| if (response.success) { | |
| // 이미지 업로드 관련 코드 | |
| clearUploadedImageUrl() | |
| onSuccess() | |
| } | |
| }.onFailure { error -> | |
| error.printStackTrace() | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
around lines 64 to 83, the parameter default imageUrl: String? =
uploadedImageUrl captures uploadedImageUrl at declaration time (not call time)
and there is no error handling; change the signature to accept imageUrl: String?
= null and inside the function compute val finalImageUrl = imageUrl ?:
uploadedImageUrl, use finalImageUrl when building the request, and add
runCatching.onFailure { /* log or notify error */ } and handle non-success
responses (else branch) to clear the uploaded image only on success and report
or log errors appropriately.
| fun updatePost( | ||
| postId: Long, | ||
| title: String, | ||
| content: String, | ||
| imageUrl: String? = null, | ||
| imageUrl: String?, | ||
| 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() | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 처리 추가 필요 (수정 기능 관련)
게시글 수정 성공 시 onSuccess 콜백이 호출되어 스낵바가 표시될 것으로 보입니다 (6주차 미션). 하지만 실패 시 사용자에게 피드백이 제공되지 않습니다.
다음과 같이 에러 처리를 추가하세요:
fun updatePost(
postId: Long,
title: String,
content: String,
imageUrl: String?,
onSuccess: () -> Unit = {}
) {
viewModelScope.launch {
runCatching {
val request = PostCreateRequest(title, content, imageUrl)
apiService.updatePost(postId, request)
}.onSuccess { response ->
if (response.success && response.data != null) {
postDetail = response.data
onSuccess()
}
+ }.onFailure { error ->
+ error.printStackTrace()
+ // UI에 에러 전달 필요
}
}
}🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
around lines 85-104, the updatePost function only handles success and provides
no user feedback on failure; add error handling by using runCatching().onFailure
{ ... } to catch network/exception errors and handle responses where
response.success is false by invoking a provided error callback or setting an
error LiveData/StateFlow (e.g., errorMessage) so the UI can show a
Snackbar/Toast; ensure you propagate meaningful error text (exception.message or
response.message) to the UI and call onSuccess only on true success.
| fun deletePost(postId: Long, onSuccess: () -> Unit = {}) { | ||
| // 삭제 후 게시글 목록 나오게 | ||
| viewModelScope.launch { | ||
| runCatching { | ||
| apiService.deletePost(postId) | ||
| }.onSuccess { response -> | ||
| if (response.success) { | ||
| postDetail = null | ||
| getPosts() | ||
| onSuccess() | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 처리 추가 필요 (삭제 기능 관련)
게시글 삭제 성공 시 onSuccess 콜백이 호출되어 스낵바가 표시될 것으로 보입니다 (6주차 미션). 하지만 실패 시 사용자에게 피드백이 제공되지 않습니다.
다음과 같이 에러 처리를 추가하세요:
fun deletePost(postId: Long, onSuccess: () -> Unit = {}) {
viewModelScope.launch {
runCatching {
apiService.deletePost(postId)
}.onSuccess { response ->
if (response.success) {
postDetail = null
getPosts()
onSuccess()
}
+ }.onFailure { error ->
+ error.printStackTrace()
+ // UI에 에러 전달 필요
}
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun deletePost(postId: Long, onSuccess: () -> Unit = {}) { | |
| // 삭제 후 게시글 목록 나오게 | |
| viewModelScope.launch { | |
| runCatching { | |
| apiService.deletePost(postId) | |
| }.onSuccess { response -> | |
| if (response.success) { | |
| postDetail = null | |
| getPosts() | |
| onSuccess() | |
| } | |
| } | |
| } | |
| } | |
| fun deletePost(postId: Long, onSuccess: () -> Unit = {}) { | |
| // 삭제 후 게시글 목록 나오게 | |
| viewModelScope.launch { | |
| runCatching { | |
| apiService.deletePost(postId) | |
| }.onSuccess { response -> | |
| if (response.success) { | |
| postDetail = null | |
| getPosts() | |
| onSuccess() | |
| } | |
| }.onFailure { error -> | |
| error.printStackTrace() | |
| // UI에 에러 전달 필요 | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
around lines 106 to 119, the deletePost function only handles the success path
and provides no feedback on failure; add proper error handling by extending the
function to accept an optional onError callback (e.g., onError: (String) -> Unit
= {}) or use an existing LiveData/Event to surface errors, check the response
when response.success is false and call onError with a meaningful message, and
add a .onFailure block on the runCatching to capture exceptions and call onError
with the exception message so the UI can show a snackbar or error state.
미션 요구 사항
[v] 각 API 요청 성공 시 스낵바 띄우기
설명
게시글 작성 성공 시 스낵바 띄우기와 동일한 방법으로 미션 완료. snackBarState: SnackbarHostState와 val scope = rememberCoroutineScope() 활용.
실행 영상
KUITweek6.mp4