diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4138d5ba..f7e77992 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -40,4 +40,5 @@ dependencies {
implementation(libs.kakao.user)
implementation(projects.presentation)
-}
\ No newline at end of file
+ implementation(projects.core.data)
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e76a7103..3c408875 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,6 +11,7 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Piece"
@@ -32,10 +33,12 @@
android:exported="true">
+
-
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 00000000..85e7ad8e
--- /dev/null
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build-logic/src/main/java/com/puzzle/build/logic/TestAndroid.kt b/build-logic/src/main/java/com/puzzle/build/logic/TestAndroid.kt
index 58291218..43440561 100644
--- a/build-logic/src/main/java/com/puzzle/build/logic/TestAndroid.kt
+++ b/build-logic/src/main/java/com/puzzle/build/logic/TestAndroid.kt
@@ -15,6 +15,10 @@ internal fun Project.configureJUnitAndroid() {
unitTests.all { it.useJUnitPlatform() }
}
+ defaultConfig {
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
val libs = extensions.libs
dependencies {
"androidTestImplementation"(libs.findLibrary("androidx.test.ext").get())
diff --git a/core/common/.gitignore b/core/common/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/core/common/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts
new file mode 100644
index 00000000..492e1cf0
--- /dev/null
+++ b/core/common/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ id("piece.kotlin.library")
+ id("piece.kotlin.hilt")
+}
+
+dependencies {
+ implementation(libs.coroutines.core)
+}
diff --git a/core/common/src/main/java/com/puzzle/common/TimeUtil.kt b/core/common/src/main/java/com/puzzle/common/TimeUtil.kt
new file mode 100644
index 00000000..2df910d2
--- /dev/null
+++ b/core/common/src/main/java/com/puzzle/common/TimeUtil.kt
@@ -0,0 +1,18 @@
+package com.puzzle.common
+
+import java.time.LocalDateTime
+import java.time.format.DateTimeParseException
+
+/**
+ * String?을 LocalDateTime으로 변환합니다.
+ *
+ * - 문자열이 null인 경우, [LocalDateTime.MIN]을 반환합니다.
+ * - 잘못된 형식으로 인해 파싱에 실패할 경우, [LocalDateTime.MIN]을 반환합니다.
+ */
+fun String?.parseDateTime(): LocalDateTime {
+ return try {
+ this?.let { LocalDateTime.parse(it) } ?: LocalDateTime.MIN
+ } catch (e: DateTimeParseException) {
+ LocalDateTime.MIN
+ }
+}
diff --git a/core/common/src/test/kotlin/com/puzzle/common/TimeUtilTest.kt b/core/common/src/test/kotlin/com/puzzle/common/TimeUtilTest.kt
new file mode 100644
index 00000000..c7a765f4
--- /dev/null
+++ b/core/common/src/test/kotlin/com/puzzle/common/TimeUtilTest.kt
@@ -0,0 +1,47 @@
+package com.puzzle.common
+
+import org.junit.Assert.assertEquals
+import org.junit.jupiter.api.Test
+import java.time.LocalDateTime
+
+class TimeUtilTest {
+
+ @Test
+ fun `올바른 형식의 문자열을 LocalDateTime으로 변환할 수 있다`() {
+ // given
+ val dateTimeString = "2024-06-01T00:00:00"
+ val expected = LocalDateTime.parse(dateTimeString)
+
+ // when
+ val actual = dateTimeString.parseDateTime()
+
+ // then
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `null 값을 파싱하려고 할 경우 LocalDateTime_MIN을 반환한다`() {
+ // given
+ val nullString: String? = null
+ val expected = LocalDateTime.MIN
+
+ // when
+ val actual = nullString.parseDateTime()
+
+ // then
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `형식에 맞지 않은 문자열을 파싱하려고 할 경우 LocalDateTime_MIN을 반환한다`() {
+ // given
+ val invalidString = "invalid-date-format"
+ val expected = LocalDateTime.MIN
+
+ // when
+ val actual = invalidString.parseDateTime()
+
+ // then
+ assertEquals(expected, actual)
+ }
+}
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
index f299343f..e903b3cc 100644
--- a/core/data/build.gradle.kts
+++ b/core/data/build.gradle.kts
@@ -10,4 +10,5 @@ android {
dependencies {
implementation(projects.core.domain)
implementation(projects.core.network)
-}
\ No newline at end of file
+ implementation(projects.core.database)
+}
diff --git a/core/data/src/androidTest/java/com/puzzle/data/ExampleInstrumentedTest.kt b/core/data/src/androidTest/java/com/puzzle/data/ExampleInstrumentedTest.kt
deleted file mode 100644
index 60180280..00000000
--- a/core/data/src/androidTest/java/com/puzzle/data/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.puzzle.data
-
-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.puzzle.data.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/core/data/src/main/java/com/puzzle/data/di/DataModule.kt b/core/data/src/main/java/com/puzzle/data/di/DataModule.kt
index c52cbaff..bba9932e 100644
--- a/core/data/src/main/java/com/puzzle/data/di/DataModule.kt
+++ b/core/data/src/main/java/com/puzzle/data/di/DataModule.kt
@@ -1,7 +1,9 @@
package com.puzzle.data.di
import com.puzzle.data.repository.AuthRepositoryImpl
+import com.puzzle.data.repository.TermsRepositoryImpl
import com.puzzle.domain.repository.AuthRepository
+import com.puzzle.domain.repository.TermsRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -18,4 +20,10 @@ abstract class DataModule {
abstract fun bindsAuthRepository(
authRepositoryImpl: AuthRepositoryImpl,
): AuthRepository
-}
\ No newline at end of file
+
+ @Binds
+ @Singleton
+ abstract fun bindsTermsRepository(
+ termsRepositoryImpl: TermsRepositoryImpl,
+ ): TermsRepository
+}
diff --git a/core/data/src/main/java/com/puzzle/data/repository/TermsRepositoryImpl.kt b/core/data/src/main/java/com/puzzle/data/repository/TermsRepositoryImpl.kt
new file mode 100644
index 00000000..225a74ed
--- /dev/null
+++ b/core/data/src/main/java/com/puzzle/data/repository/TermsRepositoryImpl.kt
@@ -0,0 +1,38 @@
+package com.puzzle.data.repository
+
+import com.puzzle.database.model.terms.TermEntity
+import com.puzzle.database.source.term.LocalTermDataSource
+import com.puzzle.domain.model.terms.Term
+import com.puzzle.domain.repository.TermsRepository
+import com.puzzle.network.model.UNKNOWN_INT
+import com.puzzle.network.source.TermDataSource
+import javax.inject.Inject
+
+class TermsRepositoryImpl @Inject constructor(
+ private val termDataSource: TermDataSource,
+ private val localTermDataSource: LocalTermDataSource,
+) : TermsRepository {
+ override suspend fun loadTerms(): Result = runCatching {
+ val terms = termDataSource.loadTerms()
+ .getOrThrow()
+ .toDomain()
+ .filter { it.termId != UNKNOWN_INT }
+
+ val termsEntity = terms.map {
+ TermEntity(
+ id = it.termId,
+ title = it.title,
+ content = it.content,
+ required = it.required,
+ startDate = it.startDate,
+ )
+ }
+
+ localTermDataSource.clearAndInsertTerms(termsEntity)
+ }
+
+ override suspend fun getTerms(): Result> = runCatching {
+ localTermDataSource.getTerms()
+ .map { it.toDomain() }
+ }
+}
diff --git a/core/data/src/test/java/com/puzzle/data/repository/TermsRepositoryImplTest.kt b/core/data/src/test/java/com/puzzle/data/repository/TermsRepositoryImplTest.kt
new file mode 100644
index 00000000..35d9df38
--- /dev/null
+++ b/core/data/src/test/java/com/puzzle/data/repository/TermsRepositoryImplTest.kt
@@ -0,0 +1,106 @@
+package com.puzzle.data.repository
+
+import com.puzzle.database.source.term.LocalTermDataSource
+import com.puzzle.network.model.UNKNOWN_INT
+import com.puzzle.network.model.terms.LoadTermsResponse
+import com.puzzle.network.model.terms.TermResponse
+import com.puzzle.network.source.TermDataSource
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.just
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class TermsRepositoryImplTest {
+
+ private lateinit var termDataSource: TermDataSource
+ private lateinit var localTermDataSource: LocalTermDataSource
+ private lateinit var termsRepository: TermsRepositoryImpl
+
+ @BeforeEach
+ fun setUp() {
+ termDataSource = mockk()
+ localTermDataSource = mockk()
+ termsRepository = TermsRepositoryImpl(termDataSource, localTermDataSource)
+ }
+
+ @Test
+ fun `약관을 새로 갱신할 경우 id값이 올바르게 내려오지 않은 약관은 무시한다`() = runTest {
+ // given
+ val invalidTerm = TermResponse(
+ termId = UNKNOWN_INT,
+ title = "Invalid",
+ content = "Invalid Content",
+ required = false,
+ startDate = "2024-06-01T00:00:00",
+ )
+ val validTerm = TermResponse(
+ termId = 1,
+ title = "Valid",
+ content = "Valid Content",
+ required = true,
+ startDate = "2024-06-01T00:00:00",
+ )
+
+ coEvery { termDataSource.loadTerms() } returns
+ Result.success(LoadTermsResponse(listOf(invalidTerm, validTerm)))
+ coEvery { localTermDataSource.clearAndInsertTerms(any()) } just Runs
+
+ // when
+ val result = termsRepository.loadTerms()
+
+ // then
+ assertTrue(result.isSuccess)
+ coVerify(exactly = 1) {
+ localTermDataSource.clearAndInsertTerms(
+ match {
+ it.size == 1 && it.first().id == validTerm.termId
+ }
+ )
+ }
+ }
+
+ @Test
+ fun `갱신한 데이터는 로컬 데이터베이스에 저장한다`() = runTest {
+ // given
+ val validTerms = listOf(
+ TermResponse(
+ termId = 1,
+ title = "Valid1",
+ content = "Content1",
+ required = true,
+ startDate = "2024-06-01T00:00:00"
+ ),
+ TermResponse(
+ termId = 2,
+ title = "Valid2",
+ content = "Content2",
+ required = false,
+ startDate = "2024-06-01T00:00:00"
+ )
+ )
+
+ coEvery { termDataSource.loadTerms() } returns Result.success(LoadTermsResponse(validTerms))
+ coEvery { localTermDataSource.clearAndInsertTerms(any()) } just Runs
+
+ // when
+ termsRepository.loadTerms()
+
+ // then
+ coVerify(exactly = 1) {
+ localTermDataSource.clearAndInsertTerms(
+ match {
+ it.size == validTerms.size && it.all { entity ->
+ validTerms.any { term ->
+ term.termId == entity.id && term.title == entity.title
+ }
+ }
+ }
+ )
+ }
+ }
+}
diff --git a/core/database/.gitignore b/core/database/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/core/database/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts
new file mode 100644
index 00000000..6a86ca5f
--- /dev/null
+++ b/core/database/build.gradle.kts
@@ -0,0 +1,16 @@
+plugins {
+ id("piece.android.library")
+ id("piece.android.hilt")
+}
+
+android {
+ namespace = "com.puzzle.database"
+}
+
+dependencies {
+ implementation(projects.core.domain)
+ implementation(projects.core.common)
+
+ implementation(libs.androidx.room.ktx)
+ ksp(libs.androidx.room.compiler)
+}
diff --git a/core/database/consumer-rules.pro b/core/database/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/core/database/proguard-rules.pro b/core/database/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/core/database/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/core/database/src/androidTest/java/com/puzzle/database/dao/TermsDaoTest.kt b/core/database/src/androidTest/java/com/puzzle/database/dao/TermsDaoTest.kt
new file mode 100644
index 00000000..260fb6df
--- /dev/null
+++ b/core/database/src/androidTest/java/com/puzzle/database/dao/TermsDaoTest.kt
@@ -0,0 +1,89 @@
+package com.puzzle.database.dao
+
+import android.content.Context
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.puzzle.common.parseDateTime
+import com.puzzle.database.PieceDatabase
+import com.puzzle.database.model.terms.TermEntity
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TermsDaoTest {
+ private lateinit var termsDao: TermsDao
+ private lateinit var db: PieceDatabase
+
+ @Before
+ fun setUp() {
+ val context = ApplicationProvider.getApplicationContext()
+ db = Room.inMemoryDatabaseBuilder(
+ context = context,
+ klass = PieceDatabase::class.java
+ ).build()
+ termsDao = db.termsDao()
+ }
+
+ @After
+ fun tearDown() {
+ db.close()
+ }
+
+ @Test
+ fun 약관을_삽입하고_조회할_수_있다() = runTest {
+ // given
+ val expected = listOf(
+ TermEntity(1, "이용약관", "내용1", true, "2024-06-01T00:00:00".parseDateTime()),
+ TermEntity(2, "개인정보처리방침", "내용2", false, "2024-06-01T00:00:00".parseDateTime())
+ )
+
+ // when
+ termsDao.insertTerms(*expected.toTypedArray())
+ val actual = termsDao.getTerms()
+
+ // then
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun 약관을_모두_삭제할_수_있다() = runTest {
+ // given
+ val terms = listOf(
+ TermEntity(1, "이용약관", "내용1", true, "2024-06-01T00:00:00".parseDateTime()),
+ TermEntity(2, "개인정보처리방침", "내용2", false, "2024-06-01T00:00:00".parseDateTime())
+ )
+ termsDao.insertTerms(*terms.toTypedArray())
+
+ // when
+ termsDao.clearTerms()
+ val actual = termsDao.getTerms()
+
+ // then
+ val expected = emptyList()
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun 이전_약관을_삭제하고_새로운_약관을_삽입할_수_있다() = runTest {
+ // given
+ val oldTerms = listOf(
+ TermEntity(1, "이전 약관", "이전 내용", true, "2024-06-01T00:00:00".parseDateTime())
+ )
+ termsDao.insertTerms(*oldTerms.toTypedArray())
+
+ // when
+ val expected = listOf(
+ TermEntity(2, "새로운 약관", "새로운 내용", false, "2024-06-01T00:00:00".parseDateTime())
+ )
+ termsDao.clearAndInsertTerms(*expected.toTypedArray())
+ val actual = termsDao.getTerms()
+
+ // then
+ assertEquals(expected, actual)
+ }
+}
diff --git a/core/database/src/main/AndroidManifest.xml b/core/database/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/core/database/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/database/src/main/java/com/puzzle/database/PieceDatabase.kt b/core/database/src/main/java/com/puzzle/database/PieceDatabase.kt
new file mode 100644
index 00000000..52685244
--- /dev/null
+++ b/core/database/src/main/java/com/puzzle/database/PieceDatabase.kt
@@ -0,0 +1,21 @@
+package com.puzzle.database
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import com.puzzle.database.converter.PieceConverters
+import com.puzzle.database.dao.TermsDao
+import com.puzzle.database.model.terms.TermEntity
+
+@Database(
+ entities = [TermEntity::class],
+ version = 1,
+)
+@TypeConverters(PieceConverters::class)
+internal abstract class PieceDatabase : RoomDatabase() {
+ abstract fun termsDao(): TermsDao
+
+ companion object {
+ internal const val NAME = "piece-database"
+ }
+}
diff --git a/core/database/src/main/java/com/puzzle/database/converter/PieceConverters.kt b/core/database/src/main/java/com/puzzle/database/converter/PieceConverters.kt
new file mode 100644
index 00000000..5cb2f1df
--- /dev/null
+++ b/core/database/src/main/java/com/puzzle/database/converter/PieceConverters.kt
@@ -0,0 +1,17 @@
+package com.puzzle.database.converter
+
+import androidx.room.TypeConverter
+import com.puzzle.common.parseDateTime
+import java.time.LocalDateTime
+
+class PieceConverters {
+ @TypeConverter
+ fun fromLocalDate(date: LocalDateTime?): String? {
+ return date?.toString()
+ }
+
+ @TypeConverter
+ fun toLocalDate(dateString: String?): LocalDateTime? {
+ return dateString?.parseDateTime()
+ }
+}
diff --git a/core/database/src/main/java/com/puzzle/database/dao/TermsDao.kt b/core/database/src/main/java/com/puzzle/database/dao/TermsDao.kt
new file mode 100644
index 00000000..abed2014
--- /dev/null
+++ b/core/database/src/main/java/com/puzzle/database/dao/TermsDao.kt
@@ -0,0 +1,26 @@
+package com.puzzle.database.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+import com.puzzle.database.model.terms.TermEntity
+
+@Dao
+interface TermsDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertTerms(vararg terms: TermEntity)
+
+ @Query(value = "SELECT * FROM term")
+ suspend fun getTerms(): List
+
+ @Query(value = "DELETE FROM term")
+ suspend fun clearTerms()
+
+ @Transaction
+ suspend fun clearAndInsertTerms(vararg terms: TermEntity) {
+ clearTerms()
+ insertTerms(*terms)
+ }
+}
diff --git a/core/database/src/main/java/com/puzzle/database/di/DaosModule.kt b/core/database/src/main/java/com/puzzle/database/di/DaosModule.kt
new file mode 100644
index 00000000..53c31113
--- /dev/null
+++ b/core/database/src/main/java/com/puzzle/database/di/DaosModule.kt
@@ -0,0 +1,17 @@
+package com.puzzle.database.di
+
+import com.puzzle.database.PieceDatabase
+import com.puzzle.database.dao.TermsDao
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal object DaosModule {
+ @Provides
+ fun providesTermsDao(
+ database: PieceDatabase,
+ ): TermsDao = database.termsDao()
+}
diff --git a/core/database/src/main/java/com/puzzle/database/di/DatabaseModule.kt b/core/database/src/main/java/com/puzzle/database/di/DatabaseModule.kt
new file mode 100644
index 00000000..457905c2
--- /dev/null
+++ b/core/database/src/main/java/com/puzzle/database/di/DatabaseModule.kt
@@ -0,0 +1,25 @@
+package com.puzzle.database.di
+
+import android.content.Context
+import androidx.room.Room
+import com.puzzle.database.PieceDatabase
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal object DatabaseModule {
+ @Provides
+ @Singleton
+ fun providesPieceDatabase(
+ @ApplicationContext context: Context,
+ ): PieceDatabase = Room.databaseBuilder(
+ context,
+ PieceDatabase::class.java,
+ PieceDatabase.NAME,
+ ).build()
+}
diff --git a/core/database/src/main/java/com/puzzle/database/model/terms/TermEntity.kt b/core/database/src/main/java/com/puzzle/database/model/terms/TermEntity.kt
new file mode 100644
index 00000000..89182334
--- /dev/null
+++ b/core/database/src/main/java/com/puzzle/database/model/terms/TermEntity.kt
@@ -0,0 +1,25 @@
+package com.puzzle.database.model.terms
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.puzzle.domain.model.terms.Term
+import java.time.LocalDateTime
+
+@Entity(tableName = "term")
+data class TermEntity(
+ @PrimaryKey
+ @ColumnInfo(name = "id") val id: Int,
+ val title: String,
+ val content: String,
+ val required: Boolean,
+ @ColumnInfo(name = "start_date") val startDate: LocalDateTime,
+) {
+ fun toDomain() = Term(
+ termId = id,
+ title = title,
+ content = content,
+ required = required,
+ startDate = startDate,
+ )
+}
diff --git a/core/database/src/main/java/com/puzzle/database/source/term/LocalTermDataSource.kt b/core/database/src/main/java/com/puzzle/database/source/term/LocalTermDataSource.kt
new file mode 100644
index 00000000..3c9da99e
--- /dev/null
+++ b/core/database/src/main/java/com/puzzle/database/source/term/LocalTermDataSource.kt
@@ -0,0 +1,15 @@
+package com.puzzle.database.source.term
+
+import com.puzzle.database.dao.TermsDao
+import com.puzzle.database.model.terms.TermEntity
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class LocalTermDataSource @Inject constructor(
+ private val termsDao: TermsDao,
+) {
+ suspend fun getTerms() = termsDao.getTerms()
+ suspend fun clearAndInsertTerms(terms: List) =
+ termsDao.clearAndInsertTerms(*terms.toTypedArray())
+}
diff --git a/core/data/src/test/java/com/puzzle/data/ExampleUnitTest.kt b/core/database/src/test/java/com/puzzle/database/ExampleUnitTest.kt
similarity index 91%
rename from core/data/src/test/java/com/puzzle/data/ExampleUnitTest.kt
rename to core/database/src/test/java/com/puzzle/database/ExampleUnitTest.kt
index bd567504..b15a51f3 100644
--- a/core/data/src/test/java/com/puzzle/data/ExampleUnitTest.kt
+++ b/core/database/src/test/java/com/puzzle/database/ExampleUnitTest.kt
@@ -1,4 +1,4 @@
-package com.puzzle.data
+package com.puzzle.database
import org.junit.Test
diff --git a/core/designsystem/src/main/java/com/puzzle/designsystem/component/Check.kt b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Check.kt
new file mode 100644
index 00000000..aff087b5
--- /dev/null
+++ b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Check.kt
@@ -0,0 +1,138 @@
+package com.puzzle.designsystem.component
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+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.padding
+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.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.puzzle.designsystem.R
+import com.puzzle.designsystem.foundation.PieceTheme
+
+@Composable
+fun PieceCheck(
+ checked: Boolean,
+ onCheckedChange: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Image(
+ painter = painterResource(R.drawable.ic_check),
+ contentDescription = "",
+ colorFilter = ColorFilter.tint(
+ color = if (checked) PieceTheme.colors.primaryDefault
+ else PieceTheme.colors.light1,
+ ),
+ modifier = modifier
+ .size(20.dp)
+ .clickable { onCheckedChange() },
+ )
+}
+
+@Composable
+fun PieceCheckList(
+ checked: Boolean,
+ label: String,
+ onCheckedChange: () -> Unit,
+ modifier: Modifier = Modifier,
+ arrowEnabled: Boolean = false,
+ containerColor: Color = PieceTheme.colors.white,
+ onArrowClick: () -> Unit = {},
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier
+ .height(52.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .background(containerColor),
+ ) {
+ PieceCheck(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = Modifier.padding(start = 14.dp, end = 12.dp),
+ )
+
+ Text(
+ text = label,
+ style = PieceTheme.typography.bodyMR,
+ color = PieceTheme.colors.black,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ modifier = Modifier.weight(1f),
+ )
+
+ if (arrowEnabled) {
+ Image(
+ painter = painterResource(R.drawable.ic_arrow_right),
+ contentDescription = "",
+ modifier = Modifier
+ .padding(end = 14.dp)
+ .size(24.dp)
+ .clickable { onArrowClick() },
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewPieceCheck() {
+ PieceTheme {
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ PieceCheck(
+ checked = true,
+ onCheckedChange = {},
+ )
+
+ PieceCheck(
+ checked = false,
+ onCheckedChange = {},
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewPieceCheckList() {
+ PieceTheme {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ modifier = Modifier
+ .background(PieceTheme.colors.black)
+ .padding(20.dp),
+ ) {
+ PieceCheckList(
+ checked = true,
+ label = "약관 전체 동의",
+ onCheckedChange = {},
+ containerColor = PieceTheme.colors.light3,
+ modifier = Modifier.fillMaxWidth(),
+ )
+
+ PieceCheckList(
+ checked = false,
+ arrowEnabled = true,
+ label = "약관 전체 동의",
+ onCheckedChange = {},
+ onArrowClick = {},
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ }
+}
diff --git a/core/designsystem/src/main/java/com/puzzle/designsystem/component/TopBar.kt b/core/designsystem/src/main/java/com/puzzle/designsystem/component/TopBar.kt
index 6588ede2..5305d2cf 100644
--- a/core/designsystem/src/main/java/com/puzzle/designsystem/component/TopBar.kt
+++ b/core/designsystem/src/main/java/com/puzzle/designsystem/component/TopBar.kt
@@ -102,7 +102,7 @@ fun PieceSubCloseTopBar(
title: String,
onCloseClick: () -> Unit,
modifier: Modifier = Modifier,
- showCloseButton: Boolean = true,
+ closeButtonEnabled: Boolean = true,
contentColor: Color = PieceTheme.colors.black,
) {
Box(modifier = modifier.fillMaxWidth()) {
@@ -113,7 +113,7 @@ fun PieceSubCloseTopBar(
modifier = Modifier.align(Alignment.Center),
)
- if (showCloseButton) {
+ if (closeButtonEnabled) {
Image(
painter = painterResource(R.drawable.ic_close),
contentDescription = "닫기 버튼",
diff --git a/core/designsystem/src/main/java/com/puzzle/designsystem/foundation/Typography.kt b/core/designsystem/src/main/java/com/puzzle/designsystem/foundation/Typography.kt
index 2fd62025..ff976358 100644
--- a/core/designsystem/src/main/java/com/puzzle/designsystem/foundation/Typography.kt
+++ b/core/designsystem/src/main/java/com/puzzle/designsystem/foundation/Typography.kt
@@ -29,6 +29,13 @@ private val PretendardMedium = FontFamily(
),
)
+private val PretendardRegular = FontFamily(
+ Font(
+ resId = R.font.pretendard_medium,
+ weight = FontWeight.Normal,
+ ),
+)
+
@Immutable
data class PieceTypography(
val headingXLSB: TextStyle = TextStyle(
@@ -67,7 +74,7 @@ data class PieceTypography(
lineHeight = 24.sp,
),
val bodyMR: TextStyle = TextStyle(
- fontFamily = PretendardMedium,
+ fontFamily = PretendardRegular,
fontSize = 16.sp,
lineHeight = 24.sp,
),
diff --git a/core/designsystem/src/main/res/drawable/ic_arrow_right.xml b/core/designsystem/src/main/res/drawable/ic_arrow_right.xml
new file mode 100644
index 00000000..c82291c8
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_arrow_right.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_check.xml b/core/designsystem/src/main/res/drawable/ic_check.xml
new file mode 100644
index 00000000..29c39a1d
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_check.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/core/domain/src/main/java/com/puzzle/domain/model/terms/Term.kt b/core/domain/src/main/java/com/puzzle/domain/model/terms/Term.kt
new file mode 100644
index 00000000..e0896524
--- /dev/null
+++ b/core/domain/src/main/java/com/puzzle/domain/model/terms/Term.kt
@@ -0,0 +1,11 @@
+package com.puzzle.domain.model.terms
+
+import java.time.LocalDateTime
+
+data class Term(
+ val termId: Int,
+ val title: String,
+ val content: String,
+ val required: Boolean,
+ val startDate: LocalDateTime,
+)
diff --git a/core/domain/src/main/java/com/puzzle/domain/repository/TermsRepository.kt b/core/domain/src/main/java/com/puzzle/domain/repository/TermsRepository.kt
new file mode 100644
index 00000000..4649741e
--- /dev/null
+++ b/core/domain/src/main/java/com/puzzle/domain/repository/TermsRepository.kt
@@ -0,0 +1,8 @@
+package com.puzzle.domain.repository
+
+import com.puzzle.domain.model.terms.Term
+
+interface TermsRepository {
+ suspend fun loadTerms(): Result
+ suspend fun getTerms(): Result>
+}
diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts
index 5955643c..01868a59 100644
--- a/core/network/build.gradle.kts
+++ b/core/network/build.gradle.kts
@@ -1,3 +1,5 @@
+import java.util.Properties
+
plugins {
id("piece.android.library")
id("piece.android.hilt")
@@ -6,19 +8,22 @@ plugins {
android {
namespace = "com.puzzle.network"
+ val localProperties = Properties()
+ localProperties.load(project.rootProject.file("local.properties").bufferedReader())
+
buildTypes {
debug {
buildConfigField(
"String",
"PIECE_BASE_URL",
- "\"${properties["PIECE_DEV_BASE_URL"]}\"",
+ "\"${localProperties["PIECE_DEV_BASE_URL"]}\"",
)
}
release {
buildConfigField(
"String",
"PIECE_BASE_URL",
- "\"${properties["PIECE_PROD_BASE_URL"]}\"",
+ "\"${localProperties["PIECE_PROD_BASE_URL"]}\"",
)
}
}
@@ -30,6 +35,7 @@ android {
dependencies {
implementation(projects.core.domain)
+ implementation(projects.core.common)
implementation(libs.retrofit.core)
implementation(libs.retrofit.kotlin.serialization)
diff --git a/core/network/src/main/java/com/puzzle/network/adapter/PieceCallAdapter.kt b/core/network/src/main/java/com/puzzle/network/adapter/PieceCallAdapter.kt
index 65b8c290..2d42936b 100644
--- a/core/network/src/main/java/com/puzzle/network/adapter/PieceCallAdapter.kt
+++ b/core/network/src/main/java/com/puzzle/network/adapter/PieceCallAdapter.kt
@@ -18,7 +18,7 @@ import javax.inject.Singleton
class PieceCallAdapterFactory @Inject constructor() : CallAdapter.Factory() {
override fun get(
type: Type,
- annotations: Array,
+ annotations: Array,
retrofit: Retrofit
): CallAdapter<*, *>? {
// 반환 타입의 최상위 객체가 Result 객체인지 확인, 아닐 경우 null 반환
@@ -68,13 +68,13 @@ private class PieceCall(
return Result.failure(
HttpResponseException(
status = HttpResponseStatus.create(code()),
- msg = "", // Todo
+ msg = "일시적인 서버 에러입니다. 계속해서 반복될 경우 문의 하기를 이용해주세요.",
)
)
} ?: return Result.failure(
HttpResponseException(
status = HttpResponseStatus.create(-1),
- msg = "알 수 없는 에러입니다."
+ msg = "일시적인 서버 에러입니다. 계속해서 반복될 경우 문의 하기를 이용해주세요."
)
)
}
@@ -91,4 +91,4 @@ private class PieceCall(
override fun request(): Request = delegate.request()
override fun timeout(): Timeout = delegate.timeout()
override fun cancel() = delegate.cancel()
-}
\ No newline at end of file
+}
diff --git a/core/network/src/main/java/com/puzzle/network/api/PieceApi.kt b/core/network/src/main/java/com/puzzle/network/api/PieceApi.kt
index 691c4f21..58ff540b 100644
--- a/core/network/src/main/java/com/puzzle/network/api/PieceApi.kt
+++ b/core/network/src/main/java/com/puzzle/network/api/PieceApi.kt
@@ -1,21 +1,26 @@
package com.puzzle.network.api
+import com.puzzle.network.model.ApiResponse
import com.puzzle.network.model.auth.LoginOauthRequest
import com.puzzle.network.model.auth.LoginOauthResponse
import com.puzzle.network.model.auth.RequestAuthCodeRequest
import com.puzzle.network.model.auth.VerifyAuthCodeRequest
import com.puzzle.network.model.auth.VerifyAuthCodeResponse
-import retrofit2.Response
+import com.puzzle.network.model.terms.LoadTermsResponse
import retrofit2.http.Body
+import retrofit2.http.GET
import retrofit2.http.POST
interface PieceApi {
@POST("/api/login/oauth")
- suspend fun loginOauth(@Body loginOauthRequest: LoginOauthRequest): Result
+ suspend fun loginOauth(@Body loginOauthRequest: LoginOauthRequest): Result>
@POST("/api/register/sms/auth/code")
- suspend fun requestAuthCode(@Body requestAuthCodeRequest: RequestAuthCodeRequest): Result
+ suspend fun requestAuthCode(@Body requestAuthCodeRequest: RequestAuthCodeRequest): Result>
@POST("/api/register/sms/auth/code/verify")
- suspend fun verifyAuthCode(@Body verifyAuthCodeRequest: VerifyAuthCodeRequest): Result
+ suspend fun verifyAuthCode(@Body verifyAuthCodeRequest: VerifyAuthCodeRequest): Result>
+
+ @GET("/api/terms")
+ suspend fun loadTerms(): Result>
}
diff --git a/core/network/src/main/java/com/puzzle/network/di/RetrofitModule.kt b/core/network/src/main/java/com/puzzle/network/di/RetrofitModule.kt
index 8833b9f8..e32c9e95 100644
--- a/core/network/src/main/java/com/puzzle/network/di/RetrofitModule.kt
+++ b/core/network/src/main/java/com/puzzle/network/di/RetrofitModule.kt
@@ -19,7 +19,9 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object RetrofitModule {
- private val json = Json {
+ @Singleton
+ @Provides
+ fun provideJson(): Json = Json {
ignoreUnknownKeys = true
}
@@ -39,7 +41,8 @@ object RetrofitModule {
@Singleton
@Provides
- fun providesAuthApi(
+ fun providesPieceApi(
+ json: Json,
okHttpClient: OkHttpClient,
callAdapterFactory: PieceCallAdapterFactory,
): PieceApi = Retrofit.Builder()
diff --git a/core/network/src/main/java/com/puzzle/network/model/ApiResponse.kt b/core/network/src/main/java/com/puzzle/network/model/ApiResponse.kt
new file mode 100644
index 00000000..5654c8e3
--- /dev/null
+++ b/core/network/src/main/java/com/puzzle/network/model/ApiResponse.kt
@@ -0,0 +1,26 @@
+package com.puzzle.network.model
+
+import com.puzzle.domain.model.error.HttpResponseException
+import com.puzzle.domain.model.error.HttpResponseStatus
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ApiResponse(
+ val status: String?,
+ val message: String?,
+ val data: T?,
+)
+
+internal fun Result>.unwrapData(): Result {
+ return this.map { response ->
+ response.data ?: return Result.failure(
+ HttpResponseException(
+ status = HttpResponseStatus.InternalError,
+ msg = "일시적인 서버 에러입니다. 계속해서 반복될 경우 문의 하기를 이용해주세요.",
+ )
+ )
+ }
+}
+
+const val UNKNOWN_INT = -1
+const val UNKNOWN_STRING = "UNKNOWN"
diff --git a/core/network/src/main/java/com/puzzle/network/model/auth/LoginOauthResponse.kt b/core/network/src/main/java/com/puzzle/network/model/auth/LoginOauthResponse.kt
index 90161c89..843d8fb4 100644
--- a/core/network/src/main/java/com/puzzle/network/model/auth/LoginOauthResponse.kt
+++ b/core/network/src/main/java/com/puzzle/network/model/auth/LoginOauthResponse.kt
@@ -4,15 +4,8 @@ import kotlinx.serialization.Serializable
@Serializable
data class LoginOauthResponse(
- val status: String?,
- val message: String?,
- val data: Data?,
-) {
- @Serializable
- data class Data(
- val smsVerified: Boolean?,
- val registerCompleted: Boolean?,
- val accessToken: String?,
- val refreshToken: String?,
- )
-}
+ val smsVerified: Boolean?,
+ val registerCompleted: Boolean?,
+ val accessToken: String?,
+ val refreshToken: String?,
+)
diff --git a/core/network/src/main/java/com/puzzle/network/model/auth/RequestAuthCodeResponse.kt b/core/network/src/main/java/com/puzzle/network/model/auth/RequestAuthCodeResponse.kt
deleted file mode 100644
index c608ef15..00000000
--- a/core/network/src/main/java/com/puzzle/network/model/auth/RequestAuthCodeResponse.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.puzzle.network.model.auth
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class RequestAuthCodeResponse(
- val status: String?,
- val message: String?,
- val data: String?,
-)
diff --git a/core/network/src/main/java/com/puzzle/network/model/auth/VerifyAuthCodeResponse.kt b/core/network/src/main/java/com/puzzle/network/model/auth/VerifyAuthCodeResponse.kt
index 49b3d287..2acf64d6 100644
--- a/core/network/src/main/java/com/puzzle/network/model/auth/VerifyAuthCodeResponse.kt
+++ b/core/network/src/main/java/com/puzzle/network/model/auth/VerifyAuthCodeResponse.kt
@@ -4,15 +4,8 @@ import kotlinx.serialization.Serializable
@Serializable
data class VerifyAuthCodeResponse(
- val status: String?,
- val message: String?,
- val data: Data?,
-) {
- @Serializable
- data class Data(
- val smsVerified: Boolean?,
- val registerCompleted: Boolean?,
- val accessToken: String?,
- val refreshToken: String?,
- )
-}
+ val smsVerified: Boolean?,
+ val registerCompleted: Boolean?,
+ val accessToken: String?,
+ val refreshToken: String?,
+)
diff --git a/core/network/src/main/java/com/puzzle/network/model/terms/LoadTermsResponse.kt b/core/network/src/main/java/com/puzzle/network/model/terms/LoadTermsResponse.kt
new file mode 100644
index 00000000..e2b6cfa4
--- /dev/null
+++ b/core/network/src/main/java/com/puzzle/network/model/terms/LoadTermsResponse.kt
@@ -0,0 +1,32 @@
+package com.puzzle.network.model.terms
+
+import com.puzzle.common.parseDateTime
+import com.puzzle.domain.model.terms.Term
+import com.puzzle.network.model.UNKNOWN_INT
+import com.puzzle.network.model.UNKNOWN_STRING
+import kotlinx.serialization.Serializable
+import java.time.LocalDateTime
+
+@Serializable
+data class LoadTermsResponse(
+ val responses: List?,
+) {
+ fun toDomain() = responses?.map { it.toDomain() } ?: emptyList()
+}
+
+@Serializable
+data class TermResponse(
+ val termId: Int?,
+ val title: String?,
+ val content: String?,
+ val required: Boolean?,
+ val startDate: String?,
+) {
+ fun toDomain(): Term = Term(
+ termId = termId ?: UNKNOWN_INT,
+ title = title ?: UNKNOWN_STRING,
+ content = content ?: UNKNOWN_STRING,
+ required = required ?: false,
+ startDate = startDate?.parseDateTime() ?: LocalDateTime.MIN
+ )
+}
diff --git a/core/network/src/main/java/com/puzzle/network/source/AuthDataSource.kt b/core/network/src/main/java/com/puzzle/network/source/AuthDataSource.kt
index e79faa6b..fb402bc7 100644
--- a/core/network/src/main/java/com/puzzle/network/source/AuthDataSource.kt
+++ b/core/network/src/main/java/com/puzzle/network/source/AuthDataSource.kt
@@ -7,6 +7,7 @@ import com.puzzle.network.model.auth.LoginOauthResponse
import com.puzzle.network.model.auth.RequestAuthCodeRequest
import com.puzzle.network.model.auth.VerifyAuthCodeRequest
import com.puzzle.network.model.auth.VerifyAuthCodeResponse
+import com.puzzle.network.model.unwrapData
import javax.inject.Inject
import javax.inject.Singleton
@@ -20,10 +21,11 @@ class AuthDataSource @Inject constructor(
providerName = provider.apiValue,
token = token,
)
- )
+ ).unwrapData()
- suspend fun requestAuthCode(phoneNumber: String): Result =
+ suspend fun requestAuthCode(phoneNumber: String): Result =
pieceApi.requestAuthCode(RequestAuthCodeRequest(phoneNumber))
+ .unwrapData()
suspend fun verifyAuthCode(phoneNumber: String, code: String): Result =
pieceApi.verifyAuthCode(
@@ -31,5 +33,5 @@ class AuthDataSource @Inject constructor(
phoneNumber = phoneNumber,
code = code,
)
- )
+ ).unwrapData()
}
diff --git a/core/network/src/main/java/com/puzzle/network/source/TermDataSource.kt b/core/network/src/main/java/com/puzzle/network/source/TermDataSource.kt
new file mode 100644
index 00000000..1f9b78ec
--- /dev/null
+++ b/core/network/src/main/java/com/puzzle/network/source/TermDataSource.kt
@@ -0,0 +1,14 @@
+package com.puzzle.network.source
+
+import com.puzzle.network.api.PieceApi
+import com.puzzle.network.model.unwrapData
+import com.puzzle.network.model.terms.LoadTermsResponse
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class TermDataSource @Inject constructor(
+ private val pieceApi: PieceApi,
+) {
+ suspend fun loadTerms(): Result = pieceApi.loadTerms().unwrapData()
+}
diff --git a/feature/auth/src/androidTest/java/com/puzzle/auth/ExampleInstrumentedTest.kt b/feature/auth/src/androidTest/java/com/puzzle/auth/ExampleInstrumentedTest.kt
deleted file mode 100644
index 95428875..00000000
--- a/feature/auth/src/androidTest/java/com/puzzle/auth/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.puzzle.auth
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * 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.auth.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/registration/RegistrationDetailScreen.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/RegistrationDetailScreen.kt
new file mode 100644
index 00000000..408dd8ce
--- /dev/null
+++ b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/RegistrationDetailScreen.kt
@@ -0,0 +1,8 @@
+package com.puzzle.auth.graph.registration
+
+import androidx.compose.runtime.Composable
+
+@Composable
+internal fun RegistrationDetailScreen() {
+
+}
\ No newline at end of file
diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/registration/RegistrationScreen.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/RegistrationScreen.kt
index 6226dfc6..23015919 100644
--- a/feature/auth/src/main/java/com/puzzle/auth/graph/registration/RegistrationScreen.kt
+++ b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/RegistrationScreen.kt
@@ -1,11 +1,29 @@
package com.puzzle.auth.graph.registration
import androidx.compose.foundation.layout.Column
+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.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
+import com.puzzle.auth.graph.registration.contract.RegistrationIntent
+import com.puzzle.auth.graph.registration.contract.RegistrationSideEffect
import com.puzzle.auth.graph.registration.contract.RegistrationState
+import com.puzzle.designsystem.component.PieceCheckList
+import com.puzzle.designsystem.component.PieceSolidButton
+import com.puzzle.designsystem.component.PieceSubBackTopBar
+import com.puzzle.designsystem.foundation.PieceTheme
+import com.puzzle.navigation.NavigationEvent
@Composable
internal fun RegistrationRoute(
@@ -15,14 +33,84 @@ internal fun RegistrationRoute(
RegistrationScreen(
state = state,
+ checkAllTerms = { viewModel.onIntent(RegistrationIntent.CheckAllTerms) },
+ checkTerm = { viewModel.onIntent(RegistrationIntent.CheckTerm(it)) },
+ navigate = { event -> viewModel.onSideEffect(RegistrationSideEffect.Navigate(event)) }
)
}
@Composable
private fun RegistrationScreen(
state: RegistrationState,
+ checkAllTerms: () -> Unit,
+ checkTerm: (Int) -> Unit,
+ navigate: (NavigationEvent) -> Unit,
) {
- Column() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 20.dp),
+ ) {
+ PieceSubBackTopBar(
+ title = "",
+ onBackClick = { navigate(NavigationEvent.NavigateUp) },
+ modifier = Modifier.height(60.dp),
+ )
+ Text(
+ text = buildAnnotatedString {
+ append("Piece의\n")
+
+ withStyle(style = SpanStyle(color = PieceTheme.colors.primaryDefault)) {
+ append("이용약관")
+ }
+
+ append("을 확인해 주세요")
+ },
+ style = PieceTheme.typography.headingLSB,
+ color = PieceTheme.colors.black,
+ modifier = Modifier.padding(top = 20.dp),
+ )
+
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ )
+
+ PieceCheckList(
+ checked = state.allTermsAgreed,
+ label = "약관 전체 동의",
+ containerColor = PieceTheme.colors.light3,
+ onCheckedChange = checkAllTerms,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp),
+ )
+
+ state.terms.forEach { termInfo ->
+ PieceCheckList(
+ checked = state.termsCheckedInfo.getOrDefault(termInfo.termId, false),
+ arrowEnabled = true,
+ label = termInfo.title,
+ onCheckedChange = { checkTerm(termInfo.termId) },
+ onArrowClick = {},
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(2f),
+ )
+
+ PieceSolidButton(
+ label = "다음",
+ onClick = {},
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 10.dp),
+ )
}
-}
\ No newline at end of file
+}
diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/registration/RegistrationViewModel.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/RegistrationViewModel.kt
index 2bae953a..8579cf4b 100644
--- a/feature/auth/src/main/java/com/puzzle/auth/graph/registration/RegistrationViewModel.kt
+++ b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/RegistrationViewModel.kt
@@ -4,17 +4,94 @@ import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.hilt.AssistedViewModelFactory
import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory
+import com.puzzle.auth.graph.registration.contract.RegistrationIntent
+import com.puzzle.auth.graph.registration.contract.RegistrationSideEffect
+import com.puzzle.auth.graph.registration.contract.RegistrationSideEffect.Navigate
import com.puzzle.auth.graph.registration.contract.RegistrationState
+import com.puzzle.domain.model.error.ErrorHelper
+import com.puzzle.domain.repository.TermsRepository
import com.puzzle.navigation.NavigationHelper
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
class RegistrationViewModel @AssistedInject constructor(
@Assisted initialState: RegistrationState,
+ private val termsRepository: TermsRepository,
private val navigationHelper: NavigationHelper,
+ private val errorHelper: ErrorHelper,
) : MavericksViewModel(initialState) {
+ private val intents = Channel(BUFFERED)
+ private val sideEffects = Channel(BUFFERED)
+
+ init {
+ fetchTerms()
+
+ intents.receiveAsFlow()
+ .onEach(::processIntent)
+ .launchIn(viewModelScope)
+
+ sideEffects.receiveAsFlow()
+ .onEach(::handleSideEffect)
+ .launchIn(viewModelScope)
+ }
+
+ internal fun onIntent(intent: RegistrationIntent) = viewModelScope.launch {
+ intents.send(intent)
+ }
+
+ internal fun onSideEffect(sideEffect: RegistrationSideEffect) = viewModelScope.launch {
+ sideEffects.send(sideEffect)
+ }
+
+ private fun processIntent(intent: RegistrationIntent) {
+ when (intent) {
+ is RegistrationIntent.CheckTerm -> checkTerm(intent.termId)
+ is RegistrationIntent.CheckAllTerms -> checkAllTerms()
+ }
+ }
+
+ private fun handleSideEffect(sideEffect: RegistrationSideEffect) {
+ when (sideEffect) {
+ is Navigate -> navigationHelper.navigate(sideEffect.navigationEvent)
+ }
+ }
+
+ private fun fetchTerms() = viewModelScope.launch {
+ termsRepository.getTerms().onSuccess {
+ setState { copy(terms = it) }
+ }.onFailure { errorHelper.sendError(it) }
+ }
+
+ private fun checkTerm(termId: Int) = setState {
+ val updatedTermsCheckedInfo = termsCheckedInfo.toMutableMap().apply {
+ this[termId] = !(this[termId] ?: false)
+ }
+
+ copy(termsCheckedInfo = updatedTermsCheckedInfo)
+ }
+
+ private fun checkAllTerms() = setState {
+ if (allTermsAgreed) {
+ copy(termsCheckedInfo = mutableMapOf())
+ } else {
+ val updatedTermsCheckedInfo = termsCheckedInfo.toMutableMap()
+
+ terms.forEach { termInfo ->
+ updatedTermsCheckedInfo[termInfo.termId] = true
+ }
+
+ copy(termsCheckedInfo = updatedTermsCheckedInfo)
+ }
+ }
+
@AssistedFactory
interface Factory : AssistedViewModelFactory {
override fun create(state: RegistrationState): RegistrationViewModel
diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationIntent.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationIntent.kt
index 72a182d3..bdaf85d0 100644
--- a/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationIntent.kt
+++ b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationIntent.kt
@@ -1,3 +1,6 @@
package com.puzzle.auth.graph.registration.contract
-sealed class RegistrationIntent
\ No newline at end of file
+sealed class RegistrationIntent {
+ data object CheckAllTerms : RegistrationIntent()
+ data class CheckTerm(val termId: Int) : RegistrationIntent()
+}
diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationSideEffect.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationSideEffect.kt
index 286396f9..cad6aa7f 100644
--- a/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationSideEffect.kt
+++ b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationSideEffect.kt
@@ -1,3 +1,7 @@
package com.puzzle.auth.graph.registration.contract
-sealed class RegistrationSideEffect
\ No newline at end of file
+import com.puzzle.navigation.NavigationEvent
+
+sealed class RegistrationSideEffect {
+ data class Navigate(val navigationEvent: NavigationEvent) : RegistrationSideEffect()
+}
diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationState.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationState.kt
index ec85d52d..2dc7c451 100644
--- a/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationState.kt
+++ b/feature/auth/src/main/java/com/puzzle/auth/graph/registration/contract/RegistrationState.kt
@@ -1,7 +1,11 @@
package com.puzzle.auth.graph.registration.contract
import com.airbnb.mvrx.MavericksState
+import com.puzzle.domain.model.terms.Term
data class RegistrationState(
- val a: Boolean = false,
-) : MavericksState
\ No newline at end of file
+ val terms: List = emptyList(),
+ val termsCheckedInfo: MutableMap = mutableMapOf(),
+) : MavericksState {
+ val allTermsAgreed = terms.all { termsCheckedInfo.getOrDefault(it.termId, false) }
+}
diff --git a/feature/auth/src/test/java/com/puzzle/auth/ExampleUnitTest.kt b/feature/auth/src/test/java/com/puzzle/auth/ExampleUnitTest.kt
deleted file mode 100644
index 1214d72a..00000000
--- a/feature/auth/src/test/java/com/puzzle/auth/ExampleUnitTest.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.puzzle.auth
-
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-/**
- * 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/feature/matching/src/androidTest/java/com/puzzle/matching/ExampleInstrumentedTest.kt b/feature/matching/src/androidTest/java/com/puzzle/matching/ExampleInstrumentedTest.kt
deleted file mode 100644
index 431e3e19..00000000
--- a/feature/matching/src/androidTest/java/com/puzzle/matching/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.puzzle.matching
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * 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.matching.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailRoute.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailRoute.kt
index ff849695..5036f469 100644
--- a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailRoute.kt
+++ b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailRoute.kt
@@ -178,7 +178,7 @@ private fun MatchingDetailScreen(
PieceSubCloseTopBar(
title = state.currentPage.title,
onCloseClick = onCloseClick,
- showCloseButton = !(showDialog && dialogType == DialogType.PROFILE_IMAGE_DETAIL),
+ closeButtonEnabled = !(showDialog && dialogType == DialogType.PROFILE_IMAGE_DETAIL),
modifier = Modifier
.fillMaxWidth()
.height(topBarHeight)
diff --git a/feature/mypage/src/androidTest/java/com/puzzle/mypage/ExampleInstrumentedTest.kt b/feature/mypage/src/androidTest/java/com/puzzle/mypage/ExampleInstrumentedTest.kt
deleted file mode 100644
index 5c8e1c43..00000000
--- a/feature/mypage/src/androidTest/java/com/puzzle/mypage/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.puzzle.mypage
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * 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.mypage.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/feature/setting/src/androidTest/java/com/puzzle/setting/ExampleInstrumentedTest.kt b/feature/setting/src/androidTest/java/com/puzzle/setting/ExampleInstrumentedTest.kt
deleted file mode 100644
index 8d6f6fc1..00000000
--- a/feature/setting/src/androidTest/java/com/puzzle/setting/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.puzzle.setting
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * 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.etc.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index aec575af..0c81f9fa 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,7 +6,7 @@
# 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
+org.gradle.jvmargs=-Xmx4096m -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
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5514cf41..e4cd6575 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -19,25 +19,21 @@ androidxNavigation = "2.8.3"
androidxFragment = "1.8.4"
# https://developer.android.com/jetpack/androidx/releases/constraintlayout
androidxConstraintlayout = "2.1.4"
-# https://developer.android.com/jetpack/androidx/releases/core\
+# https://developer.android.com/jetpack/androidx/releases/core
androidxSplashscreen = "1.0.1"
-
-## Compose
+# https://developer.android.com/training/data-storage/room
+androidxRoom = "2.6.1"
# https://developer.android.com/develop/ui/compose/bom/bom-mapping
androidxComposeBom = "2024.12.01"
# https://developer.android.com/jetpack/androidx/releases/navigation
androidxComposeNavigation = "2.8.4"
-## Amplitude
-# https://amplitude.com/docs/sdks/analytics/android
-amplitudeAnalytics = "1.16.8"
-
## Material
material = "1.12.0"
## Kotlin Symbol Processing
# https://github.com/google/ksp/
-ksp = "2.0.0-1.0.22"
+ksp = "2.1.0-1.0.29"
## Hilt
# https://github.com/google/dagger/releases
@@ -55,29 +51,24 @@ retrofit = "2.11.0"
## Kotlin
# https://github.com/JetBrains/kotlin
-kotlin = "2.0.0"
+kotlin = "2.1.0"
# https://github.com/Kotlin/kotlinx.serialization
kotlinxSerializationJson = "1.7.0"
-
-## Coroutine
# https://github.com/Kotlin/kotlinx.coroutines
-coroutine = "1.9.0-RC"
+kotlinxCoroutine = "1.9.0-RC"
## Test
# https://github.com/junit-team/junit4
junit4 = "4.13.2"
junitJupiter = "5.8.2"
-
-# https://developer.android.com/jetpack/androidx/releases/test
-androidxTestRunner = "1.6.2"
-androidxTestExt = "1.2.1"
-androidxEspresso = "3.6.1"
-
# https://mockk.io/
mockk = "1.13.11"
-
# https://github.com/pinterest/ktlint
ktlint = "12.1.1"
+# https://developer.android.com/jetpack/androidx/releases/test
+androidxTestRunner = "1.6.2"
+androidxTestExt = "1.2.1"
+androidxEspresso = "3.6.1"
## firebase
googleServices = "4.4.2"
@@ -87,18 +78,18 @@ messaging = "24.1.0"
# https://coil-kt.github.io/coil/#jetpack-compose
coil = "2.7.0"
-
# https://airbnb.io/lottie/#/android-compose
lottie-compose = "6.5.2"
-
+# https://github.com/skydoves/Cloudy
+cloudy = "0.2.4"
# https://developers.kakao.com/docs/latest/ko/android/getting-started#apply-sdk
kakao = "2.20.6"
-
# https://airbnb.io/mavericks/#/setup
mavericks = "3.0.9"
-# https://github.com/skydoves/Cloudy
-cloudy = "0.2.4"
+## Amplitude
+# https://amplitude.com/docs/sdks/analytics/android
+amplitudeAnalytics = "1.16.8"
[libraries]
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
@@ -127,6 +118,13 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxComposeNavigation" }
compose-compiler-gradle-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
+androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" }
+androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidxRoom" }
+
+coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutine" }
+coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutine" }
+coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutine" }
+
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" }
@@ -153,10 +151,6 @@ mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" }
mockk-agent = { group = "io.mockk", name = "mockk-agent", version.ref = "mockk" }
-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutine" }
-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutine" }
-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutine" }
-
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
diff --git a/presentation/src/main/java/com/puzzle/presentation/MainViewModel.kt b/presentation/src/main/java/com/puzzle/presentation/MainViewModel.kt
index 63e18a98..f7a06df4 100644
--- a/presentation/src/main/java/com/puzzle/presentation/MainViewModel.kt
+++ b/presentation/src/main/java/com/puzzle/presentation/MainViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.puzzle.common.event.EventHelper
import com.puzzle.domain.model.error.ErrorHelper
import com.puzzle.domain.model.error.HttpResponseException
+import com.puzzle.domain.repository.TermsRepository
import com.puzzle.navigation.NavigationHelper
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
@@ -12,6 +13,7 @@ import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
+ private val termsRepository: TermsRepository,
internal val navigationHelper: NavigationHelper,
internal val eventHelper: EventHelper,
private val errorHelper: ErrorHelper,
@@ -19,6 +21,7 @@ class MainViewModel @Inject constructor(
init {
handleError()
+ loadTerms()
}
private fun handleError() = viewModelScope.launch {
@@ -33,4 +36,10 @@ class MainViewModel @Inject constructor(
}
}
}
+
+ private fun loadTerms() = viewModelScope.launch {
+ termsRepository.loadTerms().onFailure {
+ errorHelper.sendError(it)
+ }
+ }
}
diff --git a/presentation/src/main/java/com/puzzle/presentation/ui/App.kt b/presentation/src/main/java/com/puzzle/presentation/ui/App.kt
index 001275e2..caccf04c 100644
--- a/presentation/src/main/java/com/puzzle/presentation/ui/App.kt
+++ b/presentation/src/main/java/com/puzzle/presentation/ui/App.kt
@@ -34,6 +34,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.puzzle.common.ui.NoRippleInteractionSource
+import com.puzzle.designsystem.R
import com.puzzle.designsystem.foundation.PieceTheme
import com.puzzle.navigation.AuthGraph
import com.puzzle.navigation.MatchingGraph
@@ -41,7 +42,6 @@ import com.puzzle.navigation.MatchingGraphDest.MatchingDetailRoute
import com.puzzle.navigation.MyPageRoute
import com.puzzle.navigation.Route
import com.puzzle.navigation.SettingRoute
-import com.puzzle.designsystem.R
import com.puzzle.presentation.navigation.AppNavHost
import com.puzzle.presentation.navigation.TopLevelDestination
import kotlin.reflect.KClass
@@ -59,6 +59,7 @@ fun App(
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
+ containerColor = PieceTheme.colors.white,
bottomBar = {
if (currentDestination?.shouldHideBottomNavigation() == false) {
AppBottomBar(
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 41c4c480..c74ff13e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -24,14 +24,18 @@ dependencyResolutionManagement {
rootProject.name = "Piece"
include(":app")
-include(":feature:auth")
-include(":feature:mypage")
-include(":feature:matching")
-include(":feature:setting")
+
include(":core:domain")
include(":core:designsystem")
include(":core:data")
include(":core:network")
include(":core:navigation")
include(":core:common-ui")
+include(":core:database")
+include(":core:common")
+
+include(":feature:auth")
+include(":feature:mypage")
+include(":feature:matching")
+include(":feature:setting")
include(":presentation")