Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
18 changes: 18 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")

import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import java.io.FileInputStream
import java.util.Properties

plugins {
alias(libs.plugins.booket.android.application)
alias(libs.plugins.booket.android.application.compose)
alias(libs.plugins.booket.android.hilt)
}

val localPropertiesFile = rootProject.file("local.properties")
val localProperties = Properties()
localProperties.load(FileInputStream(localPropertiesFile))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

android {
namespace = "com.ninecraft.booket"

defaultConfig {
buildConfigField("String", "KAKAO_NATIVE_APP_KEY", getApiKey("KAKAO_NATIVE_APP_KEY"))
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = (localProperties["KAKAO_NATIVE_APP_KEY"] as String).trim('"')
}

buildTypes {
release {
isMinifyEnabled = false
Expand Down Expand Up @@ -40,9 +53,14 @@ dependencies {
libs.androidx.activity.compose,
libs.androidx.startup,
libs.logger,
libs.kakao.auth,

libs.bundles.circuit,
)
api(libs.circuit.codegen.annotation)
ksp(libs.circuit.codegen.ksp)
}

fun getApiKey(propertyKey: String): String {
return gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
}
18 changes: 17 additions & 1 deletion app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,20 @@

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile

# Kakao Login
-keep class com.kakao.sdk.**.model.* { <fields>; }

# https://github.com/square/okhttp/pull/6792
-dontwarn org.bouncycastle.jsse.**
-dontwarn org.conscrypt.*
-dontwarn org.openjsse.**

# refrofit2 (with r8 full mode)
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
-keep,allowobfuscation,allowshrinking class retrofit2.Response
Comment on lines +31 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Retrofit2 R8 룰 문법 오류
플레이스홀더(<methods>, <1>, <3>)가 남아 있어 룰이 적용되지 않을 가능성이 높습니다. 공식 가이드에 맞게 다음과 같이 수정하세요.

-# Retrofit2 (with R8 full mode)
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
-keep,allowobfuscation,allowshrinking class retrofit2.Response
+# Retrofit2 (with R8 full mode)
+# Retrofit HTTP 인터페이스 및 응답 타입 보존
+keep class retrofit2.** { *; }
+keep interface retrofit2.http.** { *; }
+keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation

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

🤖 Prompt for AI Agents
In app/proguard-rules.pro around lines 31 to 37, the Retrofit2 R8 rules contain
placeholder tokens like <methods>, <1>, and <3> which are invalid and prevent
the rules from applying correctly. Replace these placeholders with proper
ProGuard syntax according to the official Retrofit2 R8 configuration guide,
ensuring all interface and class specifications are correctly defined without
placeholders.

24 changes: 24 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<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=".BooketApplication"
android:allowBackup="true"
Expand All @@ -24,8 +26,30 @@
android:name="com.ninecraft.booket.initializer.LoggerInitializer"
android:value="androidx.startup" />

<meta-data
android:name="com.ninecraft.booket.initializer.KakaoSdkInitializer"
android:value="androidx.startup" />

</provider>

<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
android:exported="true">

<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="oauth"
android:scheme="kakao${KAKAO_NATIVE_APP_KEY}" />

</intent-filter>

</activity>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ninecraft.booket.initializer

import android.content.Context
import androidx.startup.Initializer
import com.kakao.sdk.common.KakaoSdk
import com.ninecraft.booket.BuildConfig

class KakaoSdkInitializer : Initializer<Unit> {

override fun create(context: Context) {
Copy link
Copy Markdown

@jisungbin jisungbin Jun 24, 2025

Choose a reason for hiding this comment

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

@easyhooon 제가 잘 몰라서 그러는데, android.app.Application 클래스의 onCreate에서 init 하는거랑, androidx.startup.Initializer 클래스의 create에서 init 하는거랑 무슨 차이가 있나요?

(init 불리는 시점이나 성능 차이..?)

Androidx Startup 나온지 꽤 됐는데 아직 한 번도 제대로 본 적이 없네요 😭

Copy link
Copy Markdown
Contributor Author

@easyhooon easyhooon Jun 24, 2025

Choose a reason for hiding this comment

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

image

@jisungbin InitializationProvider 코드를 보면 startup 라이브러리가 ContentProvider 추상클래스를 상속하여, ContentProvider를 기반으로 구현되어있음을 알 수 있습니다

ContentProvider는 Application.onCreate()보다 더 이른 시점에 실행되어, Application.onCreate()에서 Logger, FIrebaseCrashlytics, KakaoSdk등을 초기화하는 것보다 빠른 초기화를 할 수 있구, 공식문서에 따르면 여러 Initializer들이 하나의 ContentProvider를 공유하는 방식으로 동작하여, 각 컴포넌트마다 별도의 ContentProvider를 생성하는 것보다 성능상 이점을 얻을 수 있다고 합니다!

그외에도 depedencies() 함수를 override하여 초기화 순서를 관리할 수도 있는데, Application.onCreate()에서도 동일하게 코드 순서로 제어가 가능하긴해서 장점인지는 잘 모르겠네요(의존성을 직접 관리해서 초기화 시점을 조절해본 적은 없습니다 ^_^)

답변을 드리다가 문득 생각이 든게, Application Context의 초기화 시점보다 빠르게 초기화가 이뤄지면 문제가 생기지 않을까? 였는데 해당 글을 통해 문제가 없음을 확인할 수 있었습니다

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

아하!!! 제 핑거프린세스 질문에도 자세한 답변 감사합니다.

그러면 현재 시나리오에서는 Application.onCreate() 안에서 KakaoSdk.init를 진행하여도 문제 없어 보이는데(Application.onCreate() 보다 이른 시점에 진행하지 않아도 됨), Application 대신에 Initializer로 구현하신 결정 과정이 궁금해요.

Copy link
Copy Markdown
Contributor Author

@easyhooon easyhooon Jun 24, 2025

Choose a reason for hiding this comment

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

(Application.onCreate() 보다 이른 시점에 진행하지 않아도 됨)

음 말씀하신것처럼 반드시 이른 시점에 진행하지 않아도 되는 것은 맞지만, 저는 onCreate()보다 빠른 초기화를 할 수 있다는 점에서 메리트가 있다고 생각하여 채택했습니다!

벤치마크를 통해 직접 어느정도 이득을 볼 수 있는지 측정해보진 않았지만, Jetpack에서 지원하는 라이브러리이고, 사용하면 장점이 있다고 공식문서에서도 안내를 하기 때문에(사용한다고 trade off가 있는 것도 아닌걸로 판단됨) Intializer로 구현을 결정하였습니다

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

오 모두 공감됩니다.
감사합니다!

KakaoSdk.init(context, BuildConfig.KAKAO_NATIVE_APP_KEY)
}

override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ internal class AndroidFeatureConventionPlugin : Plugin<Project> {

dependencies {
implementation(project(path = ":core:designsystem"))
implementation(project(path = ":core:ui"))

implementation(libs.bundles.circuit)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

val Kakao = Color(0xFFFBD300)
3 changes: 3 additions & 0 deletions core/designsystem/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<resources>
<string name="app_name">Booket</string>
<string name="network_error_message">네트워크 연결이 불안해요.\n잠시후 다시 이용해주세요.</string>
<string name="server_error_message">이용에 불편을 드려 죄송합니다.\n잠시후 다시 이용해주세요.</string>
<string name="unknown_error_message">알 수 없는 오류가 발생하였습니다.</string>
</resources>
4 changes: 2 additions & 2 deletions core/network/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ android {
}

buildTypes {
getByName("debug") {
debug {
buildConfigField("String", "SERVER_BASE_URL", getServerBaseUrl("DEBUG_SERVER_BASE_URL"))
}

getByName("release") {
release {
buildConfigField("String", "SERVER_BASE_URL", getServerBaseUrl("RELEASE_SERVER_BASE_URL"))
}
}
Expand Down
1 change: 1 addition & 0 deletions core/ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
18 changes: 18 additions & 0 deletions core/ui/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")

plugins {
alias(libs.plugins.booket.android.library)
alias(libs.plugins.booket.android.library.compose)
}

android {
namespace = "com.ninecraft.booket.core.ui"
}

dependencies {
implementations(
projects.core.designsystem,

libs.logger,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.ninecraft.booket.core.ui.component

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.ninecraft.booket.core.designsystem.ComponentPreview
import com.ninecraft.booket.core.designsystem.theme.BooketTheme

@Composable
fun BooketButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
containerColor: Color = MaterialTheme.colorScheme.primaryContainer,
contentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,
disabledContainerColor: Color = MaterialTheme.colorScheme.tertiaryContainer,
disabledContentColor: Color = MaterialTheme.colorScheme.onTertiaryContainer,
text: @Composable () -> Unit,
leadingIcon: @Composable (() -> Unit)? = null,
) {
Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor,
),
contentPadding = if (leadingIcon != null) {
ButtonDefaults.ButtonWithIconContentPadding
} else {
ButtonDefaults.ContentPadding
},
) {
TogetherButtonContent(
text = text,
leadingIcon = leadingIcon,
)
}
}

@Composable
private fun TogetherButtonContent(
text: @Composable () -> Unit,
leadingIcon: @Composable (() -> Unit)? = null,
) {
if (leadingIcon != null) {
Box(Modifier.sizeIn(maxHeight = ButtonDefaults.IconSize)) {
leadingIcon()
}
}
Box(
Modifier.padding(
start = if (leadingIcon != null) {
ButtonDefaults.IconSpacing
} else {
0.dp
},
),
) {
text()
}
}

@ComponentPreview
@Composable
private fun TogetherButtonPreview() {
BooketTheme {
BooketButton(
onClick = {},
text = {
Text(text = "Button")
},
)
}
}

@ComponentPreview
@Composable
private fun TogetherButtonWithLeadingIconPreview() {
BooketTheme {
BooketButton(
onClick = {},
text = {
Text("Check Button")
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Check icon",
tint = Color.White,
)
},
)
}
}
1 change: 1 addition & 0 deletions feature/login/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
28 changes: 28 additions & 0 deletions feature/login/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")

plugins {
alias(libs.plugins.booket.android.feature)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.parcelize)
}

android {
namespace = "com.ninecraft.booket.feature.login"

buildFeatures {
buildConfig = true
}
}

ksp {
arg("circuit.codegen.mode", "hilt")
}

dependencies {
implementations(
projects.feature.home,

libs.logger,
libs.kakao.auth,
)
}
Comment thread
easyhooon marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.ninecraft.booket.feature.login

import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext

@Composable
internal fun HandleLoginEffects(
state: LoginScreen.State,
eventSink: (LoginScreen.Event) -> Unit,
) {
val context = LocalContext.current
val kakaoAuthClient = remember { KakaoAuthClient() }

LaunchedEffect(state.sideEffect) {
when (state.sideEffect) {
is LoginScreen.SideEffect.KakaoLogin -> {
kakaoAuthClient.loginWithKakao(
context = context,
onSuccess = { token ->
eventSink(LoginScreen.Event.LoginSuccess(token))
},
onFailure = { errorMessage ->
eventSink(LoginScreen.Event.LoginFailure(errorMessage))
},
)
}

is LoginScreen.SideEffect.ShowToast -> {
Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show()
}

null -> {}
}
}

if (state.sideEffect != null) {
eventSink(LoginScreen.Event.InitSideEffect)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
Loading
Loading