Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ kotlin {
implementation(libs.glance.material3)
implementation(libs.compose.runtime)
implementation(libs.kakao.sdk.v2.user)
implementation(libs.ktor.client.okhttp)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
commonMain.dependencies {
implementation(compose.runtime)
Expand Down Expand Up @@ -92,6 +94,11 @@ kotlin {
implementation(libs.kmp.firebase.analytics)
implementation(libs.napier)
implementation(libs.kinappbrowser)

implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.logging)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.chukchukhaksa.mobile.common.kmp

import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.engine.okhttp.OkHttp

actual val httpClientEngineFactory: HttpClientEngineFactory<*> = OkHttp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ actual class KakaoSignInClient {
?: throw IllegalStateException("Android Context is required for Kakao Login")

val nonce = UUID.randomUUID().toString()
val hashedNonce = sha256Hex(nonce)

return suspendCancellableCoroutine { continuation ->
val callback: (OAuthToken?, Throwable?) -> Unit = callback@{ token, error ->
Expand All @@ -37,13 +38,13 @@ actual class KakaoSignInClient {
if (UserApiClient.instance.isKakaoTalkLoginAvailable(androidContext)) {
UserApiClient.instance.loginWithKakaoTalk(
context = androidContext,
nonce = nonce,
nonce = hashedNonce,
) { token, error ->
if (error != null) {
if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
UserApiClient.instance.loginWithKakaoAccount(
context = androidContext,
nonce = nonce,
nonce = hashedNonce,
callback = callback,
)
return@loginWithKakaoTalk
Expand All @@ -56,7 +57,7 @@ actual class KakaoSignInClient {
} else {
UserApiClient.instance.loginWithKakaoAccount(
context = androidContext,
nonce = nonce,
nonce = hashedNonce,
callback = callback,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.chukchukhaksa.mobile.common.kmp

import java.security.MessageDigest

actual fun sha256Hex(input: String): String {
val bytes = MessageDigest.getInstance("SHA-256").digest(input.toByteArray())
return bytes.joinToString("") { "%02x".format(it) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.chukchukhaksa.mobile.common.kmp

import io.ktor.client.engine.HttpClientEngineFactory

expect val httpClientEngineFactory: HttpClientEngineFactory<*>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.chukchukhaksa.mobile.common.kmp

expect fun sha256Hex(input: String): String
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.chukchukhaksa.mobile.data.auth.datasource

import com.chukchukhaksa.mobile.domain.auth.model.SignInResult

interface RemoteAuthDataSource {
suspend fun signIn(idToken: String, nonce: String): SignInResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.chukchukhaksa.mobile.data.auth.di

import com.chukchukhaksa.mobile.data.auth.datasource.RemoteAuthDataSource
import com.chukchukhaksa.mobile.data.auth.repository.AuthRepositoryImpl
import com.chukchukhaksa.mobile.domain.auth.repository.AuthRepository
import com.chukchukhaksa.mobile.remote.auth.RemoteAuthDataSourceImpl
import org.koin.dsl.module

val authRepositoryModule = module {
single<RemoteAuthDataSource> { RemoteAuthDataSourceImpl(get()) }
single<AuthRepository> { AuthRepositoryImpl(get()) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.chukchukhaksa.mobile.data.auth.repository

import com.chukchukhaksa.mobile.data.auth.datasource.RemoteAuthDataSource
import com.chukchukhaksa.mobile.domain.auth.model.SignInResult
import com.chukchukhaksa.mobile.domain.auth.repository.AuthRepository

class AuthRepositoryImpl(
private val remoteAuthDataSource: RemoteAuthDataSource,
) : AuthRepository {

override suspend fun signIn(idToken: String, nonce: String): SignInResult {
return remoteAuthDataSource.signIn(idToken = idToken, nonce = nonce)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ val domainModule = module {

// Auth use cases
factory { AppleLoginUseCase(get()) }
factory { KakaoLoginUseCase(get()) }
factory { KakaoLoginUseCase(get(), get()) }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.chukchukhaksa.mobile.di

import com.chukchukhaksa.mobile.data.auth.di.authRepositoryModule
import com.chukchukhaksa.mobile.data.config.di.appConfigRepositoryModule
import com.chukchukhaksa.mobile.data.openlecture.di.openLectureRepositoryModule
import com.chukchukhaksa.mobile.data.openmajor.di.openMajorRepositoryModule
Expand All @@ -13,6 +14,7 @@ import com.chukchukhaksa.mobile.local.datasource.timetable.di.localTimetableData
import com.chukchukhaksa.mobile.local.datastore.di.dataStoreModule
import com.chukchukhaksa.mobile.remote.config.remoteAppConfigDataSourceModule
import com.chukchukhaksa.mobile.remote.di.firebaseDatabaseModule
import com.chukchukhaksa.mobile.remote.di.httpClientModule
import com.chukchukhaksa.mobile.remote.timetable.remoteOpenLectureDataSourceModule
import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration
Expand All @@ -36,8 +38,10 @@ fun initKoin(config: KoinAppDeclaration? = null) {
openLectureRepositoryModule,
presentationModule,
firebaseDatabaseModule,
httpClientModule,
remoteAppConfigDataSourceModule,
appConfigRepositoryModule,
authRepositoryModule,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.chukchukhaksa.mobile.domain.auth.model

data class SignInResult(
val accessToken: String,
val refreshToken: String,
val isPortalLinked: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.chukchukhaksa.mobile.domain.auth.repository

import com.chukchukhaksa.mobile.domain.auth.model.SignInResult

interface AuthRepository {
suspend fun signIn(idToken: String, nonce: String): SignInResult
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package com.chukchukhaksa.mobile.domain.auth.usecase

import com.chukchukhaksa.mobile.common.kmp.KakaoSignInClient
import com.chukchukhaksa.mobile.common.kmp.KakaoSignInResult
import com.chukchukhaksa.mobile.domain.auth.model.SignInResult
import com.chukchukhaksa.mobile.domain.auth.repository.AuthRepository
import com.chukchukhaksa.mobile.domain.common.runCatchingIgnoreCancelled

class KakaoLoginUseCase(
private val kakaoSignInClient: KakaoSignInClient,
private val authRepository: AuthRepository,
) {
suspend operator fun invoke(context: Any? = null): Result<KakaoSignInResult> =
suspend operator fun invoke(context: Any? = null): Result<SignInResult> =
runCatchingIgnoreCancelled {
kakaoSignInClient.signIn(context)
val kakaoResult = kakaoSignInClient.signIn(context)
authRepository.signIn(
idToken = kakaoResult.idToken,
nonce = kakaoResult.nonce,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.chukchukhaksa.mobile.remote.auth

import com.chukchukhaksa.mobile.data.auth.datasource.RemoteAuthDataSource
import com.chukchukhaksa.mobile.domain.auth.model.SignInResult
import com.chukchukhaksa.mobile.remote.auth.model.SignInRequest
import com.chukchukhaksa.mobile.remote.auth.model.SignInResponse
import com.chukchukhaksa.mobile.remote.common.ApiResponse
import com.chukchukhaksa.mobile.remote.common.getDataOrThrow
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody

class RemoteAuthDataSourceImpl(
private val httpClient: HttpClient,
) : RemoteAuthDataSource {

override suspend fun signIn(idToken: String, nonce: String): SignInResult {
val response = httpClient.post("users/signin") {
setBody(SignInRequest(idToken = idToken, nonce = nonce))
}.body<ApiResponse<SignInResponse>>()

return response.getDataOrThrow().toSignInResult()
Comment thread
lluke0 marked this conversation as resolved.
}
}

private fun SignInResponse.toSignInResult() = SignInResult(
accessToken = accessToken,
refreshToken = refreshToken,
isPortalLinked = isPortalLinked,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.chukchukhaksa.mobile.remote.auth.model

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

@Serializable
data class SignInRequest(
@SerialName("id_token") val idToken: String,
val nonce: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.chukchukhaksa.mobile.remote.auth.model

import kotlinx.serialization.Serializable

@Serializable
data class SignInResponse(
val accessToken: String,
val refreshToken: String,
val isPortalLinked: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.chukchukhaksa.mobile.remote.common

class ApiException(
val code: String,
override val message: String,
) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.chukchukhaksa.mobile.remote.common

import kotlinx.serialization.Serializable

@Serializable
data class ApiResponse<T>(
val success: Boolean,
val data: T? = null,
val error: ApiError? = null,
)

@Serializable
data class ApiError(
val code: String,
val message: String,
)

fun <T> ApiResponse<T>.getDataOrThrow(): T {
return data ?: throw ApiException(
code = error?.code.orEmpty(),
message = error?.message ?: "์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”.",
)
}
Comment thread
lluke0 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.chukchukhaksa.mobile.remote.di

import com.chukchukhaksa.mobile.common.kmp.httpClientEngineFactory
import com.chukchukhaksa.mobile.common.kmp.isDebug
import io.github.aakira.napier.Napier
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.observer.ResponseObserver
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import org.koin.dsl.module

private val prettyJson = Json { prettyPrint = true }

private fun String.prettyPrintJson(): String = try {
val element = prettyJson.decodeFromString<JsonElement>(this)
prettyJson.encodeToString(JsonElement.serializer(), element)
} catch (_: Exception) {
this
}

val httpClientModule = module {
single {
HttpClient(httpClientEngineFactory) {
install(ContentNegotiation) {
json(
Json {
ignoreUnknownKeys = true
isLenient = true
},
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Napier.d(message, tag = "HttpClient")
}
}
level = if (isDebug) LogLevel.HEADERS else LogLevel.NONE
}

if (isDebug) {
install(ResponseObserver) {
onResponse { response ->
val body = response.bodyAsText()
if (body.isNotBlank()) {
Napier.d(
"RESPONSE BODY:\n${body.prettyPrintJson()}",
tag = "HttpClient",
)
Comment on lines +73 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

๐Ÿงน Nitpick | ๐Ÿ”ต Trivial

์‘๋‹ต ๋ณธ๋ฌธ ์ž˜๋ฆผ ์—ฌ๋ถ€๋ฅผ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€์— ํ‘œ์‹œํ•˜๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๊ฐ€ "RESPONSE BODY"๋กœ ๊ณ ์ •๋˜์–ด ์žˆ์–ด 4096์ž ์ดˆ๊ณผ ์‹œ ์ž˜๋ ธ๋Š”์ง€ ์•Œ๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ๋””๋ฒ„๊น… ์‹œ ํ˜ผ๋™์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ์ž˜๋ฆผ ์—ฌ๋ถ€๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

โœจ ์ œ์•ˆ ์ˆ˜์ •์•ˆ
                         val body = response.bodyAsText()
                         if (body.isNotBlank()) {
+                            val isTruncated = body.length > 4096
                             val preview = body.take(4096)
+                            val label = if (isTruncated) "RESPONSE BODY (truncated ${body.length} -> 4096)" else "RESPONSE BODY"
                             Napier.d(
-                                "RESPONSE BODY:\n${preview.prettyPrintJson()}",
+                                "$label:\n${preview.prettyPrintJson()}",
                                 tag = "HttpClient",
                             )
                         }
๐Ÿ“ 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
Napier.d(
"RESPONSE BODY:\n${preview.prettyPrintJson()}",
tag = "HttpClient",
)
if (isDebug) {
install(ResponseObserver) {
onResponse { response ->
val contentType = response.headers[io.ktor.http.HttpHeaders.ContentType]
if (contentType?.contains("application/json", ignoreCase = true) != true) return@onResponse
val body = response.bodyAsText()
if (body.isNotBlank()) {
val isTruncated = body.length > 4096
val preview = body.take(4096)
val label = if (isTruncated) "RESPONSE BODY (truncated ${body.length} -> 4096)" else "RESPONSE BODY"
Napier.d(
"$label:\n${preview.prettyPrintJson()}",
tag = "HttpClient",
)
}
}
}
}
๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt`
around lines 73 - 76, The log currently always prints "RESPONSE
BODY:\n${preview.prettyPrintJson()}" via Napier.d in HttpClientModule.kt; change
it so you compute the JSON payload string from preview.prettyPrintJson() and if
its length exceeds 4096 (or the chosen max) append or replace with a clear
truncated indicator (e.g., "...[TRUNCATED, length=NNNN]") and include the
original length in the message, then pass that final string to Napier.d so logs
show whether the response was cut off; update the Napier.d call site (the code
using preview.prettyPrintJson()) to use this truncated-aware string.

}
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

defaultRequest {
url("https://api.cchaksa.com/api/")
contentType(ContentType.Application.Json)
}
}
}
}
Loading
Loading