Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/markdown.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .kotlin/errors/errors-1762482068299.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}
117 changes: 0 additions & 117 deletions CLAUDE.md

This file was deleted.

16 changes: 16 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import java.util.Properties

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
Expand All @@ -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")
buildConfigField("String", "BASE_URL", "\"$baseUrl\"")
Comment on lines +23 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

BASE_URL 누락 시 런타임 크래시가 발생합니다.

Line [23]~[29]에서 local.properties에 BASE_URL이 없으면 buildConfigField"null"이 들어가고, Retrofit.Builder().baseUrl("null") 호출 시 즉시 예외가 터집니다. 빌드 단계에서 명시적으로 검증하거나 기본값을 강제해 주세요.

-        if (localPropertiesFile.exists()) {
-            properties.load(localPropertiesFile.inputStream())
-        }
-        val baseUrl = properties.getProperty("BASE_URL")
-        buildConfigField("String", "BASE_URL", "\"$baseUrl\"")
+        if (localPropertiesFile.exists()) {
+            localPropertiesFile.inputStream().use(properties::load)
+        }
+        val baseUrl = properties.getProperty("BASE_URL")
+            ?: error("BASE_URL must be defined 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.

Suggested change
val properties = Properties()
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
properties.load(localPropertiesFile.inputStream())
}
val baseUrl = properties.getProperty("BASE_URL")
buildConfigField("String", "BASE_URL", "\"$baseUrl\"")
val properties = Properties()
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.inputStream().use(properties::load)
}
val baseUrl = properties.getProperty("BASE_URL")
?: error("BASE_URL must be defined in local.properties")
buildConfigField("String", "BASE_URL", "\"$baseUrl\"")
🤖 Prompt for AI Agents
In app/build.gradle.kts around lines 23-29, the script reads BASE_URL from
local.properties but when BASE_URL is missing it writes "null" into
buildConfigField causing a runtime crash; update the block to validate the
property at build time and either provide a safe default (e.g., an empty string
or a well-known dev URL) or fail the build with a clear error. Specifically,
after loading properties check properties.getProperty("BASE_URL") for
null/blank; if null/blank call error(...) or throw GradleException with a
message instructing to set BASE_URL, otherwise use the validated value when
calling buildConfigField so the generated BuildConfig always contains a valid
non-null string.

}

buildTypes {
Expand All @@ -38,6 +47,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}

Expand Down Expand Up @@ -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.kotlinx.serialization)
implementation(libs.okhttp)
implementation(libs.okhttp.logging.interceptor)
}
23 changes: 19 additions & 4 deletions app/src/main/java/com/example/kuit6_android_api/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,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
Expand Down Expand Up @@ -40,15 +45,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(hostState = snackBarState)
}
) {
val navController = rememberNavController()

NavGraph(
navController = navController,
startDestination = PostListRoute
startDestination = PostListRoute,
snackBarState = snackBarState
)
}
Comment on lines +49 to 66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scaffold의 content padding을 적용하세요.

Scaffold가 제공하는 paddingValuesNavGraph에 전달하지 않으면 콘텐츠가 Snackbar 아래에 가려질 수 있습니다.

다음과 같이 수정하세요:

-                ) {
+                ) { paddingValues ->
                     val navController = rememberNavController()
 
                     NavGraph(
                         navController = navController,
                         startDestination = PostListRoute,
-                        snackBarState = snackBarState
+                        snackBarState = snackBarState,
+                        modifier = Modifier.padding(paddingValues)
                     )
                 }

참고: NavGraphModifier 파라미터를 받도록 시그니처를 수정해야 할 수 있습니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/MainActivity.kt around lines
49 to 66, the Scaffold's content paddingValues are not forwarded to NavGraph so
UI (e.g., Snackbar) can overlap content; change the Scaffold content lambda to
capture the paddingValues parameter and pass them into NavGraph (either via a
Modifier.padding(paddingValues) or by adding a paddingValues/Modifier parameter
to NavGraph's signature), and update NavGraph definition/call sites accordingly
so the passed padding is applied to the root composable.

}
Expand All @@ -71,6 +84,7 @@ class MainActivity : ComponentActivity() {
) == PackageManager.PERMISSION_GRANTED -> {
// 이미 권한이 있음
}

