diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..c99b1a7
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,58 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.compose)
+}
+
+android {
+ namespace = "com.example.bookon"
+ compileSdk {
+ version = release(36) {
+ minorApiLevel = 1
+ }
+ }
+
+ defaultConfig {
+ applicationId = "com.example.bookon"
+ minSdk = 34
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ buildFeatures {
+ compose = true
+ }
+}
+
+dependencies {
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.compose.material3)
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.ui.graphics)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ testImplementation(libs.junit)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.compose.ui.test.junit4)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(libs.androidx.junit)
+ debugImplementation(libs.androidx.compose.ui.test.manifest)
+ debugImplementation(libs.androidx.compose.ui.tooling)
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/example/bookon/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/bookon/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..b533843
--- /dev/null
+++ b/app/src/androidTest/java/com/example/bookon/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.example.bookon
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+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
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.example.bookon", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4c1c8ea
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bookon/MainActivity.kt b/app/src/main/java/com/example/bookon/MainActivity.kt
new file mode 100644
index 0000000..1c64e48
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/MainActivity.kt
@@ -0,0 +1,47 @@
+package com.example.bookon
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.example.bookon.theme.BookOnTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ BookOnTheme {
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ Greeting(
+ name = "Android",
+ modifier = Modifier.padding(innerPadding)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun Greeting(name: String, modifier: Modifier = Modifier) {
+ Text(
+ text = "Hello $name!",
+ modifier = modifier
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+fun GreetingPreview() {
+ BookOnTheme {
+ Greeting("Android")
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/theme/Color.kt b/app/src/main/java/com/example/bookon/theme/Color.kt
new file mode 100644
index 0000000..ea6068c
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/theme/Color.kt
@@ -0,0 +1,39 @@
+package com.example.bookon.theme
+
+import androidx.compose.ui.graphics.Color
+
+val ButtonColor = Color(0xFF8DC73F)
+val DisabledButtonColor = Color(0xFFEBEBEB)
+val InputPlaceholderTextColor = Color(0xFFAEAEB2)
+val InputTextColor = Color(0xFF000000)
+val WarningTextColor = Color(0xFFB60000)
+val DescriptionTextColor = Color(0xFFB60000)
+val DarkGrayTextColor = Color(0xFF404040)
+val ErrorBackgroundColor = Color(0xFFFFF1F1)
+
+object BookOnColor {
+ val Primary = ButtonColor
+ val PrimaryDark = Color(0xFF72A21D)
+ val PrimaryPressed = Color(0xFF7BAB1F)
+ val PrimaryLight = Color(0xFFEFF6E3)
+ val PrimaryContainer = Color(0xFFF7F9EF)
+ val Disabled = DisabledButtonColor
+ val Background = Color(0xFFFBFBFC)
+ val Surface = Color(0xFFFFFFFF)
+ val SurfaceAlt = Color(0xFFF1F1F4)
+ val SurfaceBorder = Color(0xFFEAEAEC)
+ val Divider = Color(0xFFF2F2F4)
+ val TextPrimary = InputTextColor
+ val TextSecondary = Color(0xFF8E8E93)
+ val TextTertiary = Color(0xFF9A9AA1)
+ val TextPlaceholder = InputPlaceholderTextColor
+ val TextDarkGray = DarkGrayTextColor
+ val Error = WarningTextColor
+ val ErrorContainer = ErrorBackgroundColor
+ val NavigationInactive = Color(0xFFB0B0B5)
+ val SwitchOff = Color(0xFFD9D9D9)
+ val BookCoverPlaceholder = Color(0xFF6B4329)
+ val BookCoverSmallPlaceholder = Color(0xFFE8E0D8)
+ val IconContainer = Color(0xFFE8E8EA)
+ val StatusAvailableContainer = PrimaryLight
+}
diff --git a/app/src/main/java/com/example/bookon/theme/DesignToken.kt b/app/src/main/java/com/example/bookon/theme/DesignToken.kt
new file mode 100644
index 0000000..6f9a164
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/theme/DesignToken.kt
@@ -0,0 +1,80 @@
+package com.example.bookon.theme
+
+import androidx.compose.ui.unit.dp
+
+object AppSpacing {
+ val ScreenHorizontal = 24.dp
+ val HomeHorizontal = 28.dp
+ val ScreenVertical = 24.dp
+ val Section = 24.dp
+ val Content = 16.dp
+ val FieldHorizontal = 20.dp
+ val Item = 12.dp
+ val Small = 8.dp
+ val Tiny = 4.dp
+}
+
+object AppRadius {
+ val Button = 10.dp
+ val Field = 16.dp
+ val Search = 21.dp
+ val Card = 16.dp
+ val LargeCard = 20.dp
+ val Chip = 12.dp
+ val Small = 8.dp
+ val IconButton = 10.dp
+ val Progress = 8.dp
+}
+
+object AppElevation {
+ val None = 0.dp
+ val Field = 6.dp
+ val Button = 2.dp
+ val Card = 10.dp
+ val StrongCard = 20.dp
+ val BookCover = 4.dp
+}
+
+object AppIconSize {
+ val Small = 18.dp
+ val Default = 24.dp
+ val Medium = 30.dp
+ val Large = 32.dp
+ val XLarge = 36.dp
+ val Avatar = 64.dp
+}
+
+object AppComponentSize {
+ val MinTouchTarget = 48.dp
+ val ButtonHeight = 52.dp
+ val FieldHeight = 52.dp
+ val FieldBorderWidth = 1.dp
+ val HomeSearchHeight = 42.dp
+ val HomeActionButton = 36.dp
+ val TopBarHeight = 56.dp
+ val NavigationHeight = 89.dp
+ val ChipHeight = 34.dp
+ val SmallChipHeight = 24.dp
+ val BookCoverWidth = 132.dp
+ val BookCoverHeight = 177.dp
+ val HomeBookCoverWidth = 94.dp
+ val HomeBookCoverHeight = 160.dp
+ val HomeBookCardWidth = 112.dp
+ val LibraryBookCoverWidth = 166.dp
+ val LibraryBookCoverHeight = 234.dp
+ val LibrarySortToggleWidth = 130.dp
+ val LibrarySortToggleHeight = 36.dp
+ val LibrarySortOptionHeight = 30.dp
+ val BookListItemHeight = 88.dp
+ val BookListCoverWidth = 54.dp
+ val BookListCoverHeight = 68.dp
+ val PopularBookCoverWidth = 50.dp
+ val PopularBookCoverHeight = 64.dp
+ val RankingPedestalWidth = 100.dp
+ val RankingFirstPedestalHeight = 96.dp
+ val RankingSecondPedestalHeight = 76.dp
+ val RankingThirdPedestalHeight = 62.dp
+ val RankingListRowHeight = 64.dp
+ val StatSummaryCardHeight = 76.dp
+ val InfoCardIcon = 36.dp
+}
diff --git a/app/src/main/java/com/example/bookon/theme/Theme.kt b/app/src/main/java/com/example/bookon/theme/Theme.kt
new file mode 100644
index 0000000..00410c3
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/theme/Theme.kt
@@ -0,0 +1,73 @@
+package com.example.bookon.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = BookOnColor.Primary,
+ onPrimary = BookOnColor.Surface,
+ primaryContainer = BookOnColor.PrimaryContainer,
+ onPrimaryContainer = BookOnColor.TextPrimary,
+ secondary = BookOnColor.PrimaryPressed,
+ tertiary = BookOnColor.PrimaryLight,
+ background = BookOnColor.Background,
+ onBackground = BookOnColor.TextPrimary,
+ surface = BookOnColor.Surface,
+ onSurface = BookOnColor.TextPrimary,
+ surfaceVariant = BookOnColor.SurfaceAlt,
+ onSurfaceVariant = BookOnColor.TextSecondary,
+ outline = BookOnColor.SurfaceBorder,
+ error = BookOnColor.Error,
+ errorContainer = BookOnColor.ErrorContainer,
+ onErrorContainer = BookOnColor.Error,
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = BookOnColor.Primary,
+ onPrimary = BookOnColor.Surface,
+ primaryContainer = BookOnColor.PrimaryContainer,
+ onPrimaryContainer = BookOnColor.TextPrimary,
+ secondary = BookOnColor.PrimaryPressed,
+ tertiary = BookOnColor.PrimaryLight,
+ background = BookOnColor.Background,
+ onBackground = BookOnColor.TextPrimary,
+ surface = BookOnColor.Surface,
+ onSurface = BookOnColor.TextPrimary,
+ surfaceVariant = BookOnColor.SurfaceAlt,
+ onSurfaceVariant = BookOnColor.TextSecondary,
+ outline = BookOnColor.SurfaceBorder,
+ error = BookOnColor.Error,
+ errorContainer = BookOnColor.ErrorContainer,
+ onErrorContainer = BookOnColor.Error,
+)
+
+@Composable
+fun BookOnTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = false,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
diff --git a/app/src/main/java/com/example/bookon/theme/Type.kt b/app/src/main/java/com/example/bookon/theme/Type.kt
new file mode 100644
index 0000000..48465e2
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/theme/Type.kt
@@ -0,0 +1,119 @@
+package com.example.bookon.theme
+
+import androidx.compose.material3.Typography as MaterialTypography
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.sp
+import com.example.bookon.R
+
+val PretendardFontFamily = FontFamily(
+ Font(R.font.pretendard_thin, weight = FontWeight.Thin),
+ Font(R.font.pretendard_extra_light, weight = FontWeight.ExtraLight),
+ Font(R.font.pretendard_light, weight = FontWeight.Light),
+ Font(R.font.pretendard_regular, weight = FontWeight.Normal),
+ Font(R.font.pretendard_medium, weight = FontWeight.Medium),
+ Font(R.font.pretendard_semi_bold, weight = FontWeight.SemiBold),
+ Font(R.font.pretendard_bold, weight = FontWeight.Bold),
+ Font(R.font.pretendard_extra_bold, weight = FontWeight.ExtraBold),
+ Font(R.font.pretendard_black, weight = FontWeight.Black),
+)
+
+private val DefaultTypography = MaterialTypography()
+
+// 앱 전체 Material3 텍스트 스타일이 Pretendard weight 매핑을 사용하도록 설정한다.
+val Typography = Typography(
+ displayLarge = DefaultTypography.displayLarge.copy(fontFamily = PretendardFontFamily),
+ displayMedium = DefaultTypography.displayMedium.copy(fontFamily = PretendardFontFamily),
+ displaySmall = DefaultTypography.displaySmall.copy(fontFamily = PretendardFontFamily),
+ headlineLarge = DefaultTypography.headlineLarge.copy(fontFamily = PretendardFontFamily),
+ headlineMedium = DefaultTypography.headlineMedium.copy(fontFamily = PretendardFontFamily),
+ headlineSmall = DefaultTypography.headlineSmall.copy(fontFamily = PretendardFontFamily),
+ titleLarge = DefaultTypography.titleLarge.copy(fontFamily = PretendardFontFamily),
+ titleMedium = DefaultTypography.titleMedium.copy(fontFamily = PretendardFontFamily),
+ titleSmall = DefaultTypography.titleSmall.copy(fontFamily = PretendardFontFamily),
+ bodyLarge = DefaultTypography.bodyLarge.copy(fontFamily = PretendardFontFamily),
+ bodyMedium = DefaultTypography.bodyMedium.copy(fontFamily = PretendardFontFamily),
+ bodySmall = DefaultTypography.bodySmall.copy(fontFamily = PretendardFontFamily),
+ labelLarge = DefaultTypography.labelLarge.copy(fontFamily = PretendardFontFamily),
+ labelMedium = DefaultTypography.labelMedium.copy(fontFamily = PretendardFontFamily),
+ labelSmall = DefaultTypography.labelSmall.copy(fontFamily = PretendardFontFamily),
+)
+
+object BookOnTypography {
+ val screenTitle = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 28.sp,
+ lineHeight = 32.sp,
+ )
+ val sectionTitle = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 20.sp,
+ lineHeight = 22.sp,
+ )
+ val topBarTitle = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp,
+ )
+ val button = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp,
+ )
+ val fieldLabel = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 14.sp,
+ )
+ val fieldText = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ )
+ val fieldPlaceholder = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 12.sp,
+ )
+ val bodyMedium = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ )
+ val bodySemiBold = bodyMedium.copy(fontWeight = FontWeight.SemiBold)
+
+ val caption = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 12.sp,
+ )
+ val chip = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 13.sp,
+ lineHeight = 16.sp,
+ )
+ val badge = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 11.sp,
+ )
+ val bookTitle = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 22.sp,
+ )
+ val bookMeta = TextStyle(
+ fontFamily = PretendardFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 10.sp,
+ lineHeight = 22.sp,
+ )
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnBarData.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnBarData.kt
new file mode 100644
index 0000000..7de1c88
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnBarData.kt
@@ -0,0 +1,35 @@
+package com.example.bookon.ui.commonComponent.bar
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.compose.runtime.Immutable
+import com.example.bookon.R
+
+@Immutable
+data class BookOnNavigationItem(
+ @param:StringRes val labelRes: Int,
+ @param:DrawableRes val iconRes: Int,
+)
+
+/**
+ * 앱 하단 주요 목적지 4개를 기본 순서대로 제공한다.
+ * 반환된 항목은 BookOnBottomNavigationBar에서 선택 상태에 맞춰 색상이 적용된다.
+ */
+fun defaultBookOnNavigationItems(): List = listOf(
+ BookOnNavigationItem(
+ labelRes = R.string.nav_home,
+ iconRes = R.drawable.navigation_home,
+ ),
+ BookOnNavigationItem(
+ labelRes = R.string.nav_ranking,
+ iconRes = R.drawable.navigation_rank,
+ ),
+ BookOnNavigationItem(
+ labelRes = R.string.nav_library,
+ iconRes = R.drawable.navigation_library,
+ ),
+ BookOnNavigationItem(
+ labelRes = R.string.nav_my,
+ iconRes = R.drawable.navigation_my,
+ ),
+)
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnBottomNavigationBar.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnBottomNavigationBar.kt
new file mode 100644
index 0000000..a2e2a93
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnBottomNavigationBar.kt
@@ -0,0 +1,130 @@
+package com.example.bookon.ui.commonComponent.bar
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.selection.selectable
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.bookon.theme.AppComponentSize
+import com.example.bookon.theme.AppIconSize
+import com.example.bookon.theme.AppRadius
+import com.example.bookon.theme.AppSpacing
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnBottomNavigationUiState
+
+/**
+ * 앱 하단 주요 목적지 4개를 표시하는 공통 내비게이션 바이다.
+ * 이미지 리소스 아이콘은 선택 상태에 따라 앱 주요 색상 또는 비활성 색상으로 표시된다.
+ */
+@Composable
+fun BookOnBottomNavigationBar(
+ uiState: BookOnBottomNavigationUiState,
+ onItemClick: (Int) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ BookOnBottomNavigationBar(
+ items = uiState.items,
+ selectedIndex = uiState.selectedIndex,
+ onItemClick = onItemClick,
+ modifier = modifier,
+ )
+}
+
+/**
+ * 앱 하단 주요 목적지 4개를 표시하는 공통 내비게이션 바이다.
+ * 이미지 리소스 아이콘은 선택 상태에 따라 앱 주요 색상 또는 비활성 색상으로 표시된다.
+ */
+@Composable
+fun BookOnBottomNavigationBar(
+ items: List,
+ selectedIndex: Int,
+ onItemClick: (Int) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Surface(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(AppComponentSize.NavigationHeight),
+ color = BookOnColor.Surface,
+ tonalElevation = 0.dp,
+ shadowElevation = 0.dp,
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = AppSpacing.ScreenHorizontal),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ items.forEachIndexed { index, item ->
+ val selected = index == selectedIndex
+ val itemColor = if (selected) BookOnColor.Primary else BookOnColor.NavigationInactive
+ val label = stringResource(item.labelRes)
+ Column(
+ modifier = Modifier
+ .clip(RoundedCornerShape(AppRadius.Small))
+ .selectable(
+ selected = selected,
+ role = Role.Tab,
+ onClick = { onItemClick(index) },
+ )
+ .padding(horizontal = AppSpacing.Small, vertical = AppSpacing.Small),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(AppSpacing.Tiny),
+ ) {
+ Box(
+ modifier = Modifier.size(AppIconSize.Default),
+ contentAlignment = Alignment.Center,
+ ) {
+ Image(
+ modifier = Modifier.fillMaxSize(),
+ painter = painterResource(item.iconRes),
+ contentDescription = null,
+ contentScale = ContentScale.Fit,
+ colorFilter = ColorFilter.tint(itemColor),
+ )
+ }
+ Text(
+ text = label,
+ style = BookOnTypography.bookMeta,
+ color = itemColor,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnBottomNavigationBarPreview() {
+ BookOnTheme {
+ BookOnBottomNavigationBar(
+ items = defaultBookOnNavigationItems(),
+ selectedIndex = 0,
+ onItemClick = {},
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnStepProgress.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnStepProgress.kt
new file mode 100644
index 0000000..ba5c13b
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnStepProgress.kt
@@ -0,0 +1,88 @@
+package com.example.bookon.ui.commonComponent.bar
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+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.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.bookon.theme.AppRadius
+import com.example.bookon.theme.AppSpacing
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnStepProgressUiState
+
+/**
+ * 회원가입처럼 순차 단계가 있는 화면에서 현재 진행 상태를 표시한다.
+ * currentStep은 1부터 시작하며 totalStep보다 크면 마지막 단계로 표시한다.
+ */
+@Composable
+fun BookOnStepProgress(
+ uiState: BookOnStepProgressUiState,
+ modifier: Modifier = Modifier,
+) {
+ BookOnStepProgress(
+ currentStep = uiState.currentStep,
+ totalStep = uiState.totalStep,
+ modifier = modifier,
+ )
+}
+
+/**
+ * 회원가입처럼 순차 단계가 있는 화면에서 현재 진행 상태를 표시한다.
+ * currentStep은 1부터 시작하며 totalStep보다 크면 마지막 단계로 표시한다.
+ */
+@Composable
+fun BookOnStepProgress(
+ currentStep: Int,
+ totalStep: Int,
+ modifier: Modifier = Modifier,
+) {
+ val safeTotal = totalStep.coerceAtLeast(1)
+ val safeCurrent = currentStep.coerceIn(1, safeTotal)
+
+ Column(modifier = modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(AppSpacing.Small),
+ ) {
+ repeat(safeTotal) { index ->
+ val selected = index < safeCurrent
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .height(4.dp)
+ .clip(RoundedCornerShape(AppRadius.Progress))
+ .background(if (selected) BookOnColor.Primary else BookOnColor.SurfaceBorder),
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(AppSpacing.Small))
+ Text(
+ text = "STEP $safeCurrent / $safeTotal",
+ style = BookOnTypography.caption,
+ color = BookOnColor.TextTertiary,
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnStepProgressPreview() {
+ BookOnTheme {
+ BookOnStepProgress(
+ currentStep = 2,
+ totalStep = 3,
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnTopBar.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnTopBar.kt
new file mode 100644
index 0000000..32c87c9
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/bar/BookOnTopBar.kt
@@ -0,0 +1,132 @@
+package com.example.bookon.ui.commonComponent.bar
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.bookon.theme.AppComponentSize
+import com.example.bookon.theme.AppIconSize
+import com.example.bookon.theme.AppRadius
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnTopBarUiState
+
+/**
+ * 화면 상단 제목과 뒤로가기, 우측 액션 영역을 제공한다.
+ * Android 시스템 status bar 영역은 앱 Scaffold나 WindowInsets 정책에 맡긴다.
+ */
+@Composable
+fun BookOnTopBar(
+ uiState: BookOnTopBarUiState,
+ modifier: Modifier = Modifier,
+ onBackClick: (() -> Unit)? = null,
+ trailingContent: @Composable () -> Unit = {},
+) {
+ BookOnTopBar(
+ title = uiState.title,
+ modifier = modifier,
+ onBackClick = onBackClick,
+ backContentDescription = uiState.backContentDescription,
+ trailingContent = trailingContent,
+ )
+}
+
+/**
+ * 화면 상단 제목과 뒤로가기, 우측 액션 영역을 제공한다.
+ * Android 시스템 status bar 영역은 앱 Scaffold나 WindowInsets 정책에 맡긴다.
+ */
+@Composable
+fun BookOnTopBar(
+ title: String,
+ modifier: Modifier = Modifier,
+ onBackClick: (() -> Unit)? = null,
+ backContentDescription: String = "뒤로가기",
+ trailingContent: @Composable () -> Unit = {},
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(AppComponentSize.TopBarHeight),
+ ) {
+ if (onBackClick != null) {
+ Box(
+ modifier = Modifier
+ .align(Alignment.CenterStart)
+ .size(40.dp)
+ .clip(RoundedCornerShape(AppRadius.IconButton))
+ .background(BookOnColor.Background)
+ .clickable(
+ role = Role.Button,
+ onClickLabel = backContentDescription,
+ onClick = onBackClick,
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ BackChevron()
+ }
+ }
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = title,
+ style = BookOnTypography.topBarTitle,
+ color = BookOnColor.TextPrimary,
+ )
+
+ Box(
+ modifier = Modifier.align(Alignment.CenterEnd),
+ contentAlignment = Alignment.Center,
+ ) {
+ trailingContent()
+ }
+ }
+}
+
+@Composable
+private fun BackChevron() {
+ Canvas(modifier = Modifier.size(AppIconSize.Small)) {
+ val strokeWidth = 2.dp.toPx()
+ val startX = size.width * 0.6f
+ val centerX = size.width * 0.35f
+ drawLine(
+ color = BookOnColor.TextPrimary,
+ start = Offset(startX, size.height * 0.2f),
+ end = Offset(centerX, size.height * 0.5f),
+ strokeWidth = strokeWidth,
+ cap = StrokeCap.Round,
+ )
+ drawLine(
+ color = BookOnColor.TextPrimary,
+ start = Offset(centerX, size.height * 0.5f),
+ end = Offset(startX, size.height * 0.8f),
+ strokeWidth = strokeWidth,
+ cap = StrokeCap.Round,
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnTopBarPreview() {
+ BookOnTheme {
+ BookOnTopBar(
+ title = "내 서재",
+ onBackClick = {},
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/button/BookOnButton.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/button/BookOnButton.kt
new file mode 100644
index 0000000..f84418b
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/button/BookOnButton.kt
@@ -0,0 +1,130 @@
+package com.example.bookon.ui.commonComponent.button
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.tooling.preview.Preview
+import com.example.bookon.R
+import com.example.bookon.theme.AppComponentSize
+import com.example.bookon.theme.AppElevation
+import com.example.bookon.theme.AppRadius
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnPrimaryButtonUiState
+
+/**
+ * Figma의 52dp 녹색 CTA 버튼을 앱 공통 스타일로 제공한다.
+ * enabled와 loading 상태에 따라 클릭 가능 여부와 표시 방식을 함께 제어한다.
+ */
+@Composable
+fun BookOnPrimaryButton(
+ uiState: BookOnPrimaryButtonUiState,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ BookOnPrimaryButton(
+ text = uiState.text,
+ onClick = onClick,
+ modifier = modifier,
+ enabled = uiState.enabled,
+ loading = uiState.loading,
+ )
+}
+
+/**
+ * Figma의 52dp 녹색 CTA 버튼을 앱 공통 스타일로 제공한다.
+ * enabled와 loading 상태에 따라 클릭 가능 여부와 표시 방식을 함께 제어한다.
+ */
+@Composable
+fun BookOnPrimaryButton(
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ loading: Boolean = false,
+) {
+ val isClickable = enabled && !loading
+ val targetBackgroundColor = if (enabled) BookOnColor.Primary else BookOnColor.Disabled
+ val targetContentColor = if (enabled) BookOnColor.Surface else BookOnColor.TextPlaceholder
+ val backgroundColor by animateColorAsState(targetValue = targetBackgroundColor)
+ val contentColor by animateColorAsState(targetValue = targetContentColor)
+ val loadingDescription = stringResource(R.string.state_loading)
+
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(AppComponentSize.ButtonHeight)
+ .shadow(
+ elevation = if (enabled) AppElevation.Button else AppElevation.None,
+ shape = RoundedCornerShape(AppRadius.Button),
+ )
+ .clip(RoundedCornerShape(AppRadius.Button))
+ .background(backgroundColor)
+ .semantics {
+ if (!enabled) disabled()
+ if (loading) stateDescription = loadingDescription
+ }
+ .clickable(
+ enabled = isClickable,
+ role = Role.Button,
+ onClick = onClick,
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ if (loading) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(AppComponentSize.ChipHeight / 2),
+ color = contentColor,
+ strokeWidth = AppRadius.Progress,
+ )
+ } else {
+ Text(
+ text = text,
+ style = BookOnTypography.button,
+ color = contentColor,
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnPrimaryButtonPreview() {
+ BookOnTheme {
+ BookOnPrimaryButton(
+ text = "다음",
+ onClick = {},
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnPrimaryButtonDisabledPreview() {
+ BookOnTheme {
+ BookOnPrimaryButton(
+ text = "다음",
+ onClick = {},
+ enabled = false,
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/button/BookOnLogoutButton.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/button/BookOnLogoutButton.kt
new file mode 100644
index 0000000..00920a3
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/button/BookOnLogoutButton.kt
@@ -0,0 +1,19 @@
+package com.example.bookon.ui.commonComponent.button
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.example.bookon.R
+import com.example.bookon.ui.commonComponent.row.BookOnMenuRow
+
+/**
+ * 마이페이지 메뉴 목록과 분리된 로그아웃 전용 버튼이다.
+ */
+@Composable
+fun BookOnLogoutButton(onClick: () -> Unit) {
+ BookOnMenuRow(
+ title = stringResource(R.string.action_logout),
+ onClick = onClick,
+ destructive = true,
+ showDivider = false,
+ )
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/card/BookOnBookCard.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/card/BookOnBookCard.kt
new file mode 100644
index 0000000..cfd0825
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/card/BookOnBookCard.kt
@@ -0,0 +1,127 @@
+package com.example.bookon.ui.commonComponent.card
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import com.example.bookon.theme.AppComponentSize
+import com.example.bookon.theme.AppElevation
+import com.example.bookon.theme.AppRadius
+import com.example.bookon.theme.AppSpacing
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnBookCardUiState
+
+/**
+ * 책 표지와 제목/저자 정보를 표시하는 공통 카드이다.
+ * cover는 이미지 로더나 로컬 drawable을 연결할 수 있도록 slot으로 받는다.
+ */
+@Composable
+fun BookOnBookCard(
+ uiState: BookOnBookCardUiState,
+ modifier: Modifier = Modifier,
+ titleStyle: TextStyle = BookOnTypography.bookTitle,
+ coverWidth: Dp = AppComponentSize.BookCoverWidth,
+ coverHeight: Dp = AppComponentSize.BookCoverHeight,
+ cardWidth: Dp = coverWidth,
+ cover: @Composable () -> Unit = { BookCoverPlaceholder(coverHeight) },
+) {
+ BookOnBookCard(
+ title = uiState.title,
+ author = uiState.author,
+ modifier = modifier,
+ cover = cover,
+ titleStyle = titleStyle,
+ coverWidth = coverWidth,
+ coverHeight = coverHeight,
+ cardWidth = cardWidth,
+ )
+}
+
+/**
+ * 책 표지와 제목/저자 정보를 표시하는 공통 카드이다.
+ * cover는 이미지 로더나 로컬 drawable을 연결할 수 있도록 slot으로 받는다.
+ */
+@Composable
+fun BookOnBookCard(
+ title: String,
+ author: String,
+ modifier: Modifier = Modifier,
+ titleStyle: TextStyle = BookOnTypography.bookTitle,
+ coverWidth: Dp = AppComponentSize.BookCoverWidth,
+ coverHeight: Dp = AppComponentSize.BookCoverHeight,
+ cardWidth: Dp = coverWidth,
+ cover: @Composable () -> Unit = { BookCoverPlaceholder(coverHeight) },
+) {
+ Column(modifier = modifier.width(cardWidth)) {
+ Box(
+ modifier = Modifier
+ .size(
+ width = coverWidth,
+ height = coverHeight,
+ )
+ .shadow(
+ elevation = AppElevation.BookCover,
+ shape = RoundedCornerShape(AppRadius.Small),
+ )
+ .clip(RoundedCornerShape(AppRadius.Small)),
+ contentAlignment = Alignment.Center,
+ ) {
+ cover()
+ }
+ Spacer(modifier = Modifier.height(AppSpacing.Item))
+ Text(
+ text = title,
+ style = titleStyle,
+ color = BookOnColor.TextPrimary,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Text(
+ text = author,
+ style = BookOnTypography.bookMeta,
+ color = BookOnColor.TextPrimary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+}
+
+@Composable
+private fun BookCoverPlaceholder(
+ height: Dp = AppComponentSize.BookCoverHeight,
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(height)
+ .background(BookOnColor.BookCoverPlaceholder),
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnBookCardPreview() {
+ BookOnTheme {
+ BookOnBookCard(
+ title = "자몽 살구 클럽",
+ author = "한로로",
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/card/BookOnInfoCard.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/card/BookOnInfoCard.kt
new file mode 100644
index 0000000..1665d46
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/card/BookOnInfoCard.kt
@@ -0,0 +1,126 @@
+package com.example.bookon.ui.commonComponent.card
+
+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.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.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import com.example.bookon.theme.AppComponentSize
+import com.example.bookon.theme.AppIconSize
+import com.example.bookon.theme.AppRadius
+import com.example.bookon.theme.AppSpacing
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnInfoCardUiState
+
+/**
+ * 안내, 연동, 공지 등 아이콘과 설명이 함께 있는 공통 카드이다.
+ * leadingContent와 trailingContent는 화면별 아이콘이나 토글을 연결할 때 사용한다.
+ */
+@Composable
+fun BookOnInfoCard(
+ uiState: BookOnInfoCardUiState,
+ modifier: Modifier = Modifier,
+ leadingContent: (@Composable () -> Unit)? = null,
+ trailingContent: (@Composable () -> Unit)? = null,
+) {
+ BookOnInfoCard(
+ title = uiState.title,
+ modifier = modifier,
+ description = uiState.description,
+ containerColor = uiState.containerColor,
+ borderColor = uiState.borderColor,
+ leadingContent = leadingContent,
+ trailingContent = trailingContent,
+ )
+}
+
+/**
+ * 안내, 연동, 공지 등 아이콘과 설명이 함께 있는 공통 카드이다.
+ * leadingContent와 trailingContent는 화면별 아이콘이나 토글을 연결할 때 사용한다.
+ */
+@Composable
+fun BookOnInfoCard(
+ title: String,
+ modifier: Modifier = Modifier,
+ description: String? = null,
+ containerColor: Color = BookOnColor.Background,
+ borderColor: Color = BookOnColor.SurfaceBorder,
+ leadingContent: (@Composable () -> Unit)? = null,
+ trailingContent: (@Composable () -> Unit)? = null,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(AppRadius.Card))
+ .background(containerColor)
+ .padding(AppSpacing.Content),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (leadingContent != null) {
+ Box(
+ modifier = Modifier
+ .size(AppComponentSize.InfoCardIcon)
+ .clip(RoundedCornerShape(AppRadius.IconButton))
+ .background(borderColor),
+ contentAlignment = Alignment.Center,
+ ) {
+ leadingContent()
+ }
+ Spacer(modifier = Modifier.width(AppSpacing.Content))
+ }
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ style = BookOnTypography.bodySemiBold,
+ color = BookOnColor.TextPrimary,
+ )
+ if (description != null) {
+ Spacer(modifier = Modifier.height(AppSpacing.Tiny))
+ Text(
+ text = description,
+ style = BookOnTypography.caption,
+ color = BookOnColor.TextSecondary,
+ )
+ }
+ }
+
+ if (trailingContent != null) {
+ Spacer(modifier = Modifier.width(AppSpacing.Content))
+ trailingContent()
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnInfoCardPreview() {
+ BookOnTheme {
+ BookOnInfoCard(
+ title = "2026 독서마라톤",
+ description = "아직 연동하지 않았어요",
+ leadingContent = {
+ Box(
+ modifier = Modifier
+ .size(AppIconSize.Small)
+ .background(BookOnColor.Primary),
+ )
+ },
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/card/BookOnStatSummaryCard.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/card/BookOnStatSummaryCard.kt
new file mode 100644
index 0000000..6cc5557
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/card/BookOnStatSummaryCard.kt
@@ -0,0 +1,124 @@
+package com.example.bookon.ui.commonComponent.card
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+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.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.bookon.theme.AppComponentSize
+import com.example.bookon.theme.AppElevation
+import com.example.bookon.theme.AppRadius
+import com.example.bookon.theme.AppSpacing
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnStatSummaryCardUiState
+
+@Immutable
+data class BookOnStatItem(
+ val label: String,
+ val value: String,
+)
+
+/**
+ * 마이페이지의 대출 현황처럼 2~3개 수치를 한 줄 카드로 표시한다.
+ * 값은 호출 측에서 이미 화면 문자열로 만든 뒤 전달한다.
+ */
+@Composable
+fun BookOnStatSummaryCard(
+ uiState: BookOnStatSummaryCardUiState,
+ modifier: Modifier = Modifier,
+) {
+ BookOnStatSummaryCard(
+ items = uiState.items,
+ modifier = modifier,
+ )
+}
+
+/**
+ * 마이페이지의 대출 현황처럼 2~3개 수치를 한 줄 카드로 표시한다.
+ * 값은 호출 측에서 이미 화면 문자열로 만든 뒤 전달한다.
+ */
+@Composable
+fun BookOnStatSummaryCard(
+ items: List,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(AppComponentSize.StatSummaryCardHeight)
+ .shadow(
+ elevation = AppElevation.StrongCard,
+ shape = RoundedCornerShape(AppRadius.Card),
+ )
+ .clip(RoundedCornerShape(AppRadius.Card))
+ .background(
+ brush = Brush.horizontalGradient(
+ colors = listOf(BookOnColor.Primary, BookOnColor.PrimaryDark),
+ ),
+ )
+ .padding(vertical = AppSpacing.Content),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ items.forEachIndexed { index, item ->
+ Column(
+ modifier = Modifier.weight(1f),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(
+ text = item.label,
+ style = BookOnTypography.caption,
+ color = BookOnColor.PrimaryLight,
+ )
+ Spacer(modifier = Modifier.height(AppSpacing.Tiny))
+ Text(
+ text = item.value,
+ style = BookOnTypography.sectionTitle,
+ color = BookOnColor.PrimaryLight,
+ )
+ }
+
+ if (index < items.lastIndex) {
+ Box(
+ modifier = Modifier
+ .width(1.dp)
+ .fillMaxHeight()
+ .background(BookOnColor.PrimaryLight.copy(alpha = 0.45f)),
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnStatSummaryCardPreview() {
+ BookOnTheme {
+ BookOnStatSummaryCard(
+ items = listOf(
+ BookOnStatItem("대출 중", "3권"),
+ BookOnStatItem("반납 임박", "2권"),
+ BookOnStatItem("누적 대출", "23권"),
+ ),
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/chip/BookOnChips.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/chip/BookOnChips.kt
new file mode 100644
index 0000000..db94aa6
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/chip/BookOnChips.kt
@@ -0,0 +1,81 @@
+package com.example.bookon.ui.commonComponent.chip
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.tooling.preview.Preview
+import com.example.bookon.theme.AppComponentSize
+import com.example.bookon.theme.AppRadius
+import com.example.bookon.theme.AppSpacing
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnFilterChipUiState
+
+/**
+ * 검색 필터와 내역 상태 필터에 쓰는 선택형 칩이다.
+ * 선택 상태는 배경색과 텍스트 색상만으로 구분되지 않도록 selected 값을 semantics에 연결한다.
+ */
+@Composable
+fun BookOnFilterChip(
+ uiState: BookOnFilterChipUiState,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ BookOnFilterChip(
+ text = uiState.text,
+ selected = uiState.selected,
+ onClick = onClick,
+ modifier = modifier,
+ )
+}
+
+/**
+ * 검색 필터와 내역 상태 필터에 쓰는 선택형 칩이다.
+ * 선택 상태는 배경색과 텍스트 색상만으로 구분되지 않도록 selected 값을 semantics에 연결한다.
+ */
+@Composable
+fun BookOnFilterChip(
+ text: String,
+ selected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier
+ .height(AppComponentSize.ChipHeight)
+ .clip(RoundedCornerShape(AppRadius.Chip))
+ .background(if (selected) BookOnColor.PrimaryPressed else BookOnColor.SurfaceAlt)
+ .clickable(role = Role.Button, onClick = onClick)
+ .padding(horizontal = AppSpacing.Content),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = text,
+ style = BookOnTypography.chip,
+ color = if (selected) BookOnColor.Surface else BookOnColor.TextDarkGray,
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnFilterChipPreview() {
+ BookOnTheme {
+ Row(horizontalArrangement = Arrangement.spacedBy(AppSpacing.Small)) {
+ BookOnFilterChip(text = "대출 중 2", selected = false, onClick = {})
+ BookOnFilterChip(text = "반납 완료", selected = true, onClick = {})
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/row/BookOnMenuRow.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/row/BookOnMenuRow.kt
new file mode 100644
index 0000000..1f03855
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/row/BookOnMenuRow.kt
@@ -0,0 +1,126 @@
+package com.example.bookon.ui.commonComponent.row
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.bookon.theme.AppIconSize
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnMenuRowUiState
+
+/**
+ * 마이페이지 설정 목록처럼 제목, 우측 액션, 하단 구분선을 가진 행이다.
+ * onClick이 null이면 읽기 전용 행으로 표시한다.
+ */
+@Composable
+fun BookOnMenuRow(
+ uiState: BookOnMenuRowUiState,
+ modifier: Modifier = Modifier,
+ onClick: (() -> Unit)? = null,
+ trailingContent: @Composable () -> Unit = { MenuChevron() },
+) {
+ BookOnMenuRow(
+ title = uiState.title,
+ modifier = modifier,
+ onClick = onClick,
+ destructive = uiState.destructive,
+ showDivider = uiState.showDivider,
+ trailingContent = trailingContent,
+ )
+}
+
+/**
+ * 마이페이지 설정 목록처럼 제목, 우측 액션, 하단 구분선을 가진 행이다.
+ * onClick이 null이면 읽기 전용 행으로 표시한다.
+ */
+@Composable
+fun BookOnMenuRow(
+ title: String,
+ modifier: Modifier = Modifier,
+ onClick: (() -> Unit)? = null,
+ destructive: Boolean = false,
+ showDivider: Boolean = true,
+ trailingContent: @Composable () -> Unit = { MenuChevron() },
+) {
+ Column(modifier = modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(51.dp)
+ .then(
+ if (onClick != null) {
+ Modifier.clickable(role = Role.Button, onClick = onClick)
+ } else {
+ Modifier
+ },
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = title,
+ style = BookOnTypography.bodySemiBold,
+ color = if (destructive) BookOnColor.Error else BookOnColor.TextPrimary,
+ )
+ trailingContent()
+ }
+ if (showDivider) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(BookOnColor.Divider),
+ )
+ }
+ }
+}
+
+@Composable
+private fun MenuChevron() {
+ Canvas(modifier = Modifier.size(AppIconSize.Small)) {
+ val strokeWidth = 1.5.dp.toPx()
+ val startX = size.width * 0.35f
+ val endX = size.width * 0.65f
+ drawLine(
+ color = BookOnColor.TextPlaceholder,
+ start = Offset(startX, size.height * 0.2f),
+ end = Offset(endX, size.height * 0.5f),
+ strokeWidth = strokeWidth,
+ cap = StrokeCap.Round,
+ )
+ drawLine(
+ color = BookOnColor.TextPlaceholder,
+ start = Offset(endX, size.height * 0.5f),
+ end = Offset(startX, size.height * 0.8f),
+ strokeWidth = strokeWidth,
+ cap = StrokeCap.Round,
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnMenuRowPreview() {
+ BookOnTheme {
+ BookOnMenuRow(
+ title = "대출 / 반납 내역",
+ onClick = {},
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/row/BookOnSwitchRow.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/row/BookOnSwitchRow.kt
new file mode 100644
index 0000000..b2f8168
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/row/BookOnSwitchRow.kt
@@ -0,0 +1,97 @@
+package com.example.bookon.ui.commonComponent.row
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Switch
+import androidx.compose.material3.SwitchDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.example.bookon.theme.AppSpacing
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnSwitchRowUiState
+
+/**
+ * 제목과 설명, Switch를 한 행으로 묶어 알림/연동 설정에 사용한다.
+ */
+@Composable
+fun BookOnSwitchRow(
+ uiState: BookOnSwitchRowUiState,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ BookOnSwitchRow(
+ title = uiState.title,
+ checked = uiState.checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier,
+ description = uiState.description,
+ )
+}
+
+/**
+ * 제목과 설명, Switch를 한 행으로 묶어 알림/연동 설정에 사용한다.
+ */
+@Composable
+fun BookOnSwitchRow(
+ title: String,
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ description: String? = null,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(vertical = AppSpacing.Item, horizontal = AppSpacing.Item),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ style = BookOnTypography.bodySemiBold,
+ color = BookOnColor.TextPrimary,
+ )
+ if (description != null) {
+ Spacer(modifier = Modifier.height(AppSpacing.Tiny))
+ Text(
+ text = description,
+ style = BookOnTypography.caption,
+ color = BookOnColor.TextSecondary,
+ )
+ }
+ }
+ Switch(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ colors = SwitchDefaults.colors(
+ checkedThumbColor = BookOnColor.Surface,
+ checkedTrackColor = BookOnColor.Primary,
+ uncheckedThumbColor = BookOnColor.Surface,
+ uncheckedTrackColor = BookOnColor.SwitchOff,
+ uncheckedBorderColor = BookOnColor.SwitchOff,
+ ),
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnSwitchRowPreview() {
+ BookOnTheme {
+ BookOnSwitchRow(
+ title = "반납 알림",
+ description = "반납 3일 전과 당일에 알려드려요",
+ checked = true,
+ onCheckedChange = {},
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/textfield/BookOnPasswordField.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/textfield/BookOnPasswordField.kt
new file mode 100644
index 0000000..6e13d85
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/textfield/BookOnPasswordField.kt
@@ -0,0 +1,130 @@
+package com.example.bookon.ui.commonComponent.textfield
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.res.stringResource
+import com.example.bookon.R
+import com.example.bookon.theme.AppComponentSize
+import com.example.bookon.theme.AppSpacing
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnPasswordFieldUiState
+
+/**
+ * 비밀번호 표시 전환을 포함한 입력 필드이다.
+ * trailingIcon을 직접 넘기지 않아도 텍스트 토글로 표시 상태를 제어한다.
+ */
+@Composable
+fun BookOnPasswordField(
+ uiState: BookOnPasswordFieldUiState,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ visibleLabel: String? = null,
+ hiddenLabel: String? = null,
+) {
+ BookOnPasswordField(
+ value = uiState.value,
+ onValueChange = onValueChange,
+ modifier = modifier,
+ label = uiState.label,
+ placeholder = uiState.placeholder,
+ errorText = uiState.errorText,
+ enabled = uiState.enabled,
+ visibleLabel = visibleLabel,
+ hiddenLabel = hiddenLabel,
+ )
+}
+
+/**
+ * 비밀번호 표시 전환을 포함한 입력 필드이다.
+ * trailingIcon을 직접 넘기지 않아도 텍스트 토글로 표시 상태를 제어한다.
+ */
+@Composable
+fun BookOnPasswordField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ label: String? = null,
+ placeholder: String = "",
+ errorText: String? = null,
+ enabled: Boolean = true,
+ visibleLabel: String? = null,
+ hiddenLabel: String? = null,
+) {
+ var passwordVisible by rememberSaveable { mutableStateOf(false) }
+ val showPasswordText = hiddenLabel ?: stringResource(R.string.action_show_password)
+ val hidePasswordText = visibleLabel ?: stringResource(R.string.action_hide_password)
+ val toggleDescription = stringResource(R.string.password_visibility_toggle_description)
+
+ BookOnTextField(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = modifier,
+ label = label,
+ placeholder = placeholder,
+ errorText = errorText,
+ enabled = enabled,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
+ visualTransformation = if (passwordVisible) {
+ VisualTransformation.None
+ } else {
+ PasswordVisualTransformation()
+ },
+ trailingIcon = {
+ Box(
+ modifier = Modifier
+ .sizeIn(
+ minWidth = AppComponentSize.MinTouchTarget,
+ minHeight = AppComponentSize.MinTouchTarget,
+ )
+ .semantics {
+ contentDescription = toggleDescription
+ }
+ .clickable(
+ enabled = enabled,
+ role = Role.Button,
+ onClick = { passwordVisible = !passwordVisible },
+ )
+ .padding(horizontal = AppSpacing.Small),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = if (passwordVisible) hidePasswordText else showPasswordText,
+ style = BookOnTypography.caption,
+ color = BookOnColor.TextPlaceholder,
+ )
+ }
+ },
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnPasswordFieldPreview() {
+ BookOnTheme {
+ BookOnPasswordField(
+ value = "password",
+ onValueChange = {},
+ label = "비밀번호",
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/ui/commonComponent/textfield/BookOnTextFieldInput.kt b/app/src/main/java/com/example/bookon/ui/commonComponent/textfield/BookOnTextFieldInput.kt
new file mode 100644
index 0000000..b3be390
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/ui/commonComponent/textfield/BookOnTextFieldInput.kt
@@ -0,0 +1,231 @@
+package com.example.bookon.ui.commonComponent.textfield
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
+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.sizeIn
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.semantics.error
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import com.example.bookon.theme.AppComponentSize
+import com.example.bookon.theme.AppElevation
+import com.example.bookon.theme.AppIconSize
+import com.example.bookon.theme.AppRadius
+import com.example.bookon.theme.AppSpacing
+import com.example.bookon.theme.BookOnColor
+import com.example.bookon.theme.BookOnTheme
+import com.example.bookon.theme.BookOnTypography
+import com.example.bookon.uiState.BookOnTextFieldUiState
+
+/**
+ * 라벨, 아이콘, 오류 문구를 포함한 BookOn 입력 필드이다.
+ * 화면은 value와 onValueChange만 전달하고 검증 결과는 isError와 errorText로 표현한다.
+ */
+@Composable
+fun BookOnTextField(
+ uiState: BookOnTextFieldUiState,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ singleLine: Boolean = true,
+ textStyle: TextStyle = BookOnTypography.fieldText,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ leadingIcon: (@Composable () -> Unit)? = null,
+ trailingIcon: (@Composable () -> Unit)? = null,
+) {
+ BookOnTextField(
+ value = uiState.value,
+ onValueChange = onValueChange,
+ modifier = modifier,
+ label = uiState.label,
+ placeholder = uiState.placeholder,
+ errorText = uiState.errorText,
+ isError = uiState.isError,
+ enabled = uiState.enabled,
+ singleLine = singleLine,
+ textStyle = textStyle,
+ keyboardOptions = keyboardOptions,
+ visualTransformation = visualTransformation,
+ leadingIcon = leadingIcon,
+ trailingIcon = trailingIcon,
+ )
+}
+
+/**
+ * 라벨, 아이콘, 오류 문구를 포함한 BookOn 입력 필드이다.
+ * 화면은 value와 onValueChange만 전달하고 검증 결과는 isError와 errorText로 표현한다.
+ */
+@Composable
+fun BookOnTextField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ label: String? = null,
+ placeholder: String = "",
+ errorText: String? = null,
+ isError: Boolean = errorText != null,
+ enabled: Boolean = true,
+ singleLine: Boolean = true,
+ textStyle: TextStyle = BookOnTypography.fieldText,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ leadingIcon: (@Composable () -> Unit)? = null,
+ trailingIcon: (@Composable () -> Unit)? = null,
+) {
+ val shape = RoundedCornerShape(AppRadius.Field)
+ val interactionSource = remember { MutableInteractionSource() }
+ val isFocused by interactionSource.collectIsFocusedAsState()
+ val borderColor by animateColorAsState(
+ targetValue = when {
+ isError -> BookOnColor.Error
+ isFocused -> BookOnColor.Primary
+ else -> BookOnColor.Surface
+ },
+ )
+ val containerColor by animateColorAsState(
+ targetValue = if (isError) BookOnColor.ErrorContainer else BookOnColor.Surface,
+ )
+
+ Column(modifier = modifier.fillMaxWidth()) {
+ if (label != null) {
+ Text(
+ text = label,
+ style = BookOnTypography.fieldLabel,
+ color = BookOnColor.TextPrimary,
+ )
+ Spacer(modifier = Modifier.height(AppSpacing.Small))
+ }
+
+ BasicTextField(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(AppComponentSize.FieldHeight)
+ .shadow(
+ elevation = AppElevation.Field,
+ shape = shape,
+ )
+ .clip(shape)
+ .background(containerColor)
+ .border(
+ width = AppComponentSize.FieldBorderWidth,
+ color = borderColor,
+ shape = shape,
+ )
+ .semantics {
+ if (isError && errorText != null) error(errorText)
+ },
+ enabled = enabled,
+ singleLine = singleLine,
+ textStyle = textStyle.copy(color = BookOnColor.TextPrimary),
+ keyboardOptions = keyboardOptions,
+ visualTransformation = visualTransformation,
+ cursorBrush = SolidColor(BookOnColor.Primary),
+ interactionSource = interactionSource,
+ decorationBox = { innerTextField ->
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = AppSpacing.FieldHorizontal),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (leadingIcon != null) {
+ Box(
+ modifier = Modifier.size(AppIconSize.Small),
+ contentAlignment = Alignment.Center,
+ ) {
+ leadingIcon()
+ }
+ Spacer(modifier = Modifier.width(AppSpacing.Content))
+ }
+
+ Box(modifier = Modifier.weight(1f)) {
+ if (value.isEmpty()) {
+ Text(
+ text = placeholder,
+ style = BookOnTypography.fieldPlaceholder,
+ color = BookOnColor.TextPlaceholder,
+ )
+ }
+ innerTextField()
+ }
+
+ if (trailingIcon != null) {
+ Spacer(modifier = Modifier.width(AppSpacing.Item))
+ Box(
+ modifier = Modifier.sizeIn(
+ minWidth = AppComponentSize.MinTouchTarget,
+ minHeight = AppComponentSize.MinTouchTarget,
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ trailingIcon()
+ }
+ }
+ }
+ },
+ )
+
+ if (errorText != null) {
+ Spacer(modifier = Modifier.height(AppSpacing.Small))
+ Text(
+ text = errorText,
+ style = BookOnTypography.caption,
+ color = BookOnColor.Error,
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnTextFieldPreview() {
+ BookOnTheme {
+ BookOnTextField(
+ value = "",
+ onValueChange = {},
+ label = "학교 이메일",
+ placeholder = "이메일 주소",
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BookOnTextFieldErrorPreview() {
+ BookOnTheme {
+ BookOnTextField(
+ value = "student",
+ onValueChange = {},
+ label = "학교 이메일",
+ errorText = "올바른 이메일 형식이 아니에요",
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/bookon/uiState/BookOnBarsUiState.kt b/app/src/main/java/com/example/bookon/uiState/BookOnBarsUiState.kt
new file mode 100644
index 0000000..5e17c53
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/uiState/BookOnBarsUiState.kt
@@ -0,0 +1,22 @@
+package com.example.bookon.uiState
+
+import androidx.compose.runtime.Immutable
+import com.example.bookon.ui.commonComponent.bar.BookOnNavigationItem
+
+@Immutable
+data class BookOnTopBarUiState(
+ val title: String,
+ val backContentDescription: String = "뒤로가기",
+)
+
+@Immutable
+data class BookOnStepProgressUiState(
+ val currentStep: Int,
+ val totalStep: Int,
+)
+
+@Immutable
+data class BookOnBottomNavigationUiState(
+ val items: List,
+ val selectedIndex: Int,
+)
diff --git a/app/src/main/java/com/example/bookon/uiState/BookOnButtonUiState.kt b/app/src/main/java/com/example/bookon/uiState/BookOnButtonUiState.kt
new file mode 100644
index 0000000..6281aa2
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/uiState/BookOnButtonUiState.kt
@@ -0,0 +1,10 @@
+package com.example.bookon.uiState
+
+import androidx.compose.runtime.Immutable
+
+@Immutable
+data class BookOnPrimaryButtonUiState(
+ val text: String,
+ val enabled: Boolean = true,
+ val loading: Boolean = false,
+)
diff --git a/app/src/main/java/com/example/bookon/uiState/BookOnCardsUiState.kt b/app/src/main/java/com/example/bookon/uiState/BookOnCardsUiState.kt
new file mode 100644
index 0000000..00adec5
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/uiState/BookOnCardsUiState.kt
@@ -0,0 +1,46 @@
+package com.example.bookon.uiState
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+import com.example.bookon.ui.commonComponent.card.BookOnStatItem
+import com.example.bookon.theme.BookOnColor
+
+@Immutable
+data class BookOnStatSummaryCardUiState(
+ val items: List,
+)
+
+@Immutable
+data class BookOnInfoCardUiState(
+ val title: String,
+ val description: String? = null,
+ val containerColor: Color = BookOnColor.Background,
+ val borderColor: Color = BookOnColor.SurfaceBorder,
+)
+
+@Immutable
+data class BookOnMenuRowUiState(
+ val title: String,
+ val destructive: Boolean = false,
+ val showDivider: Boolean = true,
+)
+
+@Immutable
+data class BookOnFilterChipUiState(
+ val text: String,
+ val selected: Boolean,
+)
+
+@Immutable
+data class BookOnSwitchRowUiState(
+ val title: String,
+ val checked: Boolean,
+ val description: String? = null,
+)
+
+@Immutable
+data class BookOnBookCardUiState(
+ val title: String,
+ val author: String,
+ val coverImageUrl: String? = null,
+)
diff --git a/app/src/main/java/com/example/bookon/uiState/BookOnTextFieldUiState.kt b/app/src/main/java/com/example/bookon/uiState/BookOnTextFieldUiState.kt
new file mode 100644
index 0000000..4a556b9
--- /dev/null
+++ b/app/src/main/java/com/example/bookon/uiState/BookOnTextFieldUiState.kt
@@ -0,0 +1,22 @@
+package com.example.bookon.uiState
+
+import androidx.compose.runtime.Immutable
+
+@Immutable
+data class BookOnTextFieldUiState(
+ val value: String,
+ val label: String? = null,
+ val placeholder: String = "",
+ val errorText: String? = null,
+ val isError: Boolean = errorText != null,
+ val enabled: Boolean = true,
+)
+
+@Immutable
+data class BookOnPasswordFieldUiState(
+ val value: String,
+ val label: String? = null,
+ val placeholder: String = "",
+ val errorText: String? = null,
+ val enabled: Boolean = true,
+)
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/navigation_home.png b/app/src/main/res/drawable/navigation_home.png
new file mode 100644
index 0000000..2823923
Binary files /dev/null and b/app/src/main/res/drawable/navigation_home.png differ
diff --git a/app/src/main/res/drawable/navigation_library.png b/app/src/main/res/drawable/navigation_library.png
new file mode 100644
index 0000000..bdb983c
Binary files /dev/null and b/app/src/main/res/drawable/navigation_library.png differ
diff --git a/app/src/main/res/drawable/navigation_my.png b/app/src/main/res/drawable/navigation_my.png
new file mode 100644
index 0000000..43c9a16
Binary files /dev/null and b/app/src/main/res/drawable/navigation_my.png differ
diff --git a/app/src/main/res/drawable/navigation_rank.png b/app/src/main/res/drawable/navigation_rank.png
new file mode 100644
index 0000000..ded3c99
Binary files /dev/null and b/app/src/main/res/drawable/navigation_rank.png differ
diff --git a/app/src/main/res/font/pretendard_black.otf b/app/src/main/res/font/pretendard_black.otf
new file mode 100644
index 0000000..a0d849e
Binary files /dev/null and b/app/src/main/res/font/pretendard_black.otf differ
diff --git a/app/src/main/res/font/pretendard_bold.otf b/app/src/main/res/font/pretendard_bold.otf
new file mode 100644
index 0000000..8e5e30a
Binary files /dev/null and b/app/src/main/res/font/pretendard_bold.otf differ
diff --git a/app/src/main/res/font/pretendard_extra_bold.otf b/app/src/main/res/font/pretendard_extra_bold.otf
new file mode 100644
index 0000000..388f3ca
Binary files /dev/null and b/app/src/main/res/font/pretendard_extra_bold.otf differ
diff --git a/app/src/main/res/font/pretendard_extra_light.otf b/app/src/main/res/font/pretendard_extra_light.otf
new file mode 100644
index 0000000..40c8b69
Binary files /dev/null and b/app/src/main/res/font/pretendard_extra_light.otf differ
diff --git a/app/src/main/res/font/pretendard_light.otf b/app/src/main/res/font/pretendard_light.otf
new file mode 100644
index 0000000..228679e
Binary files /dev/null and b/app/src/main/res/font/pretendard_light.otf differ
diff --git a/app/src/main/res/font/pretendard_medium.otf b/app/src/main/res/font/pretendard_medium.otf
new file mode 100644
index 0000000..0575069
Binary files /dev/null and b/app/src/main/res/font/pretendard_medium.otf differ
diff --git a/app/src/main/res/font/pretendard_regular.otf b/app/src/main/res/font/pretendard_regular.otf
new file mode 100644
index 0000000..08bf4cf
Binary files /dev/null and b/app/src/main/res/font/pretendard_regular.otf differ
diff --git a/app/src/main/res/font/pretendard_semi_bold.otf b/app/src/main/res/font/pretendard_semi_bold.otf
new file mode 100644
index 0000000..e7e36ab
Binary files /dev/null and b/app/src/main/res/font/pretendard_semi_bold.otf differ
diff --git a/app/src/main/res/font/pretendard_thin.otf b/app/src/main/res/font/pretendard_thin.otf
new file mode 100644
index 0000000..77e792d
Binary files /dev/null and b/app/src/main/res/font/pretendard_thin.otf differ
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..63ccc9e
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,174 @@
+
+ BookOn
+
+
+ 다음
+ 확인
+ 가입 완료
+ 시작하기
+ 더보기
+ 자세히 보기
+ 검색
+ 인증하고 계속하기
+ 로그아웃
+ 보기
+ 숨김
+ 처리 중
+ 비밀번호 표시 전환
+ 가능
+ 연동됨
+ 참여 중
+
+
+ 환영합니다 !
+ 로그인
+ 회원가입
+ 회원가입 하러가기
+ 비밀번호를 잊으셨나요?
+ 올바른 이메일 형식이 아니에요
+ 이메일이 옳지 않습니다
+ 비밀번호는 6자 이상이어야 해요
+
+
+ STEP 1 / 3
+ STEP 2 / 3
+ STEP 3 / 3
+ 학교 정보
+ 계정 정보
+ 학교 계정으로 간편하게 가입하세요!
+ 학교 이메일
+ 이메일 주소
+ @gsm.hs.kr
+ 이름
+ 성별
+ 남자
+ 여자
+ 학과
+ 소프트웨어 개발과
+ IoT과
+ AI과
+ 비밀번호
+ 비밀번호 확인
+ 비밀번호 주의사항
+ 비밀번호 유의사항
+ 영문(대·소문자), 숫자\n특수문자를 포함한 6~15자를 입력
+ 비밀번호를 설정해주세요
+ 비밀번호 주의사항을 확인해주세요
+ 비밀번호를 다시 확인해주세요
+ 개인정보 수집 및 이용 안내
+ 개인정보 수집 및 이용에 동의합니다 (필수)
+ 1. 수집 항목
+ 이름, 이메일 주소, 비밀번호
+ 2. 수집 목적
+ 회원 식별 및 관리
+ 서비스 제공 및 공지사항 전달
+ 3. 보유 및 이용 기간
+ 회원 탈퇴 시까지
+ 4.이용약관 안내
+ 서버 점검이나 업데이트 시\n 서비스가 일시 중단될 수 있음
+ 이용자는 개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있으\n며, 동의를 거부할 경우 회원가입이 제한될 수 있습니다.
+ 가입 완료!
+ %1$s 님, 환영해요.\n이제 Book - on에서 마음껏 읽어보세요.
+ 보유 도서
+ 인증번호 입력
+ %1$s 으로 보낸\n6자리 코드를 입력해 주세요
+ %1$s 후 만료
+ 코드를 받지 못하셨나요? 재전송
+
+
+ 독서마라톤
+ 계정연동
+ 독서마라톤 계정을 연동하면\n읽은 책이 자동으로 기록돼요
+ 독서마라톤 아이디와 비밀번호를\n입력해 주세요
+ 독서마라톤 이용하기
+ 교내 독서마라톤에 참여 중이라면 연동\n하세요
+ 연동하면 홈 · 마이페이지에서 진척도와 랭킹이 자동으로\n표시돼요.
+ 지금 연동하지 않아도 괜찮아요.\n마이페이지에서 언제든 다시 연동할 수 있어요!
+ 독서마라톤 아이디
+ 독서마라톤 개인정보 제공 동의 체크
+ 나중에 할게요 · 건너뛰기
+ 연동하고 가입완료
+
+
+ 좋은 저녁이에요
+ %1$s님
+ 도서 찾기
+ 책 제목, 저자, 도서관 번호
+ 도서 찾기
+ 도서 검색
+ 알림
+ 프로필
+ 도서부 공지
+ NEW
+ %1$s · 도서부
+ 여름방학 도서 대출 기간 연장 안내
+ 방학 기간 동안 1인당 최대 5권, 대출 기간이 14일로 연장됩니다.\n반납은 개학일 전까지 완료해 주세요.
+ AI 추천
+ AI
+ %1$s님의 대출 이력을 분석해 골랐어요
+ 우리 학교 인기 책
+ 최근 새로 들어온 도서
+
+
+ 홈
+ 랭킹
+ 도서실
+ 마이
+
+
+ 도서관 번호
+ 재고 수량
+ 대출 여부
+ 책 소개
+ 대출 신청하기
+ 대출 불가
+ %1$s 검색 결과 %2$d건 · 제목 · 도서관 번호
+ 검색된 책이 없어요\n다른 검색어를 입력해보세요
+
+
+ 전체
+ 다독왕 랭킹
+ 2026년 · 대출 권수 기준 · 매년 1월 1일 초기화
+ %1$d권
+ %1$d 권
+ %1$d학년 · %2$s
+
+
+ 인기순
+ 신간순
+ 전체
+ 소설
+ 과학
+ 역사
+ 개발
+
+
+ 내 서재
+ %1$s 님
+ %1$d기 · %2$s
+ 대출 중
+ 반납 임박
+ 누적 대출
+ %1$d권
+ 2026 독서마라톤
+ %1$s · %2$d / %3$d권
+ %1$d%%
+ 아직 연동하지 않았어요
+ 완주까지 %1$d권 남았어요 · 상위 %2$d%%
+ 토글을 켜면 독서마라톤 계정을 연동할 수 있어요
+ 비밀번호 변경
+ 대출 / 반납 내역
+ 즐겨찾기 목록
+ 알림 설정
+ 이용 안내
+ 받고 싶은 알림을 선택하세요
+ 도서부 공지 알림
+ 새 공지가 올라오면 알려드려요
+ 대출 중 %1$d
+ 반납 완료
+ 반납 알림
+ 반납 3일 전과 당일에 알려드려요
+ D - %1$d
+ %1$s · 반납 %2$s
+ 지난 대출
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..8b2a0b5
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/example/bookon/ExampleUnitTest.kt b/app/src/test/java/com/example/bookon/ExampleUnitTest.kt
new file mode 100644
index 0000000..70d454e
--- /dev/null
+++ b/app/src/test/java/com/example/bookon/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.bookon
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..18318be
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,5 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..34c5e9e
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,15 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
\ No newline at end of file
diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties
new file mode 100644
index 0000000..6c1139e
--- /dev/null
+++ b/gradle/gradle-daemon-jvm.properties
@@ -0,0 +1,12 @@
+#This file is generated by updateDaemonJvm
+toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
+toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
+toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
+toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
+toolchainVersion=21
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..394ece7
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,31 @@
+[versions]
+agp = "9.2.1"
+coreKtx = "1.10.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+lifecycleRuntimeKtx = "2.6.1"
+activityCompose = "1.8.0"
+kotlin = "2.2.10"
+composeBom = "2026.02.01"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8bdaf60
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..16ea20a
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,9 @@
+#Tue Jul 07 19:37:36 KST 2026
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..ef07e01
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..db3a6ac
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..e7208fc
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,27 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "BookOn"
+include(":app")
+
\ No newline at end of file