Skip to content
97 changes: 81 additions & 16 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
import java.util.Properties

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.ktlint)
}
val properties =
Properties().apply {
load(project.rootProject.file("local.properties").inputStream())
}
Comment on lines +13 to +16
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

local.properties 파일 누락 시 에러 처리를 추가하세요.

현재 local.properties 파일이 존재하지 않을 경우 빌드가 실패할 수 있습니다.

 val properties =
     Properties().apply {
-        load(project.rootProject.file("local.properties").inputStream())
+        val localPropertiesFile = project.rootProject.file("local.properties")
+        if (localPropertiesFile.exists()) {
+            load(localPropertiesFile.inputStream())
+        }
     }
🤖 Prompt for AI Agents
In app/build.gradle.kts around lines 13 to 16, the code loads local.properties
without checking if the file exists, which can cause a build failure if the file
is missing. Add a check to verify the existence of local.properties before
loading it, and handle the case where the file is absent by either skipping the
load or providing a clear error message to prevent the build from failing
unexpectedly.


android {
namespace = "com.android.heartz"
compileSdk = 35
namespace = "com.heartz.app"
compileSdk = libs.versions.compileSdk.get().toInt()

defaultConfig {
applicationId = "com.android.heartz"
minSdk = 28
targetSdk = 35
versionCode = 1
versionName = "1.0"
applicationId = "com.heartz.app"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get()

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

buildConfigField("String", "BASE_URL", properties["base.url"].toString())
buildConfigField(
"String",
"KAKAO_NATIVE_APP_KEY",
properties["kakao.native.app.key"].toString()
)
Comment on lines +31 to +36
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

BuildConfig에서 민감한 정보 노출을 방지하세요.

buildConfigField를 통해 API 키와 URL이 APK에 평문으로 노출될 수 있습니다. 이는 보안상 위험할 수 있습니다.

더 안전한 방법들을 고려해보세요:

  1. Gradle 속성 사용: 런타임에 리소스에서 읽기
  2. 암호화: 키를 암호화하여 저장
  3. 서버 프록시: 민감한 API 호출을 서버를 통해 프록시
-        buildConfigField("String", "BASE_URL", properties["base.url"].toString())
-        buildConfigField(
-            "String",
-            "KAKAO_NATIVE_APP_KEY",
-            properties["kakao.native.app.key"].toString()
-        )
+        // 리소스 파일을 통한 방법 고려
+        resValue("string", "base_url", properties["base.url"]?.toString() ?: "\"\"")
+        manifestPlaceholders["kakaoAppKey"] = properties["kakao.native.app.key"]?.toString() ?: ""
📝 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
buildConfigField("String", "BASE_URL", properties["base.url"].toString())
buildConfigField(
"String",
"KAKAO_NATIVE_APP_KEY",
properties["kakao.native.app.key"].toString()
)
// 리소스 파일을 통한 방법 고려
resValue("string", "base_url", properties["base.url"]?.toString() ?: "\"\"")
manifestPlaceholders["kakaoAppKey"] = properties["kakao.native.app.key"]?.toString() ?: ""
🤖 Prompt for AI Agents
In app/build.gradle.kts around lines 31 to 36, sensitive information like API
keys and URLs are exposed in plain text via buildConfigField, which poses a
security risk. To fix this, remove these sensitive values from buildConfigField
and instead load them securely at runtime from encrypted resources or
environment variables. Alternatively, consider using a server proxy for API
calls to avoid embedding keys in the APK altogether.

}

buildTypes {
Expand All @@ -27,20 +46,66 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "11"
jvmTarget = "17"
}
buildFeatures {
compose = true
buildConfig = true
}
}

dependencies {

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
// Test
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.bundles.test)

// Debug
debugImplementation(libs.bundles.debug)

// AndroidX
implementation(libs.bundles.androidx)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.kotlinx.collections.immutable)

// Google
implementation(platform(libs.google.firebase.bom))
implementation(libs.google.firebase.crashlytics)

// Network
implementation(platform(libs.okhttp.bom))
implementation(libs.bundles.okhttp)
implementation(libs.bundles.retrofit)
implementation(libs.kotlinx.serialization.json)

// Hilt
implementation(libs.bundles.hilt)
ksp(libs.hilt.compiler)

// Coil
implementation(libs.coil.compose)