shouldShowRequestPermissionRationale(permission) -> {
// 권한 거부 이력이 있음 - 설명 표시 후 재요청
Toast.makeText(
Expand All @@ -80,6 +94,7 @@ class MainActivity : ComponentActivity() {
).show()
requestPermissionLauncher.launch(permission)
}

else -> {
// 권한 요청
requestPermissionLauncher.launch(permission)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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

interface ApiService {
// 게시글 목록 조회
@GET("/api/posts")
suspend fun getPosts(): BaseResponse<List<PostResponse>>

// 게시글 생성
@POST("/api/posts")
suspend fun createPost(
@Query("author") author: String = "규빈",
@Body request: PostCreateRequest
): BaseResponse<PostResponse>

// 게시글 상세 조회
@GET("/api/posts/{id}")
suspend fun getPostDetail(
@Path("id") id: Long
): BaseResponse<PostResponse>

// 게시글 수정
@PUT("/api/posts/{id}")
suspend fun updatePost(
@Path("id") id: Long,
@Body request: PostCreateRequest
): BaseResponse<PostResponse>

// 게시글 삭제
@DELETE("/api/posts/{id}")
suspend fun deletePost(
@Path("id") id: Long
): BaseResponse<Unit>

// 이미지 업로드
@Multipart
@POST("/api/images/upload")
suspend fun uploadImage(
@Part file: MultipartBody.Part
): BaseResponse<Map<String, String>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.example.kuit6_android_api.data.api

import com.example.kuit6_android_api.BuildConfig
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
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()
Comment on lines +13 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

로그 레벨을 운영 빌드에서 낮춰주세요.

Line [13]~[22]에서 HttpLoggingInterceptor를 항상 BODY로 두면 운영 빌드에서 민감한 요청/응답 본문이 로그에 남고, 큰 payload에서는 성능까지 악화됩니다. 디버그 빌드에서만 BODY를 사용하고 운영에서는 NONE으로 낮춰주세요.

-    private val loggingInterceptor = HttpLoggingInterceptor().apply {
-        level = HttpLoggingInterceptor.Level.BODY
-    }
+    private val loggingInterceptor = HttpLoggingInterceptor().apply {
+        level = if (BuildConfig.DEBUG) {
+            HttpLoggingInterceptor.Level.BODY
+        } else {
+            HttpLoggingInterceptor.Level.NONE
+        }
+    }
📝 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.

Suggested change
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 loggingInterceptor = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt
around lines 13 to 22, the HttpLoggingInterceptor is currently always set to
Level.BODY which leaks sensitive data and harms performance in release builds;
change it to set the level conditionally based on the build type (e.g., if
(BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else
HttpLoggingInterceptor.Level.NONE) so debug builds keep BODY logging while
production builds use NONE, and ensure the interceptor is created/configured
before adding it to the OkHttpClient builder.


private val json = Json {
ignoreUnknownKeys = true // 서버에서 추가 필드가 와도 무시
coerceInputValues = true // null이 와야 할 곳에 다른 값이 와도 처리
}

private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()

val apiService: ApiService = retrofit.create(ApiService::class.java)
}
17 changes: 0 additions & 17 deletions app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.kuit6_android_api.data.model.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class PostCreateRequest(
@SerialName("title") val title: String,
@SerialName("content") val content: String,
@SerialName("imageUrl") val imageUrl: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.kuit6_android_api.data.model.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class AuthorResponse(
@SerialName("id") val id: Long,
@SerialName("username") val username: String,
@SerialName("profileImageUrl") val profileImageUrl: String?
)
Comment on lines +1 to +11
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

파일명과 클래스명의 불일치를 수정하세요.

파일명은 AuthResponse.kt이지만 클래스명은 AuthorResponse입니다. Kotlin 컨벤션에 따라 파일명과 클래스명을 일치시켜야 합니다.

파일명을 AuthorResponse.kt로 변경하세요.

🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/data/model/response around
lines 1-11, the file is named AuthResponse.kt but the declared data class is
AuthorResponse; rename the file to AuthorResponse.kt to match the class name and
Kotlin conventions, and update any imports/usages across the project if
necessary to reference the new filename/class name.

Loading