diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 29b900515..5c5663364 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -117,8 +117,10 @@ dependencies {
implementation(projects.domain.soptamp)
implementation(projects.domain.mypage)
implementation(projects.domain.poke)
+ implementation(projects.domain.fortune)
implementation(projects.domain.notification)
implementation(projects.feature.soptamp)
+ implementation(projects.data.fortune)
implementation(projects.data.soptamp)
implementation(projects.data.mypage)
implementation(projects.data.poke)
diff --git a/data/fortune/.gitignore b/data/fortune/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/data/fortune/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/data/fortune/build.gradle.kts b/data/fortune/build.gradle.kts
new file mode 100644
index 000000000..d16316277
--- /dev/null
+++ b/data/fortune/build.gradle.kts
@@ -0,0 +1,40 @@
+/*
+ * MIT License
+ * Copyright 2023-2024 SOPT - Shout Our Passion Together
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+plugins {
+ sopt("feature")
+}
+
+android {
+ namespace = "org.sopt.official.data.fortune"
+}
+
+dependencies {
+ implementation(projects.domain.fortune)
+ implementation(projects.core.network)
+ implementation(projects.core.common)
+ implementation(platform(libs.okhttp.bom))
+ implementation(libs.bundles.okhttp)
+}
diff --git a/data/fortune/consumer-rules.pro b/data/fortune/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/data/fortune/src/main/AndroidManifest.xml b/data/fortune/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..3414a8184
--- /dev/null
+++ b/data/fortune/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/data/fortune/src/main/java/org/sopt/official/data/fortune/di/ApiModule.kt b/data/fortune/src/main/java/org/sopt/official/data/fortune/di/ApiModule.kt
new file mode 100644
index 000000000..bd74040df
--- /dev/null
+++ b/data/fortune/src/main/java/org/sopt/official/data/fortune/di/ApiModule.kt
@@ -0,0 +1,20 @@
+package org.sopt.official.data.fortune.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.sopt.official.common.di.AppRetrofit
+import org.sopt.official.data.fortune.remote.api.FortuneApi
+import retrofit2.Retrofit
+import retrofit2.create
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal object ApiModule {
+
+ @Provides
+ @Singleton
+ internal fun provideFortuneApi(@AppRetrofit(true) retrofit: Retrofit): FortuneApi = retrofit.create()
+}
diff --git a/data/fortune/src/main/java/org/sopt/official/data/fortune/di/RepositoryModule.kt b/data/fortune/src/main/java/org/sopt/official/data/fortune/di/RepositoryModule.kt
new file mode 100644
index 000000000..d720c0933
--- /dev/null
+++ b/data/fortune/src/main/java/org/sopt/official/data/fortune/di/RepositoryModule.kt
@@ -0,0 +1,18 @@
+package org.sopt.official.data.fortune.di
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.sopt.official.data.fortune.repository.DefaultFortuneRepository
+import org.sopt.official.domain.fortune.repository.FortuneRepository
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal interface RepositoryModule {
+
+ @Binds
+ @Singleton
+ abstract fun bindDefaultFortuneRepository(defaultFortuneRepository: DefaultFortuneRepository): FortuneRepository
+}
diff --git a/data/fortune/src/main/java/org/sopt/official/data/fortune/mapper/FortuneMapper.kt b/data/fortune/src/main/java/org/sopt/official/data/fortune/mapper/FortuneMapper.kt
new file mode 100644
index 000000000..bb7d2081a
--- /dev/null
+++ b/data/fortune/src/main/java/org/sopt/official/data/fortune/mapper/FortuneMapper.kt
@@ -0,0 +1,18 @@
+package org.sopt.official.data.fortune.mapper
+
+import org.sopt.official.data.fortune.remote.response.TodayFortuneCardResponse
+import org.sopt.official.data.fortune.remote.response.TodayFortuneWordResponse
+import org.sopt.official.domain.fortune.model.TodayFortuneCard
+import org.sopt.official.domain.fortune.model.TodayFortuneWord
+
+internal fun TodayFortuneCardResponse.toDomain(): TodayFortuneCard = TodayFortuneCard(
+ description = description,
+ imageColorCode = imageColorCode,
+ imageUrl = imageUrl,
+ name = name,
+)
+
+internal fun TodayFortuneWordResponse.toDomain(): TodayFortuneWord = TodayFortuneWord(
+ userName = userName,
+ title = title,
+)
diff --git a/data/fortune/src/main/java/org/sopt/official/data/fortune/remote/api/FortuneApi.kt b/data/fortune/src/main/java/org/sopt/official/data/fortune/remote/api/FortuneApi.kt
new file mode 100644
index 000000000..e7a5c4155
--- /dev/null
+++ b/data/fortune/src/main/java/org/sopt/official/data/fortune/remote/api/FortuneApi.kt
@@ -0,0 +1,17 @@
+package org.sopt.official.data.fortune.remote.api
+
+import org.sopt.official.data.fortune.remote.response.TodayFortuneCardResponse
+import org.sopt.official.data.fortune.remote.response.TodayFortuneWordResponse
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+internal interface FortuneApi {
+
+ @GET("fortune/word")
+ suspend fun getTodayFortuneWord(
+ @Query("todayDate") todayDate: String,
+ ): TodayFortuneWordResponse
+
+ @GET("fortune/card/today")
+ suspend fun getTodayFortuneCard(): TodayFortuneCardResponse
+}
diff --git a/data/fortune/src/main/java/org/sopt/official/data/fortune/remote/response/TodayFortuneCardResponse.kt b/data/fortune/src/main/java/org/sopt/official/data/fortune/remote/response/TodayFortuneCardResponse.kt
new file mode 100644
index 000000000..053214cb7
--- /dev/null
+++ b/data/fortune/src/main/java/org/sopt/official/data/fortune/remote/response/TodayFortuneCardResponse.kt
@@ -0,0 +1,16 @@
+package org.sopt.official.data.fortune.remote.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+internal data class TodayFortuneCardResponse(
+ @SerialName("description")
+ val description: String,
+ @SerialName("imageColorCode")
+ val imageColorCode: String,
+ @SerialName("imageUrl")
+ val imageUrl: String,
+ @SerialName("name")
+ val name: String,
+)
diff --git a/data/fortune/src/main/java/org/sopt/official/data/fortune/remote/response/TodayFortuneWordResponse.kt b/data/fortune/src/main/java/org/sopt/official/data/fortune/remote/response/TodayFortuneWordResponse.kt
new file mode 100644
index 000000000..e374976c3
--- /dev/null
+++ b/data/fortune/src/main/java/org/sopt/official/data/fortune/remote/response/TodayFortuneWordResponse.kt
@@ -0,0 +1,12 @@
+package org.sopt.official.data.fortune.remote.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+internal data class TodayFortuneWordResponse(
+ @SerialName("userName")
+ val userName: String,
+ @SerialName("title")
+ val title: String,
+)
diff --git a/data/fortune/src/main/java/org/sopt/official/data/fortune/repository/DefaultFortuneRepository.kt b/data/fortune/src/main/java/org/sopt/official/data/fortune/repository/DefaultFortuneRepository.kt
new file mode 100644
index 000000000..2a79bd486
--- /dev/null
+++ b/data/fortune/src/main/java/org/sopt/official/data/fortune/repository/DefaultFortuneRepository.kt
@@ -0,0 +1,17 @@
+package org.sopt.official.data.fortune.repository
+
+import org.sopt.official.data.fortune.mapper.toDomain
+import org.sopt.official.data.fortune.remote.api.FortuneApi
+import org.sopt.official.domain.fortune.model.TodayFortuneCard
+import org.sopt.official.domain.fortune.model.TodayFortuneWord
+import org.sopt.official.domain.fortune.repository.FortuneRepository
+import javax.inject.Inject
+
+internal class DefaultFortuneRepository @Inject constructor(
+ private val fortuneApi: FortuneApi,
+) : FortuneRepository {
+
+ override suspend fun fetchTodayFortuneWord(date: String): TodayFortuneWord = fortuneApi.getTodayFortuneWord(date).toDomain()
+
+ override suspend fun fetchTodayFortuneCard(): TodayFortuneCard = fortuneApi.getTodayFortuneCard().toDomain()
+}
diff --git a/domain/fortune/.gitignore b/domain/fortune/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/domain/fortune/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/domain/fortune/build.gradle.kts b/domain/fortune/build.gradle.kts
new file mode 100644
index 000000000..0adbfe1a8
--- /dev/null
+++ b/domain/fortune/build.gradle.kts
@@ -0,0 +1,36 @@
+/*
+ * MIT License
+ * Copyright 2023-2024 SOPT - Shout Our Passion Together
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+plugins {
+ sopt("kotlin")
+}
+
+kotlin {
+ jvmToolchain(17)
+}
+
+dependencies {
+ implementation(libs.javax.inject)
+}
diff --git a/domain/fortune/src/main/java/org/sopt/official/domain/fortune/model/TodayFortuneCard.kt b/domain/fortune/src/main/java/org/sopt/official/domain/fortune/model/TodayFortuneCard.kt
new file mode 100644
index 000000000..97c8e77ce
--- /dev/null
+++ b/domain/fortune/src/main/java/org/sopt/official/domain/fortune/model/TodayFortuneCard.kt
@@ -0,0 +1,8 @@
+package org.sopt.official.domain.fortune.model
+
+data class TodayFortuneCard(
+ val description: String,
+ val imageColorCode: String,
+ val imageUrl: String,
+ val name: String,
+)
diff --git a/domain/fortune/src/main/java/org/sopt/official/domain/fortune/model/TodayFortuneWord.kt b/domain/fortune/src/main/java/org/sopt/official/domain/fortune/model/TodayFortuneWord.kt
new file mode 100644
index 000000000..52ef6e948
--- /dev/null
+++ b/domain/fortune/src/main/java/org/sopt/official/domain/fortune/model/TodayFortuneWord.kt
@@ -0,0 +1,6 @@
+package org.sopt.official.domain.fortune.model
+
+data class TodayFortuneWord(
+ val userName: String,
+ val title: String,
+)
diff --git a/domain/fortune/src/main/java/org/sopt/official/domain/fortune/repository/FortuneRepository.kt b/domain/fortune/src/main/java/org/sopt/official/domain/fortune/repository/FortuneRepository.kt
new file mode 100644
index 000000000..7932187f2
--- /dev/null
+++ b/domain/fortune/src/main/java/org/sopt/official/domain/fortune/repository/FortuneRepository.kt
@@ -0,0 +1,9 @@
+package org.sopt.official.domain.fortune.repository
+
+import org.sopt.official.domain.fortune.model.TodayFortuneCard
+import org.sopt.official.domain.fortune.model.TodayFortuneWord
+
+interface FortuneRepository {
+ suspend fun fetchTodayFortuneWord(date: String): TodayFortuneWord
+ suspend fun fetchTodayFortuneCard(): TodayFortuneCard
+}
diff --git a/domain/fortune/src/main/java/org/sopt/official/domain/fortune/usecase/GetTodayDateUseCase.kt b/domain/fortune/src/main/java/org/sopt/official/domain/fortune/usecase/GetTodayDateUseCase.kt
new file mode 100644
index 000000000..b6380d578
--- /dev/null
+++ b/domain/fortune/src/main/java/org/sopt/official/domain/fortune/usecase/GetTodayDateUseCase.kt
@@ -0,0 +1,14 @@
+package org.sopt.official.domain.fortune.usecase
+
+import java.text.SimpleDateFormat
+import java.util.Locale
+import javax.inject.Inject
+
+class GetTodayDateUseCase @Inject constructor() {
+
+ operator fun invoke(): String {
+ val currentDate = System.currentTimeMillis()
+
+ return SimpleDateFormat("yyyy-MM-dd", Locale.KOREAN).format(currentDate)
+ }
+}
diff --git a/domain/fortune/src/main/java/org/sopt/official/domain/fortune/usecase/GetTodayFortuneUseCase.kt b/domain/fortune/src/main/java/org/sopt/official/domain/fortune/usecase/GetTodayFortuneUseCase.kt
new file mode 100644
index 000000000..df3d656a1
--- /dev/null
+++ b/domain/fortune/src/main/java/org/sopt/official/domain/fortune/usecase/GetTodayFortuneUseCase.kt
@@ -0,0 +1,13 @@
+package org.sopt.official.domain.fortune.usecase
+
+import org.sopt.official.domain.fortune.model.TodayFortuneWord
+import org.sopt.official.domain.fortune.repository.FortuneRepository
+import javax.inject.Inject
+
+class GetTodayFortuneUseCase @Inject constructor(
+ private val fortuneRepository: FortuneRepository,
+ private val getTodayDateUseCase: GetTodayDateUseCase,
+) {
+
+ suspend operator fun invoke(): TodayFortuneWord = fortuneRepository.fetchTodayFortuneWord(getTodayDateUseCase())
+}
diff --git a/feature/fortune/build.gradle.kts b/feature/fortune/build.gradle.kts
index aba67f215..de172d530 100644
--- a/feature/fortune/build.gradle.kts
+++ b/feature/fortune/build.gradle.kts
@@ -25,6 +25,7 @@
plugins {
sopt("feature")
sopt("compose")
+ sopt("test")
}
android {
@@ -32,6 +33,9 @@ android {
}
dependencies {
+ // domain
+ implementation(projects.domain.fortune)
+
// core
implementation(projects.core.common)
implementation(projects.core.designsystem)
diff --git a/feature/fortune/src/androidTest/java/org/sopt/official/feature/fortune/fortuneDetail/FortuneDetailScreenTest.kt b/feature/fortune/src/androidTest/java/org/sopt/official/feature/fortune/fortuneDetail/FortuneDetailScreenTest.kt
new file mode 100644
index 000000000..688ff96aa
--- /dev/null
+++ b/feature/fortune/src/androidTest/java/org/sopt/official/feature/fortune/fortuneDetail/FortuneDetailScreenTest.kt
@@ -0,0 +1,54 @@
+package org.sopt.official.feature.fortune.fortuneDetail
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import org.junit.Rule
+import org.junit.Test
+import org.sopt.official.designsystem.SoptTheme
+import org.sopt.official.feature.fortune.feature.fortuneDetail.FortuneDetailScreen
+import org.sopt.official.feature.fortune.feature.fortuneDetail.model.FortuneDetailUiState
+
+internal class FortuneDetailScreenTest {
+
+ @get:Rule
+ val composeRule = createComposeRule()
+
+ @Test
+ fun 서버통신이_성공하면_이름_솝마디_날짜가_노출된다() {
+ // given:
+ val date = "2024-09-26"
+ val name = "이현우"
+ val content = "안녕하세요안녕하세요안녕하세요안녕하세요안녕하세요"
+
+ // when:
+
+ composeRule.setContent {
+ SoptTheme {
+ FortuneDetailScreen(
+ paddingValue = PaddingValues(),
+ date = date,
+ onFortuneAmuletClick = { },
+ uiState = FortuneDetailUiState.TodaySentence(
+ userName = name,
+ content = content,
+ )
+ )
+ }
+ }
+
+ // then:
+ val todayFortune = composeRule.onNodeWithContentDescription("todaySentence")
+ .fetchSemanticsNode().config.getOrNull(SemanticsProperties.Text)?.joinToString(separator = "").orEmpty()
+
+ composeRule.waitForIdle()
+
+ composeRule.onNodeWithText(date).assertIsDisplayed()
+ assert(todayFortune.contains(name))
+ assert(todayFortune.contains(content))
+ }
+}
diff --git a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/FoundationScreen.kt b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/FoundationScreen.kt
index 9164495c1..cb155d8f6 100644
--- a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/FoundationScreen.kt
+++ b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/FoundationScreen.kt
@@ -37,8 +37,8 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import org.sopt.official.designsystem.SoptTheme
import org.sopt.official.feature.fortune.component.FortuneTopBar
-import org.sopt.official.feature.fortune.feature.fortundDetail.navigation.FortuneDetail
-import org.sopt.official.feature.fortune.feature.fortundDetail.navigation.fortuneDetailNavGraph
+import org.sopt.official.feature.fortune.feature.fortuneDetail.navigation.FortuneDetail
+import org.sopt.official.feature.fortune.feature.fortuneDetail.navigation.fortuneDetailNavGraph
import org.sopt.official.feature.fortune.feature.fortuneAmulet.navigation.FortuneAmulet
import org.sopt.official.feature.fortune.feature.fortuneAmulet.navigation.fortuneAmuletNavGraph
import org.sopt.official.feature.fortune.feature.home.navigation.Home
diff --git a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/FortuneDetailRoute.kt b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/FortuneDetailRoute.kt
new file mode 100644
index 000000000..6a552c62a
--- /dev/null
+++ b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/FortuneDetailRoute.kt
@@ -0,0 +1,24 @@
+package org.sopt.official.feature.fortune.feature.fortuneDetail
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+
+@Composable
+internal fun FortuneDetailRoute(
+ paddingValue: PaddingValues,
+ date: String,
+ onFortuneAmuletClick: () -> Unit,
+ viewModel: FortuneDetailViewModel = hiltViewModel(),
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ FortuneDetailScreen(
+ paddingValue = paddingValue,
+ date = date,
+ onFortuneAmuletClick = onFortuneAmuletClick,
+ uiState = uiState,
+ )
+}
diff --git a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortundDetail/FortuneDetailScreen.kt b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/FortuneDetailScreen.kt
similarity index 55%
rename from feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortundDetail/FortuneDetailScreen.kt
rename to feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/FortuneDetailScreen.kt
index c438930af..d8dac0c88 100644
--- a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortundDetail/FortuneDetailScreen.kt
+++ b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/FortuneDetailScreen.kt
@@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-package org.sopt.official.feature.fortune.feature.fortundDetail
+package org.sopt.official.feature.fortune.feature.fortuneDetail
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -30,65 +30,60 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Button
-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 androidx.compose.ui.unit.dp
import org.sopt.official.designsystem.SoptTheme
+import org.sopt.official.feature.fortune.feature.fortuneDetail.component.TodayFortuneDashboard
+import org.sopt.official.feature.fortune.feature.fortuneDetail.model.FortuneDetailUiState
+import org.sopt.official.feature.fortune.feature.fortuneDetail.model.FortuneDetailUiState.Error
+import org.sopt.official.feature.fortune.feature.fortuneDetail.model.FortuneDetailUiState.Loading
+import org.sopt.official.feature.fortune.feature.fortuneDetail.model.FortuneDetailUiState.TodaySentence
+import timber.log.Timber
@Composable
-fun FortuneDetailRoute(
+internal fun FortuneDetailScreen(
paddingValue: PaddingValues,
date: String,
- navigateToFortuneAmulet: () -> Unit,
-) {
- FortuneDetailScreen(
- paddingValue = paddingValue,
- date = date,
- navigateToFortuneAmulet = navigateToFortuneAmulet
- )
-}
-
-@Composable
-fun FortuneDetailScreen(
- paddingValue: PaddingValues,
- date: String,
- navigateToFortuneAmulet: () -> Unit,
+ onFortuneAmuletClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ uiState: FortuneDetailUiState = Loading,
) {
Column(
- modifier = Modifier
- .padding(paddingValue)
- .fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = modifier.fillMaxSize().padding(paddingValues = paddingValue),
) {
- Text(
- text = "Fortune Detail Screen: $date",
- color = SoptTheme.colors.onBackground
- )
+ Spacer(modifier = Modifier.height(height = 16.dp))
+ when (uiState) {
+ is TodaySentence -> {
+ TodayFortuneDashboard(
+ date = date,
+ todaySentence = uiState.message,
+ )
+ }
- Spacer(modifier = Modifier.weight(1f))
-
- Button(
- onClick = navigateToFortuneAmulet
- ) {
- Text(text = "Go to Fortune Amulet")
+ is Error -> Timber.e(uiState.errorMessage)
+ is Loading -> {
+ // 로딩 뷰
+ }
}
- Spacer(modifier = Modifier.height(50.dp))
}
}
-
@Preview
@Composable
-fun FortuneDetailScreenPreview() {
+private fun FortuneDetailScreenPreview() {
SoptTheme {
FortuneDetailScreen(
- paddingValue = PaddingValues(16.dp),
+ paddingValue = PaddingValues(vertical = 16.dp),
date = "2024-09-09",
- navigateToFortuneAmulet = {}
+ onFortuneAmuletClick = {},
+ uiState = TodaySentence(
+ userName = "누누",
+ content = "오늘 하루종일 기분 좋을 것 같은 날이네요.",
+ ),
)
}
}
diff --git a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/FortuneDetailViewModel.kt b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/FortuneDetailViewModel.kt
new file mode 100644
index 000000000..ea3ed4f87
--- /dev/null
+++ b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/FortuneDetailViewModel.kt
@@ -0,0 +1,45 @@
+package org.sopt.official.feature.fortune.feature.fortuneDetail
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.sopt.official.domain.fortune.usecase.GetTodayFortuneUseCase
+import org.sopt.official.feature.fortune.feature.fortuneDetail.model.FortuneDetailUiState
+import org.sopt.official.feature.fortune.feature.fortuneDetail.model.FortuneDetailUiState.Error
+import org.sopt.official.feature.fortune.feature.fortuneDetail.model.FortuneDetailUiState.Loading
+import org.sopt.official.feature.fortune.feature.fortuneDetail.model.FortuneDetailUiState.TodaySentence
+import javax.inject.Inject
+
+@HiltViewModel
+internal class FortuneDetailViewModel @Inject constructor(
+ getTodayFortuneUseCase: GetTodayFortuneUseCase,
+) : ViewModel() {
+ private val _uiState: MutableStateFlow = MutableStateFlow(Loading)
+ val uiState: StateFlow get() = _uiState.asStateFlow()
+
+ init {
+ viewModelScope.launch {
+ runCatching {
+ getTodayFortuneUseCase()
+ }.onSuccess { result ->
+ _uiState.update {
+ TodaySentence(
+ userName = result.userName,
+ content = result.title,
+ )
+ }
+ }.onFailure { error ->
+ _uiState.update {
+ Error(error)
+ }
+ }
+ }
+ }
+}
+
+
diff --git a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/component/FortuneDetailBox.kt b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/component/FortuneDetailBox.kt
new file mode 100644
index 000000000..d250065d5
--- /dev/null
+++ b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/component/FortuneDetailBox.kt
@@ -0,0 +1,43 @@
+package org.sopt.official.feature.fortune.feature.fortuneDetail.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+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.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import org.sopt.official.designsystem.Gray700
+
+@Composable
+internal fun TodayFortuneBox(
+ content: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp)
+ .background(
+ color = Gray700,
+ shape = RoundedCornerShape(12.dp),
+ ),
+ ) {
+ content()
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun TodayFortuneBoxPreview() {
+ TodayFortuneBox(
+ content = { Text("123") },
+ modifier = Modifier.background(color = Color.White),
+ )
+}
diff --git a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/component/TodayFortuneDashboard.kt b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/component/TodayFortuneDashboard.kt
new file mode 100644
index 000000000..1d23ff55d
--- /dev/null
+++ b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/component/TodayFortuneDashboard.kt
@@ -0,0 +1,100 @@
+/*
+ * MIT License
+ * Copyright 2024 SOPT - Shout Our Passion Together
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.sopt.official.feature.fortune.feature.fortuneDetail.component
+
+import androidx.compose.foundation.Image
+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.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.LineBreak
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import org.sopt.official.designsystem.Gray100
+import org.sopt.official.designsystem.Gray30
+import org.sopt.official.designsystem.SoptTheme
+import org.sopt.official.feature.fortune.R
+
+@Composable
+internal fun TodayFortuneDashboard(
+ date: String,
+ todaySentence: String,
+ modifier: Modifier = Modifier,
+) {
+ TodayFortuneBox(
+ content = {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = modifier,
+ ) {
+ Spacer(modifier = Modifier.height(height = 32.dp))
+ Image(
+ painter = painterResource(R.drawable.img_fortune_title),
+ contentDescription = "오늘의 솝마디",
+ )
+ Spacer(modifier = Modifier.height(height = 10.dp))
+ Text(
+ text = date,
+ style = SoptTheme.typography.title18SB,
+ color = Gray100,
+ )
+ Spacer(modifier = Modifier.height(height = 20.dp))
+ Text(
+ text = todaySentence,
+ style = SoptTheme.typography.title24SB.copy(
+ lineBreak = LineBreak.Simple,
+ ),
+ color = Gray30,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 68.dp)
+ .semantics { contentDescription = "todaySentence" },
+ )
+ Spacer(modifier = Modifier.height(height = 36.dp))
+ }
+ }
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun TodayFortuneDashboardPreview() {
+ SoptTheme {
+ TodayFortuneDashboard(
+ date = "2024-09-09",
+ todaySentence = "hi my name is Sehun kim, nice to meet you",
+ )
+ }
+}
diff --git a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/model/FortuneDetailUiState.kt b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/model/FortuneDetailUiState.kt
new file mode 100644
index 000000000..9cda01651
--- /dev/null
+++ b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/model/FortuneDetailUiState.kt
@@ -0,0 +1,23 @@
+package org.sopt.official.feature.fortune.feature.fortuneDetail.model
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+
+@Stable
+internal sealed interface FortuneDetailUiState {
+
+ @Immutable
+ data class TodaySentence(
+ val userName: String,
+ val content: String,
+ ) : FortuneDetailUiState {
+ val message: String
+ get() = "${userName}님,\n${content}"
+ }
+
+ @Immutable
+ data object Loading : FortuneDetailUiState
+
+ @Immutable
+ data class Error(val errorMessage: Throwable) : FortuneDetailUiState
+}
diff --git a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortundDetail/navigation/FortuneDetailNavGraph.kt b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/navigation/FortuneDetailNavGraph.kt
similarity index 90%
rename from feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortundDetail/navigation/FortuneDetailNavGraph.kt
rename to feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/navigation/FortuneDetailNavGraph.kt
index 731995b46..25c9369e0 100644
--- a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortundDetail/navigation/FortuneDetailNavGraph.kt
+++ b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/fortuneDetail/navigation/FortuneDetailNavGraph.kt
@@ -22,14 +22,14 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-package org.sopt.official.feature.fortune.feature.fortundDetail.navigation
+package org.sopt.official.feature.fortune.feature.fortuneDetail.navigation
import androidx.compose.foundation.layout.PaddingValues
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import kotlinx.serialization.Serializable
-import org.sopt.official.feature.fortune.feature.fortundDetail.FortuneDetailRoute
+import org.sopt.official.feature.fortune.feature.fortuneDetail.FortuneDetailRoute
@Serializable
data class FortuneDetail(val date: String)
@@ -43,7 +43,7 @@ fun NavGraphBuilder.fortuneDetailNavGraph(
FortuneDetailRoute(
paddingValue = paddingValue,
date = items.date,
- navigateToFortuneAmulet = navigateToFortuneAmulet
+ onFortuneAmuletClick = navigateToFortuneAmulet
)
}
}
diff --git a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/home/navigation/HomeNavGraph.kt b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/home/navigation/HomeNavGraph.kt
index 03473bc03..adb65ced5 100644
--- a/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/home/navigation/HomeNavGraph.kt
+++ b/feature/fortune/src/main/java/org/sopt/official/feature/fortune/feature/home/navigation/HomeNavGraph.kt
@@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import kotlinx.serialization.Serializable
-import org.sopt.official.feature.fortune.feature.fortuneAmulet.navigation.FortuneAmulet
import org.sopt.official.feature.fortune.feature.home.HomeRoute
@Serializable
@@ -38,7 +37,7 @@ fun NavGraphBuilder.homeNavGraph(
paddingValue: PaddingValues,
navigateToFortuneDetail: (String) -> Unit,
) {
- composable {
+ composable {
HomeRoute(
paddingValue = paddingValue,
navigateToFortuneDetail = navigateToFortuneDetail
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 81a7a14e0..0340a6ea4 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -30,6 +30,8 @@ include(
":data:notification",
":data:soptamp",
":data:poke",
+ ":data:fortune",
+ ":domain:fortune",
":domain:mypage",
":domain:notification",
":domain:soptamp",