// Timber
implementation(libs.timber)

// Kakao Login
implementation(libs.bundles.kakao)

// Ui
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
debugImplementation(libs.androidx.ui.tooling)
}

ktlint {
android = true
debug = true
coloredOutput = true
verbose = true
outputToConsole = true
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
package com.android.heartz

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3: 얘도 테스트코드인건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이부분은 안드로이드 Instrumented 테스트 코드입니다! 저도 unit test만 해보고 요런 테스트에 대해서는 처음 공부해봤는데 이 차이점은 나중에 같이 공부해보면 좋을 것 같아요! 여기서 수정이 일어난 이유는 ktlint에서 import * 이런식으로 와일드카드 쓰는 것을 지양하기 때문에 수정했습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

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

오오 Instrumented 테스트 코드 이 부분 저도 한 번 미리 공부해보겠습니다~~

fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.android.heartz", appContext.packageName)
}
}
}
17 changes: 16 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET"/>

<application
android:name=".Heartz"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand All @@ -11,6 +14,18 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Heartz"
tools:targetApi="31" />
tools:targetApi="31">
<activity
android:name=".presentation.main.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Heartz">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
24 changes: 24 additions & 0 deletions app/src/main/java/com/heartz/app/Heartz.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.heartz.app

import android.app.Application
import androidx.appcompat.app.AppCompatDelegate
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber

@HiltAndroidApp
class Heartz : Application() {
override fun onCreate() {
super.onCreate()

initTimber()
setDayMode()
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3: 다크 모드 확장하는 거 물어봐주는거 까먹지 말기~~

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@sohee6989 앱잼 기간 동안에는 일단 안하는걸루!

}

private fun initTimber() {
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
}

private fun setDayMode() {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.heartz.app.core.designsystem.ui.theme

import androidx.compose.material3.darkColorScheme
import androidx.compose.ui.graphics.Color

val Red80 = Color(0xFFFF5656)
val Pink80 = Color(0xFFFFB0B0)
Comment on lines +6 to +7
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

색상 네이밍이 실제 색상값과 불일치합니다.

Red80Pink80이라는 이름은 Material Design의 색상 팔레트에서 일반적으로 더 어두운 색상을 의미하지만, 실제로는 밝은 색상값이 지정되어 있습니다.

-val Red80 = Color(0xFFFF5656)
-val Pink80 = Color(0xFFFFB0B0)
+val Red40 = Color(0xFFFF5656)  // 더 밝은 색상이므로 40번대가 적절
+val Pink40 = Color(0xFFFFB0B0)

또는 Material 3 색상 팔레트 가이드라인에 따라 색상을 재정의하는 것을 고려해보세요.

📝 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 Red80 = Color(0xFFFF5656)
val Pink80 = Color(0xFFFFB0B0)
val Red40 = Color(0xFFFF5656) // 더 밝은 색상이므로 40번대가 적절
val Pink40 = Color(0xFFFFB0B0)
🤖 Prompt for AI Agents
In app/src/main/java/com/heartz/app/core/designsystem/ui/theme/Color.kt at lines
6 to 7, the color names Red80 and Pink80 do not match their actual bright color
values, which conflicts with Material Design naming conventions where "80"
typically indicates a darker shade. To fix this, either rename the variables to
reflect their true brightness or adjust the color values to match the expected
darker shades according to the Material 3 color palette guidelines.


val HeartzColorScheme =
darkColorScheme(
primary = Red80,
secondary = Pink80
)
Comment on lines +9 to +13
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

라이트 테마 지원을 위한 추가 구현이 필요합니다.

현재 다크 컬러 스키마만 정의되어 있어 라이트 테마를 지원하지 않습니다. 사용자 경험 향상을 위해 라이트 컬러 스키마도 추가하는 것을 권장합니다.

+val HeartzLightColorScheme = lightColorScheme(
+    primary = Red40,
+    secondary = Pink40
+)
+
 val HeartzColorScheme =
     darkColorScheme(
-        primary = Red80,
-        secondary = Pink80
+        primary = Red80,
+        secondary = Pink80
     )
📝 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 HeartzColorScheme =
darkColorScheme(
primary = Red80,
secondary = Pink80
)
val HeartzLightColorScheme = lightColorScheme(
primary = Red40,
secondary = Pink40
)
val HeartzColorScheme =
darkColorScheme(
primary = Red80,
secondary = Pink80
)
🤖 Prompt for AI Agents
In app/src/main/java/com/heartz/app/core/designsystem/ui/theme/Color.kt around
lines 9 to 13, only the darkColorScheme is defined, so the app lacks support for
a light theme. To fix this, define a corresponding lightColorScheme with
appropriate color values for primary and secondary colors, and ensure it is
integrated properly to enable light theme support alongside the existing dark
theme.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.heartz.app.core.designsystem.ui.theme

import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

@Composable
fun HeartzTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = HeartzColorScheme,
typography = Typography,
content = content
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.heartz.app.core.designsystem.ui.theme

import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

val Typography =
Typography(
bodyLarge =
TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
lineHeight = 24.sp
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.heartz.app.core.navigation

interface MainTabRoute : Route
3 changes: 3 additions & 0 deletions app/src/main/java/com/heartz/app/core/navigation/Route.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.heartz.app.core.navigation

interface Route
15 changes: 15 additions & 0 deletions app/src/main/java/com/heartz/app/core/state/UiState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.heartz.app.core.state

sealed interface UiState<out T> {
data object Empty : UiState<Nothing>

data object Loading : UiState<Nothing>

data class Success<T>(
val data: T
) : UiState<T>

data class Failure(
val msg: String
) : UiState<Nothing>
}
17 changes: 17 additions & 0 deletions app/src/main/java/com/heartz/app/core/util/ModifierExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.heartz.app.core.util

import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed

inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit = {}): Modifier =
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3
crossinline을 처음 봐서 찾아봤는데 람다에서 non-local return을 막아주는 역할을 하는 거구뇽 배우 ㅓ 가요 ! 🙇‍♀️

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

inline함수를 쓰면서 정말 많이 나오는 부분 같아요! 앱잼하면서 런캐칭으로 오류처리 할 때 Timber 찍는 거 같이 내부함수로 만들면서 고민해봐요!
민재형 블로그 참고 https://angrypodo.tistory.com/7

Copy link
Collaborator

Choose a reason for hiding this comment

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

오 이거 저도 공부해보겠습니다~~신기하네요

composed {
this.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
onClick()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.heartz.app.data.datasource.local

import kotlinx.coroutines.flow.Flow

// TODO: 임시
interface DummyLocalDataSource {
val isLogin: Flow<Boolean>

suspend fun setIsLogin(value: Boolean)

suspend fun clear()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.heartz.app.data.datasource.remote

import com.heartz.app.data.dto.base.DummyBaseResponse
import com.heartz.app.data.dto.request.RequestDummyDto
import com.heartz.app.data.dto.response.ResponseDummyDto

interface DummyRemoteDataSource {
suspend fun getDummies(request: RequestDummyDto): DummyBaseResponse<ResponseDummyDto>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.heartz.app.data.datasourceimpl.local

import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import com.heartz.app.data.datasource.local.DummyLocalDataSource
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

private const val FILE_NAME = "date_road_datastore"

private val Context.dataStore by preferencesDataStore(name = FILE_NAME)

class DummyLocalDataSourceImpl @Inject constructor(
@ApplicationContext private val context: Context
) : DummyLocalDataSource {
companion object {
val IS_LOGIN = booleanPreferencesKey("is_login")
}

override val isLogin: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[IS_LOGIN] ?: false
}

override suspend fun setIsLogin(value: Boolean) {
context.dataStore.edit { it[IS_LOGIN] = value }
}

override suspend fun clear() {
context.dataStore.edit { it.clear() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.heartz.app.data.datasourceimpl.remote

import com.heartz.app.data.datasource.remote.DummyRemoteDataSource
import com.heartz.app.data.dto.base.DummyBaseResponse
import com.heartz.app.data.dto.request.RequestDummyDto
import com.heartz.app.data.dto.response.ResponseDummyDto
import com.heartz.app.data.service.DummyService
import javax.inject.Inject

class DummyRemoteDataSourceImpl
@Inject
constructor(
private val dummyService: DummyService
) : DummyRemoteDataSource {
override suspend fun getDummies(
request: RequestDummyDto
): DummyBaseResponse<ResponseDummyDto> =
dummyService.getDummies(request)
}
Loading
Loading