Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.ninecraft.booket.core.common.extensions

import android.annotation.SuppressLint
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.ripple
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
Expand Down Expand Up @@ -44,7 +44,7 @@ fun Modifier.clickableSingle(
onClickLabel = onClickLabel,
onClick = { multipleEventsCutter.processEvent { onClick() } },
role = role,
indication = LocalIndication.current,
indication = ripple(),
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 👍 👍
관련해서 변경사항이 있었다는 것은 인지했었는데, 적용을 안하고 있었구만...

interactionSource = remember { MutableInteractionSource() },
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ fun ReedTopAppBar(
text = title,
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center,
style = ReedTheme.typography.heading2SemiBold,
style = ReedTheme.typography.headline2SemiBold,
Copy link

@coderabbitai coderabbitai bot Jul 12, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

타이포그래피 변경이 기존 사용처에 미치는 영향을 확인하세요.

heading2SemiBold에서 headline2SemiBold로 변경된 것이 디자인 시스템의 표준화 작업으로 보입니다. 기존에 heading2SemiBold를 사용하는 다른 컴포넌트들이 영향받지 않는지 확인이 필요합니다.

다음 스크립트로 기존 heading2SemiBold 사용처를 확인해주세요:


🏁 Script executed:

#!/bin/bash
# Description: 기존 heading2SemiBold 사용처 확인
# Expected: 다른 파일에서 heading2SemiBold 사용이 없거나 모두 업데이트되어야 함

rg "heading2SemiBold" --type kotlin -A 2 -B 2

Length of output: 2522


타이포그래피 스타일 명칭 일관성 유지 필요

heading2SemiBold가 여전히 아래 컴포넌트에서 사용되고 있어, headline2SemiBold로 통일해야 합니다:

  • feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/WithdrawConfirmationBottomSheet.kt
    // 변경 전
    style = ReedTheme.typography.heading2SemiBold
    // 변경 후
    style = ReedTheme.typography.headline2SemiBold
  • feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/LogoutConfirmationBottomSheet.kt
    // 변경 전
    style = ReedTheme.typography.heading2SemiBold
    // 변경 후
    style = ReedTheme.typography.headline2SemiBold

추가로, core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Typography.kt에서 heading2SemiBold 정의를 제거하거나 deprecated 처리하여 사용되지 않는 스타일임을 명시하는 것도 권장드립니다.

🤖 Prompt for AI Agents
In
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/appbar/ReedTopAppBar.kt
at line 65, ensure the typography style name is consistent by using
'headline2SemiBold' instead of 'heading2SemiBold'. Additionally, update the
files
feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/WithdrawConfirmationBottomSheet.kt
and LogoutConfirmationBottomSheet.kt to replace all instances of
'heading2SemiBold' with 'headline2SemiBold'. Finally, in
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Typography.kt,
remove or mark the 'heading2SemiBold' style as deprecated to indicate it is no
longer in use.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

바텀시트에서 Text style은 그게 맞아

Copy link

Choose a reason for hiding this comment

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

@seoyoon513 이해했습니다! 바텀시트에서는 heading2SemiBold가 올바른 스타일이군요. 감사합니다.

(。◕‿◕。) 🐰


✏️ Learnings added
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#45
File: core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/appbar/ReedTopAppBar.kt:65-65
Timestamp: 2025-07-12T01:33:57.074Z
Learning: Reed Android 프로젝트에서 타이포그래피 사용 규칙: 톱 앱바(Top App Bar)에서는 `headline2SemiBold`를 사용하고, 바텀시트(Bottom Sheet)에서는 `heading2SemiBold`를 사용한다. 이는 의도적인 디자인 시스템 차별화이다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#32
File: core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt:10-16
Timestamp: 2025-07-08T12:33:01.863Z
Learning: Reed Android 프로젝트에서 KAKAO 버튼 스타일은 디자이너가 pressed 상태 색상을 별도로 정의하지 않았기 때문에 pressed 상태에서도 동일한 Kakao 색상을 사용한다.

Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#32
File: core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Theme.kt:15-21
Timestamp: 2025-07-08T03:21:24.693Z
Learning: Reed Android 프로젝트에서는 다크모드/라이트모드 대응 계획이 없고 기본 테마 값만 사용할 예정이다.

)

if (endIconRes != null) {
Expand All @@ -87,14 +87,14 @@ fun ReedTopAppBar(
fun ReedBackTopAppBar(
modifier: Modifier = Modifier,
title: String = "",
onNavigateBack: () -> Unit = {},
onBackClick: () -> Unit = {},
) {
ReedTopAppBar(
modifier = modifier,
title = title,
startIconRes = R.drawable.ic_chevron_left,
startIconDescription = "Back",
startIconOnClick = onNavigateBack,
startIconOnClick = onBackClick,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal fun TermsAgreement(
.background(White),
) {
ReedBackTopAppBar(
onNavigateBack = {
onBackClick = {
state.eventSink(TermsAgreementUiEvent.OnBackClick)
},
)
Expand Down
2 changes: 1 addition & 1 deletion feature/settings/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

Expand Down
57 changes: 57 additions & 0 deletions feature/settings/src/main/assets/oss_licenses.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[
{
"name": "Circuit",
"license": "Apache License 2.0",
"url": "https://github.com/slackhq/circuit"
},
{
"name": "Compose Effects",
"license": "Apache License 2.0",
"url": "https://github.com/skydoves/compose-effects"
},
{
"name": "Compose Stable Marker",
"license": "Apache License 2.0",
"url": "https://github.com/skydoves/compose-stable-marker"
},
{
"name": "Hilt",
"license": "Apache License 2.0",
"url": "https://dagger.dev/hilt/"
},
{
"name": "Logger",
"license": "Apache License 2.0",
"url": "https://github.com/orhanobut/logger"
},
{
"name": "OkHttp",
"license": "Apache License 2.0",
"url": "https://square.github.io/okhttp/"
},
{
"name": "Retrofit",
"license": "Apache License 2.0",
"url": "https://square.github.io/retrofit/"
},
{
"name": "OkHttp Logging Interceptor",
"license": "Apache License 2.0",
"url": "https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor"
},
{
"name": "Coil",
"license": "Apache License 2.0",
"url": "https://github.com/coil-kt/coil"
},
{
"name": "Detekt",
"license": "Apache License 2.0",
"url": "https://github.com/detekt/detekt"
},
{
"name": "ktlint",
"license": "MIT License",
"url": "https://github.com/pinterest/ktlint"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.ninecraft.booket.screens.OssLicensesScreen
import com.ninecraft.booket.screens.SettingsScreen
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.retained.rememberRetained
Expand Down Expand Up @@ -34,6 +35,10 @@ class SettingsPresenter @AssistedInject constructor(
// TODO: 웹뷰 화면으로 이동
}

is SettingsUiEvent.OnOssLicensesClick -> {
navigator.goTo(OssLicensesScreen)
}

is SettingsUiEvent.OnLogoutClick -> {
isLogoutBottomSheetVisible = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ internal fun Settings(
) {
ReedBackTopAppBar(
title = stringResource(R.string.settings_title),
onNavigateBack = {
onBackClick = {
state.eventSink(SettingsUiEvent.OnBackClick)
},
)
Expand Down Expand Up @@ -95,7 +95,7 @@ internal fun Settings(
SettingItem(
title = stringResource(R.string.settings_open_source_license),
onItemClick = {
state.eventSink(SettingsUiEvent.OnTermDetailClick(""))
state.eventSink(SettingsUiEvent.OnOssLicensesClick)
},
action = {
Icon(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ data class SettingsUiState(
sealed interface SettingsUiEvent : CircuitUiEvent {
data object OnBackClick : SettingsUiEvent
data class OnTermDetailClick(val title: String) : SettingsUiEvent
data object OnOssLicensesClick : SettingsUiEvent
data object OnLogoutClick : SettingsUiEvent
data object OnWithdrawClick : SettingsUiEvent
data object OnBottomSheetDismissed : SettingsUiEvent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ninecraft.booket.feature.settings.osslicenses

import kotlinx.serialization.Serializable

@Serializable
data class OssLicenseInfo(
val name: String,
val license: String,
val url: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.ninecraft.booket.feature.settings.osslicenses

import androidx.compose.runtime.Composable
import com.ninecraft.booket.screens.OssLicensesScreen
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.components.ActivityRetainedComponent

class OssLicensesPresenter @AssistedInject constructor(
@Assisted val navigator: Navigator,
) : Presenter<OssLicensesUiState> {
@Composable
override fun present(): OssLicensesUiState {
fun handleEvent(event: OssLicensesUiEvent) {
when (event) {
is OssLicensesUiEvent.OnBackClicked -> {
navigator.pop()
}
}
}
return OssLicensesUiState(
eventSink = ::handleEvent,
)
}

@CircuitInject(OssLicensesScreen::class, ActivityRetainedComponent::class)
@AssistedFactory
fun interface Factory {
fun create(navigator: Navigator): OssLicensesPresenter
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package com.ninecraft.booket.feature.settings.osslicenses

import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import com.ninecraft.booket.core.designsystem.DevicePreview
import com.ninecraft.booket.core.designsystem.component.appbar.ReedBackTopAppBar
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
import com.ninecraft.booket.core.designsystem.theme.White
import com.ninecraft.booket.feature.settings.R
import com.ninecraft.booket.screens.OssLicensesScreen
import com.orhanobut.logger.Logger
import com.slack.circuit.codegen.annotations.CircuitInject
import dagger.hilt.android.components.ActivityRetainedComponent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.IOException

@CircuitInject(OssLicensesScreen::class, ActivityRetainedComponent::class)
@Composable
internal fun OssLicenses(
state: OssLicensesUiState,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
var licenses by remember { mutableStateOf<List<OssLicenseInfo>>(emptyList()) }

LaunchedEffect(Unit) {
licenses = withContext(Dispatchers.IO) {
getOssLicensesDataFromAsset(context)
}
}
Comment on lines +55 to +59
Copy link

@coderabbitai coderabbitai bot Jul 12, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

로딩 및 에러 상태 처리 개선 필요

현재 구현에서는 JSON 로딩 중에 사용자에게 로딩 상태를 표시하지 않고, 로딩 실패 시에도 빈 리스트만 표시됩니다. 사용자 경험 향상을 위해 로딩 상태와 에러 상태를 추가하는 것을 권장합니다.

다음과 같이 상태를 개선할 수 있습니다:

- var licenses by remember { mutableStateOf<List<OssLicenseInfo>>(emptyList()) }
+ var licenses by remember { mutableStateOf<List<OssLicenseInfo>>(emptyList()) }
+ var isLoading by remember { mutableStateOf(true) }
+ var errorMessage by remember { mutableStateOf<String?>(null) }

LaunchedEffect(Unit) {
+   isLoading = true
+   errorMessage = null
    licenses = withContext(Dispatchers.IO) {
        getOssLicensesDataFromAsset(context)
    }
+   isLoading = false
}
📝 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
LaunchedEffect(Unit) {
licenses = withContext(Dispatchers.IO) {
getOssLicensesDataFromAsset(context)
}
}
// state for the loaded licenses
var licenses by remember { mutableStateOf<List<OssLicenseInfo>>(emptyList()) }
+ // loading and error state
+ var isLoading by remember { mutableStateOf(true) }
+ var errorMessage by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
+ isLoading = true
+ errorMessage = null
licenses = withContext(Dispatchers.IO) {
getOssLicensesDataFromAsset(context)
}
+ isLoading = false
}
🤖 Prompt for AI Agents
In
feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesScreen.kt
around lines 55 to 59, the current code does not handle loading or error states
when fetching JSON data, resulting in no user feedback during loading or
failure. Introduce additional state variables to represent loading and error
conditions, update these states appropriately before and after the data fetch,
and modify the UI to display a loading indicator while fetching and an error
message if loading fails, instead of showing an empty list.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

지금은 임시 구현에 가까운거니까 oss_licenses.json이 remote 데이터로 관리될 때 적용해볼게~

Copy link

Choose a reason for hiding this comment

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

@seoyoon513 네, 맞습니다! 로컬 JSON 파일로 관리할 때는 로딩이 빠르니까 지금처럼 구현하는 게 합리적이네요. 원격 데이터로 변경될 때 로딩 상태와 에러 처리를 함께 적용하시면 될 것 같습니다! 👍

🐰✨
/| /|
( .. )
o
(")(")


✏️ Learnings added
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#45
File: feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesScreen.kt:55-59
Timestamp: 2025-07-12T03:51:35.773Z
Learning: feature/settings OSS 라이선스 화면의 현재 구현은 임시 구현이며, oss_licenses.json이 remote 데이터로 관리될 때 로딩 상태 및 에러 처리가 적용될 예정입니다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


Column(
modifier = modifier
.fillMaxSize()
.background(White),
) {
ReedBackTopAppBar(
title = stringResource(R.string.oss_licenses_title),
onBackClick = {
state.eventSink(OssLicensesUiEvent.OnBackClicked)
},
)
LazyColumn {
items(licenses) { license ->
OssLicenseItem(
name = license.name,
license = license.license,
url = license.url,
)
}
}
}
}

@Composable
private fun OssLicenseItem(
name: String,
license: String,
url: String,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(
horizontal = ReedTheme.spacing.spacing3,
vertical = ReedTheme.spacing.spacing3,
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(ReedTheme.spacing.spacing1)
.background(
color = ReedTheme.colors.contentBrand,
shape = CircleShape,
),
)
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing1))
Text(
text = "$name - $license",
fontWeight = FontWeight.W500,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = ReedTheme.typography.caption2Regular,
)
}
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
Text(
text = url,
modifier = Modifier
.fillMaxWidth()
.background(
color = ReedTheme.colors.bgSecondary,
shape = RoundedCornerShape(ReedTheme.radius.xs),
)
.padding(
horizontal = ReedTheme.spacing.spacing2,
vertical = ReedTheme.spacing.spacing4,
),
style = ReedTheme.typography.caption2Regular,
)
}
}

private fun getOssLicensesDataFromAsset(context: Context): List<OssLicenseInfo> {
return try {
val json = context.assets.open("oss_licenses.json")
.bufferedReader()
.use { it.readText() }
Json.decodeFromString(json)
} catch (e: IOException) {
Logger.e(e, "Failed to read json file")
emptyList()
}
}

@DevicePreview
@Composable
private fun OssLicensesScreenPreview() {
ReedTheme {
OssLicenses(
state = OssLicensesUiState(
eventSink = {},
),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ninecraft.booket.feature.settings.osslicenses

import com.slack.circuit.runtime.CircuitUiEvent
import com.slack.circuit.runtime.CircuitUiState

data class OssLicensesUiState(
val eventSink: (OssLicensesUiEvent) -> Unit,
) : CircuitUiState

sealed interface OssLicensesUiEvent : CircuitUiEvent {
data object OnBackClicked : OssLicensesUiEvent
}
1 change: 1 addition & 0 deletions feature/settings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
<string name="settings_withdraw_agreement">확인하였으며 이에 동의합니다.</string>
<string name="settings_cancel">취소</string>
<string name="settings_withdraw_action">탈퇴하기</string>
<string name="oss_licenses_title">오픈소스 라이선스</string>
</resources>
Loading
Loading