diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7d88adf2..ac214353 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,3 +1,5 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+
plugins {
alias(libs.plugins.spot.android.application)
alias(libs.plugins.kotlin.android)
@@ -9,9 +11,12 @@ android {
// signingConfigs {
// getByName("debug") {
-// keyAlias = "androiddebugkey"
-// keyPassword = "android"
-// storeFile = file("debug.keystore")
+// val props = gradleLocalProperties(rootDir, providers)
+//
+// storeFile = file(props.getProperty("DEBUG_STORE_FILE"))
+// storePassword = props.getProperty("DEBUG_STORE_PASSWORD")
+// keyAlias = props.getProperty("DEBUG_KEY_ALIAS")
+// keyPassword = props.getProperty("DEBUG_KEY_PASSWORD")
// }
// }
@@ -38,8 +43,6 @@ dependencies {
implementation(projects.feature.study)
implementation(projects.feature.signup)
- implementation(projects.domain.home)
-
implementation(projects.core.ui)
implementation(projects.core.network)
implementation(projects.core.model)
@@ -53,4 +56,13 @@ dependencies {
implementation(projects.data.study)
implementation(projects.data.alert)
implementation(projects.data.board)
+ implementation(projects.data.user)
+ implementation(projects.data.login)
+
+ implementation(libs.kakao.common)
+ implementation(libs.kakao.login)
+ implementation(libs.kakao.auth)
+
+ implementation(libs.naver.oauth)
+// implementation(libs.naver.jdk)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8fc3813f..fde638d1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -31,5 +31,19 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/umcspot/spot/SpotApplication.kt b/app/src/main/java/com/umcspot/spot/SpotApplication.kt
index 0b442c23..f79c28e0 100644
--- a/app/src/main/java/com/umcspot/spot/SpotApplication.kt
+++ b/app/src/main/java/com/umcspot/spot/SpotApplication.kt
@@ -1,11 +1,19 @@
package com.umcspot.spot
import android.app.Application
+import android.util.Log
+import com.umcspot.spot.buildconfig.BuildConfig
+import com.kakao.sdk.common.KakaoSdk
+import com.kakao.sdk.common.util.Utility
+import com.navercorp.nid.NidOAuth
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class SpotApplication : Application() {
override fun onCreate() {
super.onCreate()
+
+ KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_KEY)
+ NidOAuth.initialize(this, BuildConfig.NAVER_CLIENT_ID, BuildConfig.NAVER_CLIENT_SECRET, BuildConfig.APP_NAME)
}
}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/java/com/umcspot/spot/convention/BuildConfig.kt b/build-logic/convention/src/main/java/com/umcspot/spot/convention/BuildConfig.kt
index 94a5dd54..d34a2da3 100644
--- a/build-logic/convention/src/main/java/com/umcspot/spot/convention/BuildConfig.kt
+++ b/build-logic/convention/src/main/java/com/umcspot/spot/convention/BuildConfig.kt
@@ -7,19 +7,46 @@ import org.gradle.api.Project
internal fun Project.configureBuildConfig(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
+ val properties = gradleLocalProperties(rootDir, providers)
+
+ val baseUrl = properties.getProperty("BASE_URL") ?: ""
+ val kakaoNativeKey = properties.getProperty("KAKAO_NATIVE_KEY") ?: ""
+ val naverClientId = properties.getProperty("NAVER_CLIENT_ID") ?: ""
+ val naverClientSecret = properties.getProperty("NAVER_CLIENT_SECRET") ?: ""
+ val appName = properties.getProperty("APP_NAME") ?: "SPOT"
+
+
commonExtension.apply {
defaultConfig {
- val properties = gradleLocalProperties(rootDir, providers)
buildConfigField(
"String",
"BASE_URL",
- "\"${properties.getProperty("base.url") ?: "https://default-url.com/"}\""
+ "\"$baseUrl\""
)
+
buildConfigField(
"String",
"KAKAO_NATIVE_KEY",
- "\"${properties.getProperty("kakao.native.key") ?: ""}\""
+ "\"$kakaoNativeKey\""
+ )
+
+ buildConfigField(
+ "String",
+ "NAVER_CLIENT_ID",
+ "\"$naverClientId\""
+ )
+
+ buildConfigField(
+ "String",
+ "NAVER_CLIENT_SECRET",
+ "\"$naverClientSecret\""
+ )
+
+ buildConfigField(
+ "String",
+ "APP_NAME",
+ "\"$appName\""
)
}
@@ -27,4 +54,4 @@ internal fun Project.configureBuildConfig(
buildConfig = true
}
}
-}
\ No newline at end of file
+}
diff --git a/core/buildconfig/build.gradle.kts b/core/buildconfig/build.gradle.kts
index 0417476c..0dcb577c 100644
--- a/core/buildconfig/build.gradle.kts
+++ b/core/buildconfig/build.gradle.kts
@@ -8,6 +8,8 @@ plugins {
android {
namespace = "com.umcspot.spot.buildconfig"
}
+
+
dependencies {
implementation(projects.core.common)
}
\ No newline at end of file
diff --git a/core/datastore/src/main/java/com/umcspot/spot/datastore/SpotTokenData.kt b/core/datastore/src/main/java/com/umcspot/spot/datastore/SpotTokenData.kt
index e9018ffd..5c7f3aa8 100644
--- a/core/datastore/src/main/java/com/umcspot/spot/datastore/SpotTokenData.kt
+++ b/core/datastore/src/main/java/com/umcspot/spot/datastore/SpotTokenData.kt
@@ -1,7 +1,6 @@
@file:OptIn(kotlinx.serialization.InternalSerializationApi::class)
package com.umcspot.spot.datastore
-
import kotlinx.serialization.Serializable
@Serializable
diff --git a/core/datastore/src/main/java/com/umcspot/spot/datastore/di/DataStoreModule.kt b/core/datastore/src/main/java/com/umcspot/spot/datastore/di/DataStoreModule.kt
index 2f4b76f4..018d8b82 100644
--- a/core/datastore/src/main/java/com/umcspot/spot/datastore/di/DataStoreModule.kt
+++ b/core/datastore/src/main/java/com/umcspot/spot/datastore/di/DataStoreModule.kt
@@ -1,11 +1,9 @@
-package com.umcspot.spot.datastore.di
+package com.umcspot.spot.datastore
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
-import com.umcspot.spot.datastore.SpotSecureDataStoreSerializer
-import com.umcspot.spot.datastore.SpotTokenData
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -16,17 +14,16 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {
+
@Provides
@Singleton
- fun providesDataStore(
+ fun provideSpotTokenDataStore(
@ApplicationContext context: Context,
- spotSecureDataStoreSerializer: SpotSecureDataStoreSerializer
- ): DataStore =
- DataStoreFactory.create(
- serializer = spotSecureDataStoreSerializer
- ) {
- context.dataStoreFile(DATASTORE_PREFERENCES)
- }
-
- private const val DATASTORE_PREFERENCES = "com.umcspot.spot.datastore"
-}
\ No newline at end of file
+ serializer: SpotSecureDataStoreSerializer
+ ): DataStore {
+ return DataStoreFactory.create(
+ serializer = serializer,
+ produceFile = { context.dataStoreFile("spot_tokens.secure") }
+ )
+ }
+}
diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/GageBar.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/GageBar.kt
index ffd71329..1dfd6967 100644
--- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/GageBar.kt
+++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/GageBar.kt
@@ -34,7 +34,7 @@ import com.umcspot.spot.designsystem.theme.*
* @param borderColor 테두리 색
*/
@Composable
-fun GaugeBar(
+fun GageBar(
value: Float,
modifier: Modifier = Modifier,
height: Dp = 12.dp,
@@ -77,7 +77,7 @@ fun GaugeBar(
@Composable
fun Preview_GaugeBar_15() {
Column(modifier = Modifier.padding(16.dp)) {
- GaugeBar(
+ GageBar(
value = 0.15f, // 퍼센트 조절 가능
height = 12.dp,
trackColor = SpotTheme.colors.G100,
@@ -90,7 +90,7 @@ fun Preview_GaugeBar_15() {
@Composable
fun Preview_GaugeBar_75() {
Column(modifier = Modifier.padding(16.dp)) {
- GaugeBar(
+ GageBar(
value = 0.75f, // 퍼센트 조절 가능
height = 12.dp,
trackColor = SpotTheme.colors.G100,
diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt
index a60eda10..1a48b81d 100644
--- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt
+++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt
@@ -2,65 +2,60 @@ package com.umcspot.spot.designsystem.component.study.section
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
+import com.umcspot.spot.designsystem.R
+import com.umcspot.spot.designsystem.component.button.MultiButton
import com.umcspot.spot.model.StudyTheme
import com.umcspot.spot.ui.extension.screenHeightDp
import com.umcspot.spot.ui.extension.screenWidthDp
-import com.umcspot.spot.designsystem.R
-import com.umcspot.spot.designsystem.component.button.MultiButton
import kotlinx.collections.immutable.ImmutableList
@Composable
fun ActivityThemeSection(
- activityTheme: StudyTheme?,
- onSelect: (StudyTheme) -> Unit
+ selectedTheme: StudyTheme?,
+ onSelect: (StudyTheme) -> Unit,
+ modifier: Modifier = Modifier
) {
- Column(
- modifier = Modifier.fillMaxWidth()
- ) {
- FlowRow(
- horizontalArrangement = Arrangement.spacedBy(screenWidthDp(14.dp)),
- verticalArrangement = Arrangement.spacedBy(screenHeightDp(16.dp))
- ) {
- StudyTheme.entries.forEach { theme ->
- val iconRes = when (theme) {
- StudyTheme.LANGUAGE -> painterResource(R.drawable.language)
- StudyTheme.LICENSE -> painterResource(R.drawable.license)
- StudyTheme.EMPLOYMENT -> painterResource(R.drawable.employment)
- StudyTheme.DISCUSSION -> painterResource(R.drawable.discussion)
- StudyTheme.NEWS -> painterResource(R.drawable.news)
- StudyTheme.SELFSTUDY -> painterResource(R.drawable.self_study)
- StudyTheme.PROJECT -> painterResource(R.drawable.project)
- StudyTheme.CONTEST -> painterResource(R.drawable.contest)
- StudyTheme.MAJOR -> painterResource(R.drawable.major)
- StudyTheme.ETC -> painterResource(R.drawable.resource_else)
- }
-
- MultiButton(
- text = theme.title,
- painter = iconRes,
- checked = activityTheme == theme,
- onClick = { onSelect(theme) },
- )
- }
- }
- }
+ BaseActivityThemeSection(
+ modifier = modifier,
+ isThemeSelected = { it == selectedTheme },
+ isThemeEnabled = { true },
+ onSelect = onSelect
+ )
}
@Composable
fun ActivityThemeSection(
selectedThemes: ImmutableList,
onSelect: (StudyTheme) -> Unit,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ maxSelection: Int = 3
+) {
+ val isMaxSelected = selectedThemes.size >= maxSelection
+
+ BaseActivityThemeSection(
+ modifier = modifier,
+ isThemeSelected = { selectedThemes.contains(it) },
+
+ isThemeEnabled = { theme -> !isMaxSelected || selectedThemes.contains(theme) },
+ onSelect = onSelect
+ )
+}
+
+@Composable
+private fun BaseActivityThemeSection(
+ modifier: Modifier,
+ isThemeSelected: (StudyTheme) -> Boolean,
+ isThemeEnabled: (StudyTheme) -> Boolean,
+ onSelect: (StudyTheme) -> Unit
) {
val themesInRows = StudyTheme.entries.chunked(2)
- val isMaxSelected = selectedThemes.size >= 3
Column(
modifier = modifier,
@@ -72,31 +67,34 @@ fun ActivityThemeSection(
horizontalArrangement = Arrangement.spacedBy(screenWidthDp(14.dp))
) {
themesInRow.forEach { theme ->
- val iconRes = getIconForTheme(theme)
- val isChecked = selectedThemes.contains(theme)
-
MultiButton(
+ modifier = Modifier.weight(1f),
text = theme.title,
- painter = iconRes,
- checked = isChecked,
- enabled = !isMaxSelected || isChecked,
+ painter = getIconForTheme(theme),
+ checked = isThemeSelected(theme),
+ enabled = isThemeEnabled(theme),
onClick = { onSelect(theme) },
)
}
+
+ if (themesInRow.size < 2) {
+ Spacer(modifier = Modifier.weight(1f))
+ }
}
}
}
}
+
@Composable
private fun getIconForTheme(theme: StudyTheme) = when (theme) {
StudyTheme.LANGUAGE -> painterResource(R.drawable.language)
- StudyTheme.LICENSE -> painterResource(R.drawable.license)
- StudyTheme.EMPLOYMENT -> painterResource(R.drawable.employment)
- StudyTheme.DISCUSSION -> painterResource(R.drawable.discussion)
- StudyTheme.NEWS -> painterResource(R.drawable.news)
- StudyTheme.SELFSTUDY -> painterResource(R.drawable.self_study)
+ StudyTheme.CERTIFICATION -> painterResource(R.drawable.license)
+ StudyTheme.CAREER -> painterResource(R.drawable.employment)
+ StudyTheme.DEBATE -> painterResource(R.drawable.discussion)
+ StudyTheme.CURRENT_AFFAIRS -> painterResource(R.drawable.news)
+ StudyTheme.SELF_STUDY -> painterResource(R.drawable.self_study)
StudyTheme.PROJECT -> painterResource(R.drawable.project)
- StudyTheme.CONTEST -> painterResource(R.drawable.contest)
- StudyTheme.MAJOR -> painterResource(R.drawable.major)
- StudyTheme.ETC -> painterResource(R.drawable.resource_else)
+ StudyTheme.COMPETITION -> painterResource(R.drawable.contest)
+ StudyTheme.MAJOR_CAREER -> painterResource(R.drawable.major)
+ StudyTheme.OTHER -> painterResource(R.drawable.resource_else)
}
\ No newline at end of file
diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/shapes/Shapes.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/shapes/Shapes.kt
index f728d65c..8222b422 100644
--- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/shapes/Shapes.kt
+++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/shapes/Shapes.kt
@@ -176,7 +176,6 @@ fun StateCardActive(
borderWidth: Dp = 1.dp,
modifier: Modifier = Modifier
) = ShapeBox(
-
shape = shape,
color = color,
borderWidth = borderWidth,
diff --git a/core/model/src/main/java/com/umcspot/spot/model/Global.kt b/core/model/src/main/java/com/umcspot/spot/model/Global.kt
index d7609ff7..3babae72 100644
--- a/core/model/src/main/java/com/umcspot/spot/model/Global.kt
+++ b/core/model/src/main/java/com/umcspot/spot/model/Global.kt
@@ -48,13 +48,20 @@ enum class StudyTheme(
val title: String
) {
LANGUAGE("어학"),
- LICENSE("자격증"),
- EMPLOYMENT("취업"),
- DISCUSSION("토론"),
- NEWS("시사 / 뉴스"),
- SELFSTUDY("자율 학습"),
+ CERTIFICATION("자격증"),
+ CAREER("취업"),
+ DEBATE("토론"),
+ CURRENT_AFFAIRS("시사 / 뉴스"),
+ SELF_STUDY("자율 학습"),
PROJECT("프로젝트"),
- CONTEST("공모전"),
- MAJOR("전공 / 진로 학습"),
- ETC("기타")
+ COMPETITION("공모전"),
+ MAJOR_CAREER("전공 / 진로 학습"),
+ OTHER("기타")
+}
+
+enum class SocialLoginType(
+ val title: String
+) {
+ KAKAO("kakao"),
+ NAVER("naver"),
}
diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts
index 4e05fdbb..4aaee96f 100644
--- a/core/network/build.gradle.kts
+++ b/core/network/build.gradle.kts
@@ -9,9 +9,11 @@ android {
namespace = "com.umcspot.spot.network"
}
dependencies {
+
implementation(libs.bundles.datastore)
implementation(projects.core.common)
implementation(projects.core.model)
+ implementation(projects.core.datastore)
implementation(libs.kotlinx.serialization.json)
implementation(libs.retrofit.core)
implementation(libs.retrofit.kotlin.serialization)
diff --git a/core/network/src/main/java/com/umcspot/spot/network/AuthInterceptor.kt b/core/network/src/main/java/com/umcspot/spot/network/AuthInterceptor.kt
new file mode 100644
index 00000000..bc71e817
--- /dev/null
+++ b/core/network/src/main/java/com/umcspot/spot/network/AuthInterceptor.kt
@@ -0,0 +1,34 @@
+package com.umcspot.spot.network
+
+import androidx.datastore.core.DataStore
+import com.umcspot.spot.datastore.SpotTokenData
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import okhttp3.Interceptor
+import okhttp3.Response
+import javax.inject.Inject
+
+class AuthInterceptor @Inject constructor(
+ private val spotTokenDataStore: DataStore
+) : Interceptor {
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val original = chain.request()
+
+ val accessToken = runBlocking {
+ spotTokenDataStore.data.first().accessToken
+ }
+
+ // 토큰 없으면 그냥 원래 요청 진행
+ if (accessToken.isBlank()) {
+ return chain.proceed(original)
+ }
+
+ // 토큰 있으면 Authorization 헤더 추가
+ val newRequest = original.newBuilder()
+ .addHeader("Authorization", "Bearer $accessToken")
+ .build()
+
+ return chain.proceed(newRequest)
+ }
+}
diff --git a/core/network/src/main/java/com/umcspot/spot/network/di/NetworkModule.kt b/core/network/src/main/java/com/umcspot/spot/network/di/NetworkModule.kt
index 730a7247..4ced1415 100644
--- a/core/network/src/main/java/com/umcspot/spot/network/di/NetworkModule.kt
+++ b/core/network/src/main/java/com/umcspot/spot/network/di/NetworkModule.kt
@@ -2,11 +2,11 @@ package com.umcspot.spot.network.di
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.umcspot.spot.common.BuildConfigFieldProvider
+import com.umcspot.spot.network.AuthInterceptor
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
@@ -14,6 +14,7 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter
import retrofit2.Retrofit
+import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@@ -27,9 +28,13 @@ object NetworkModule {
@Provides
@Singleton
- fun providesOkHttpClient(loggingInterceptor: HttpLoggingInterceptor): OkHttpClient =
+ fun providesOkHttpClient(
+ loggingInterceptor: HttpLoggingInterceptor,
+ authInterceptor: AuthInterceptor
+ ): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
+ .addInterceptor(authInterceptor)
.build()
@OptIn(ExperimentalSerializationApi::class)
diff --git a/core/network/src/main/java/com/umcspot/spot/network/model/BaseResponse.kt b/core/network/src/main/java/com/umcspot/spot/network/model/BaseResponse.kt
index 87f66168..26c59dae 100644
--- a/core/network/src/main/java/com/umcspot/spot/network/model/BaseResponse.kt
+++ b/core/network/src/main/java/com/umcspot/spot/network/model/BaseResponse.kt
@@ -5,10 +5,22 @@ import kotlinx.serialization.Serializable
@Serializable
data class BaseResponse(
- @SerialName("success")
- val success: Boolean,
+ @SerialName("isSuccess")
+ val isSuccess: Boolean,
+ @SerialName("code")
+ val code : String,
@SerialName("message")
val message: String,
- @SerialName("data")
- val data: T
+ @SerialName("result")
+ val result: T
+)
+
+@Serializable
+data class NullResultResponse(
+ @SerialName("isSuccess")
+ val isSuccess: Boolean,
+ @SerialName("code")
+ val code : String,
+ @SerialName("message")
+ val message: String
)
\ No newline at end of file
diff --git a/data/alert/src/main/java/com/umcspot/spot/alert/repositoryimpl/AlertRepositoryImpl.kt b/data/alert/src/main/java/com/umcspot/spot/alert/repositoryimpl/AlertRepositoryImpl.kt
index eac1a5ce..37507538 100644
--- a/data/alert/src/main/java/com/umcspot/spot/alert/repositoryimpl/AlertRepositoryImpl.kt
+++ b/data/alert/src/main/java/com/umcspot/spot/alert/repositoryimpl/AlertRepositoryImpl.kt
@@ -13,7 +13,7 @@ class AlertRepositoryImpl @Inject constructor(
override suspend fun getAlerts(): Result =
runCatching {
val response = studyService.getAlerts()
- response.data.toDomainList()
+ response.result.toDomainList()
}.recoverCatching {
getAlertDummies()
}
@@ -24,7 +24,7 @@ class AlertRepositoryImpl @Inject constructor(
override suspend fun getAppliedAlerts(): Result =
runCatching {
val response = studyService.getAppliedAlerts()
- response.data.toDomainList()
+ response.result.toDomainList()
}.recoverCatching {
getAppliedAlertDummies()
}
diff --git a/data/board/src/main/java/com/umcspot/spot/board/repositoryimpl/BoardRepositoryImpl.kt b/data/board/src/main/java/com/umcspot/spot/board/repositoryimpl/BoardRepositoryImpl.kt
index faa9efa0..f850644d 100644
--- a/data/board/src/main/java/com/umcspot/spot/board/repositoryimpl/BoardRepositoryImpl.kt
+++ b/data/board/src/main/java/com/umcspot/spot/board/repositoryimpl/BoardRepositoryImpl.kt
@@ -15,7 +15,7 @@ class BoardRepositoryImpl @Inject constructor(
override suspend fun getTagBoardData(sortType: SortType): Result =
runCatching {
val res = boardService.getTagBoardInfo(sortType)
- res.data.toDomainList()
+ res.result.toDomainList()
}.recoverCatching {
rankedListDummies(sortType) // 태그 보드도 랭크 리스트 형태로 더미 복구
}
@@ -23,7 +23,7 @@ class BoardRepositoryImpl @Inject constructor(
override suspend fun getRankedBoardData(): Result =
runCatching {
val res = boardService.getRankedBoardInfo()
- res.data.toDomainList()
+ res.result.toDomainList()
}.recoverCatching {
rankedListDummies()
}
@@ -31,7 +31,7 @@ class BoardRepositoryImpl @Inject constructor(
override suspend fun getLabeledBoardData(): Result =
runCatching {
val res = boardService.getLabeledBoardInfo()
- res.data.toDomainList()
+ res.result.toDomainList()
}.recoverCatching {
labeledListDummies()
}
diff --git a/domain/signup/.gitignore b/data/login/.gitignore
similarity index 100%
rename from domain/signup/.gitignore
rename to data/login/.gitignore
diff --git a/data/login/build.gradle.kts b/data/login/build.gradle.kts
new file mode 100644
index 00000000..704ea29d
--- /dev/null
+++ b/data/login/build.gradle.kts
@@ -0,0 +1,15 @@
+plugins {
+ alias(libs.plugins.spot.data)
+}
+
+android {
+ namespace = "com.umcspot.spot.login"
+}
+dependencies {
+ implementation(projects.core.model)
+ implementation(projects.core.network)
+ implementation(projects.core.datastore)
+ implementation(projects.domain.token)
+
+ implementation(libs.datastore.core)
+}
diff --git a/data/login/consumer-rules.pro b/data/login/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/data/login/proguard-rules.pro b/data/login/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/data/login/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/data/login/src/main/AndroidManifest.xml b/data/login/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/data/login/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/data/login/src/main/java/com/umcspot/spot/login/datasource/LoginDataSource.kt b/data/login/src/main/java/com/umcspot/spot/login/datasource/LoginDataSource.kt
new file mode 100644
index 00000000..79494c1e
--- /dev/null
+++ b/data/login/src/main/java/com/umcspot/spot/login/datasource/LoginDataSource.kt
@@ -0,0 +1,10 @@
+package com.umcspot.spot.login.datasource
+
+import com.umcspot.spot.login.dto.response.TokenResponseDto
+import com.umcspot.spot.model.SocialLoginType
+import com.umcspot.spot.network.model.BaseResponse
+
+interface LoginDataSource {
+ suspend fun finishSocialLogin(type : String, accessToken : String): BaseResponse
+
+}
\ No newline at end of file
diff --git a/data/login/src/main/java/com/umcspot/spot/login/datasourceimpl/LoginDataSourceImpl.kt b/data/login/src/main/java/com/umcspot/spot/login/datasourceimpl/LoginDataSourceImpl.kt
new file mode 100644
index 00000000..29541bfe
--- /dev/null
+++ b/data/login/src/main/java/com/umcspot/spot/login/datasourceimpl/LoginDataSourceImpl.kt
@@ -0,0 +1,19 @@
+package com.umcspot.spot.login.datasourceimpl
+
+import androidx.datastore.core.DataStore
+import com.umcspot.spot.login.datasource.LoginDataSource
+import com.umcspot.spot.login.dto.response.TokenResponseDto
+import com.umcspot.spot.login.service.LoginService
+import com.umcspot.spot.network.model.BaseResponse
+import javax.inject.Inject
+
+class LoginDataSourceImpl @Inject constructor(
+ private val loginService: LoginService
+) : LoginDataSource {
+
+ override suspend fun finishSocialLogin(
+ type: String,
+ accessToken: String
+ ): BaseResponse =
+ loginService.getCallBackToken(type, accessToken)
+}
\ No newline at end of file
diff --git a/data/login/src/main/java/com/umcspot/spot/login/di/LoginDataModule.kt b/data/login/src/main/java/com/umcspot/spot/login/di/LoginDataModule.kt
new file mode 100644
index 00000000..7936f7c4
--- /dev/null
+++ b/data/login/src/main/java/com/umcspot/spot/login/di/LoginDataModule.kt
@@ -0,0 +1,17 @@
+package com.umcspot.spot.login.di
+
+import com.umcspot.spot.login.datasource.LoginDataSource
+import com.umcspot.spot.login.datasourceimpl.LoginDataSourceImpl
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class LoginDataModule {
+ @Binds
+ @Singleton
+ abstract fun bindDummyRemoteDataSource(impl: LoginDataSourceImpl): LoginDataSource
+}
\ No newline at end of file
diff --git a/data/login/src/main/java/com/umcspot/spot/login/di/LoginRepositoryModule.kt b/data/login/src/main/java/com/umcspot/spot/login/di/LoginRepositoryModule.kt
new file mode 100644
index 00000000..6ea1247f
--- /dev/null
+++ b/data/login/src/main/java/com/umcspot/spot/login/di/LoginRepositoryModule.kt
@@ -0,0 +1,17 @@
+package com.umcspot.spot.login.di
+
+import com.umcspot.spot.login.repositoryimpl.LoginRepositoryImpl
+import com.umcspot.spot.token.repository.TokenRepository
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class LoginRepositoryModule {
+ @Binds
+ @Singleton
+ abstract fun bindsDummyRepository(dummyRepositoryImpl: LoginRepositoryImpl): TokenRepository
+}
\ No newline at end of file
diff --git a/data/login/src/main/java/com/umcspot/spot/login/di/LoginServiceModule.kt b/data/login/src/main/java/com/umcspot/spot/login/di/LoginServiceModule.kt
new file mode 100644
index 00000000..4eb8fa62
--- /dev/null
+++ b/data/login/src/main/java/com/umcspot/spot/login/di/LoginServiceModule.kt
@@ -0,0 +1,19 @@
+package com.umcspot.spot.login.di
+
+import com.umcspot.spot.login.service.LoginService
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import retrofit2.Retrofit
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object LoginServiceModule {
+ @Provides
+ @Singleton
+ fun provideOAuthApi(retrofit: Retrofit): LoginService =
+ retrofit.create(LoginService::class.java)
+
+}
\ No newline at end of file
diff --git a/data/login/src/main/java/com/umcspot/spot/login/dto/request/TokenRequestDto.kt b/data/login/src/main/java/com/umcspot/spot/login/dto/request/TokenRequestDto.kt
new file mode 100644
index 00000000..7f2d3597
--- /dev/null
+++ b/data/login/src/main/java/com/umcspot/spot/login/dto/request/TokenRequestDto.kt
@@ -0,0 +1,11 @@
+package com.umcspot.spot.login.dto.request
+
+import com.umcspot.spot.model.SocialLoginType
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TokenRequestDto(
+ @SerialName("type")
+ val type : SocialLoginType
+)
diff --git a/data/login/src/main/java/com/umcspot/spot/login/dto/response/TokenResponseDto.kt b/data/login/src/main/java/com/umcspot/spot/login/dto/response/TokenResponseDto.kt
new file mode 100644
index 00000000..ebb9da6e
--- /dev/null
+++ b/data/login/src/main/java/com/umcspot/spot/login/dto/response/TokenResponseDto.kt
@@ -0,0 +1,14 @@
+package com.umcspot.spot.login.dto.response
+
+import android.annotation.SuppressLint
+import com.umcspot.spot.model.BoardType
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TokenResponseDto(
+ @SerialName("accessToken")
+ val accessToken: String,
+ @SerialName("refreshToken")
+ val refreshToken: String
+)
\ No newline at end of file
diff --git a/data/login/src/main/java/com/umcspot/spot/login/mapper/TokenMapper.kt b/data/login/src/main/java/com/umcspot/spot/login/mapper/TokenMapper.kt
new file mode 100644
index 00000000..e68648a7
--- /dev/null
+++ b/data/login/src/main/java/com/umcspot/spot/login/mapper/TokenMapper.kt
@@ -0,0 +1,17 @@
+package com.umcspot.spot.login.mapper
+
+import com.umcspot.spot.login.dto.request.TokenRequestDto
+import com.umcspot.spot.login.dto.response.TokenResponseDto
+import com.umcspot.spot.token.model.TokenResult
+import com.umcspot.spot.token.model.TokenType
+
+fun TokenType.toDto() : TokenRequestDto =
+ TokenRequestDto (
+ type = this.type
+ )
+
+fun TokenResponseDto.toDomain() : TokenResult =
+ TokenResult (
+ accessToken = this.accessToken,
+ refreshToken = this.refreshToken
+ )
diff --git a/data/login/src/main/java/com/umcspot/spot/login/repositoryimpl/LoginRepositoryImpl.kt b/data/login/src/main/java/com/umcspot/spot/login/repositoryimpl/LoginRepositoryImpl.kt
new file mode 100644
index 00000000..d16a11c6
--- /dev/null
+++ b/data/login/src/main/java/com/umcspot/spot/login/repositoryimpl/LoginRepositoryImpl.kt
@@ -0,0 +1,37 @@
+package com.umcspot.spot.login.repositoryimpl
+
+import androidx.datastore.core.DataStore
+import com.umcspot.spot.datastore.SpotTokenData
+import com.umcspot.spot.login.mapper.toDomain
+import com.umcspot.spot.login.service.LoginService
+import com.umcspot.spot.model.SocialLoginType
+import com.umcspot.spot.token.model.TokenResult
+import com.umcspot.spot.token.repository.TokenRepository
+import javax.inject.Inject
+
+class LoginRepositoryImpl @Inject constructor(
+ private val studyService: LoginService,
+ private val spotTokenDataStore: DataStore
+) : TokenRepository {
+
+ override suspend fun finishSocialLogin(
+ type: SocialLoginType,
+ accessToken: String
+ ): Result =
+ runCatching {
+ // 1) 소셜 로그인 콜백 토큰 서버로부터 받기
+ val response = studyService.getCallBackToken(type.title, accessToken)
+ val tokenResult: TokenResult = response.result.toDomain()
+
+ // 2) DataStore에 access / refresh 저장
+ spotTokenDataStore.updateData { current ->
+ current.copy(
+ accessToken = tokenResult.accessToken,
+ refreshToken = tokenResult.refreshToken
+ )
+ }
+
+ // 3) ViewModel 에서 추가 처리 필요하면 쓰라고 그대로 반환
+ tokenResult
+ }
+}
\ No newline at end of file
diff --git a/data/login/src/main/java/com/umcspot/spot/login/service/LoginService.kt b/data/login/src/main/java/com/umcspot/spot/login/service/LoginService.kt
new file mode 100644
index 00000000..947e19da
--- /dev/null
+++ b/data/login/src/main/java/com/umcspot/spot/login/service/LoginService.kt
@@ -0,0 +1,17 @@
+package com.umcspot.spot.login.service
+
+import com.umcspot.spot.login.dto.response.TokenResponseDto
+import com.umcspot.spot.network.model.BaseResponse
+import retrofit2.http.GET
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface LoginService {
+
+ @GET("/api/oauth/client/{type}")
+ suspend fun getCallBackToken(
+ @Path("type") type : String,
+ @Query("accessToken") accessToken : String
+ ) : BaseResponse
+
+}
diff --git a/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt b/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
index 7681fcc5..f093851f 100644
--- a/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
+++ b/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
@@ -16,7 +16,7 @@ class StudyRepositoryImpl @Inject constructor(
override suspend fun getPopularStudies(): Result =
runCatching {
val response = studyService.getPopularStudies()
- response.data.toDomainList()
+ response.result!!.toDomainList()
}.recoverCatching {
setPopularDummies()
}
@@ -28,7 +28,7 @@ class StudyRepositoryImpl @Inject constructor(
override suspend fun getRecommendStudies(): Result =
runCatching {
val response = studyService.getPopularStudies()
- response.data.toDomainList()
+ response.result!!.toDomainList()
}.recoverCatching {
setRecommendDummies()
}
@@ -40,7 +40,7 @@ class StudyRepositoryImpl @Inject constructor(
override suspend fun getRecruitingStudies(sortType : RecruitingStudySort, activityType: ActivityType?, theme: StudyTheme?, feeRange: FeeRange?): Result =
runCatching {
val response = studyService.getRecruitingStudies(sortType = sortType, activityType = activityType, theme = theme, feeRange = feeRange)
- response.data.toDomainList()
+ response.result!!.toDomainList()
}.recoverCatching {
setRecommendDummies(30)
}
@@ -48,7 +48,7 @@ class StudyRepositoryImpl @Inject constructor(
override suspend fun getPreferLocationStudies(sortType : RecruitingStudySort, activityType: ActivityType?, theme: StudyTheme?, feeRange: FeeRange?): Result =
runCatching {
val response = studyService.getRecruitingStudies(sortType = sortType, activityType = activityType, theme = theme, feeRange = feeRange)
- response.data.toDomainList()
+ response.result!!.toDomainList()
}.recoverCatching {
setRecommendDummies(0)
}
diff --git a/data/user/.gitignore b/data/user/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/data/user/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/data/user/build.gradle.kts b/data/user/build.gradle.kts
new file mode 100644
index 00000000..43cf3db4
--- /dev/null
+++ b/data/user/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins {
+ alias(libs.plugins.spot.data)
+}
+
+android {
+ namespace = "com.umcspot.spot.user"
+}
+dependencies {
+ implementation(projects.domain.user)
+}
\ No newline at end of file
diff --git a/data/user/consumer-rules.pro b/data/user/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/data/user/proguard-rules.pro b/data/user/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/data/user/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/data/user/src/main/AndroidManifest.xml b/data/user/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/data/user/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/data/user/src/main/java/com/umcspot/spot/user/datasource/UserDataSource.kt b/data/user/src/main/java/com/umcspot/spot/user/datasource/UserDataSource.kt
new file mode 100644
index 00000000..352abf67
--- /dev/null
+++ b/data/user/src/main/java/com/umcspot/spot/user/datasource/UserDataSource.kt
@@ -0,0 +1,17 @@
+package com.umcspot.spot.user.datasource
+
+import com.umcspot.spot.model.StudyTheme
+import com.umcspot.spot.network.model.BaseResponse
+import com.umcspot.spot.network.model.NullResultResponse
+import com.umcspot.spot.user.dto.request.UserNameRequestDto
+import com.umcspot.spot.user.dto.request.UserThemeRequestDto
+import com.umcspot.spot.user.dto.response.UserResponseDto
+import com.umcspot.spot.user.dto.response.UserThemeResponseDto
+
+interface UserDataSource {
+ suspend fun getUser(): BaseResponse
+
+ suspend fun setUserName(name : UserNameRequestDto) : NullResultResponse
+ suspend fun setUserTheme(themes : UserThemeRequestDto): NullResultResponse
+
+}
\ No newline at end of file
diff --git a/data/user/src/main/java/com/umcspot/spot/user/datasourceimpl/UserDataSourceImpl.kt b/data/user/src/main/java/com/umcspot/spot/user/datasourceimpl/UserDataSourceImpl.kt
new file mode 100644
index 00000000..f5abb230
--- /dev/null
+++ b/data/user/src/main/java/com/umcspot/spot/user/datasourceimpl/UserDataSourceImpl.kt
@@ -0,0 +1,34 @@
+package com.umcspot.spot.user.datasourceimpl
+
+import com.umcspot.spot.model.StudyTheme
+import com.umcspot.spot.network.model.BaseResponse
+import com.umcspot.spot.network.model.NullResultResponse
+import com.umcspot.spot.user.datasource.UserDataSource
+import com.umcspot.spot.user.dto.request.UserNameRequestDto
+import com.umcspot.spot.user.dto.request.UserThemeRequestDto
+import com.umcspot.spot.user.dto.response.UserResponseDto
+import com.umcspot.spot.user.dto.response.UserThemeResponseDto
+import com.umcspot.spot.user.service.UserService
+import javax.inject.Inject
+
+
+class UserDataSourceImpl @Inject constructor(
+ private val userService: UserService
+) : UserDataSource {
+
+ override suspend fun getUser(
+
+ ): BaseResponse =
+ userService.getUser()
+
+ override suspend fun setUserName(
+ name : UserNameRequestDto
+ ): NullResultResponse =
+ userService.setUserName(name)
+
+ override suspend fun setUserTheme(
+ themes: UserThemeRequestDto
+ ): NullResultResponse =
+ userService.setUserTheme(themes)
+
+}
\ No newline at end of file
diff --git a/data/user/src/main/java/com/umcspot/spot/user/di/UserDataModule.kt b/data/user/src/main/java/com/umcspot/spot/user/di/UserDataModule.kt
new file mode 100644
index 00000000..8c3cfdbc
--- /dev/null
+++ b/data/user/src/main/java/com/umcspot/spot/user/di/UserDataModule.kt
@@ -0,0 +1,18 @@
+package com.umcspot.spot.user.di
+
+import com.umcspot.spot.user.datasource.UserDataSource
+import com.umcspot.spot.user.datasourceimpl.UserDataSourceImpl
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class UserDataModule {
+ @Binds
+ @Singleton
+ abstract fun bindDummyRemoteDataSource(impl: UserDataSourceImpl): UserDataSource
+}
\ No newline at end of file
diff --git a/data/user/src/main/java/com/umcspot/spot/user/di/UserRepositoryModule.kt b/data/user/src/main/java/com/umcspot/spot/user/di/UserRepositoryModule.kt
new file mode 100644
index 00000000..3007aec4
--- /dev/null
+++ b/data/user/src/main/java/com/umcspot/spot/user/di/UserRepositoryModule.kt
@@ -0,0 +1,17 @@
+package com.umcspot.spot.user.di
+
+import com.umcspot.spot.user.repository.UserRepository
+import com.umcspot.spot.user.repositoryimpl.UserRepositoryImpl
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class UserRepositoryModule {
+ @Binds
+ @Singleton
+ abstract fun bindsDummyRepository(impl: UserRepositoryImpl): UserRepository
+}
\ No newline at end of file
diff --git a/data/user/src/main/java/com/umcspot/spot/user/di/UserServiceModule.kt b/data/user/src/main/java/com/umcspot/spot/user/di/UserServiceModule.kt
new file mode 100644
index 00000000..0bbee0e7
--- /dev/null
+++ b/data/user/src/main/java/com/umcspot/spot/user/di/UserServiceModule.kt
@@ -0,0 +1,19 @@
+package com.umcspot.spot.user.di
+
+import com.umcspot.spot.user.service.UserService
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import retrofit2.Retrofit
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object UserServiceModule {
+ @Provides
+ @Singleton
+ fun providesUserService(retrofit: Retrofit): UserService = retrofit.create(
+ UserService::class.java
+ )
+}
\ No newline at end of file
diff --git a/data/user/src/main/java/com/umcspot/spot/user/dto/request/UserRequestDto.kt b/data/user/src/main/java/com/umcspot/spot/user/dto/request/UserRequestDto.kt
new file mode 100644
index 00000000..52f1530d
--- /dev/null
+++ b/data/user/src/main/java/com/umcspot/spot/user/dto/request/UserRequestDto.kt
@@ -0,0 +1,17 @@
+package com.umcspot.spot.user.dto.request
+
+import com.umcspot.spot.model.StudyTheme
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class UserThemeRequestDto(
+ @SerialName("userThemes")
+ val userThemes: List
+)
+
+@Serializable
+data class UserNameRequestDto(
+ @SerialName("name")
+ val name: String
+)
\ No newline at end of file
diff --git a/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserResponseDto.kt b/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserResponseDto.kt
new file mode 100644
index 00000000..17dcc2f2
--- /dev/null
+++ b/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserResponseDto.kt
@@ -0,0 +1,13 @@
+package com.umcspot.spot.user.dto.response
+
+import android.annotation.SuppressLint
+import com.umcspot.spot.model.WeatherType
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@SuppressLint("UnsafeOptInUsageError")
+@Serializable
+data class UserResponseDto(
+ @SerialName("name")
+ val name: String
+)
\ No newline at end of file
diff --git a/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserThemeResponseDto.kt b/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserThemeResponseDto.kt
new file mode 100644
index 00000000..37b950a7
--- /dev/null
+++ b/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserThemeResponseDto.kt
@@ -0,0 +1,13 @@
+package com.umcspot.spot.user.dto.response
+
+import android.annotation.SuppressLint
+import com.umcspot.spot.model.StudyTheme
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@SuppressLint("UnsafeOptInUsageError")
+@Serializable
+data class UserThemeResponseDto (
+ @SerialName("userThemes")
+ val userThemes : List
+)
\ No newline at end of file
diff --git a/data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt b/data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt
new file mode 100644
index 00000000..30882c80
--- /dev/null
+++ b/data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt
@@ -0,0 +1,31 @@
+package com.umcspot.spot.user.mapper
+
+import com.umcspot.spot.model.StudyTheme
+import com.umcspot.spot.user.dto.request.UserNameRequestDto
+import com.umcspot.spot.user.dto.request.UserThemeRequestDto
+import com.umcspot.spot.user.model.UserResult
+import com.umcspot.spot.user.dto.response.UserResponseDto
+import com.umcspot.spot.user.dto.response.UserThemeResponseDto
+import com.umcspot.spot.user.model.UserTheme
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import java.time.format.DateTimeParseException
+import java.util.Locale
+
+fun List.toRequestDto(): UserThemeRequestDto =
+ UserThemeRequestDto(userThemes = this)
+
+fun String.toRequestDto(): UserNameRequestDto =
+ UserNameRequestDto(name = this)
+
+// DTO -> Domain
+fun UserResponseDto.toDomain(): UserResult =
+ UserResult(
+ name = this.name
+ )
+
+fun UserThemeResponseDto.toDomain(): UserTheme =
+ UserTheme(
+ userThemes = userThemes
+ )
+
diff --git a/data/user/src/main/java/com/umcspot/spot/user/repositoryimpl/UserRepositoryImpl.kt b/data/user/src/main/java/com/umcspot/spot/user/repositoryimpl/UserRepositoryImpl.kt
new file mode 100644
index 00000000..a6393a53
--- /dev/null
+++ b/data/user/src/main/java/com/umcspot/spot/user/repositoryimpl/UserRepositoryImpl.kt
@@ -0,0 +1,33 @@
+package com.umcspot.spot.user.repositoryimpl
+
+import com.umcspot.spot.model.StudyTheme
+import com.umcspot.spot.user.mapper.toDomain
+import com.umcspot.spot.user.mapper.toRequestDto
+import com.umcspot.spot.user.model.UserResult
+import com.umcspot.spot.user.model.UserTheme
+import com.umcspot.spot.user.repository.UserRepository
+import com.umcspot.spot.user.service.UserService
+import javax.inject.Inject
+
+class UserRepositoryImpl @Inject constructor(
+ private val userService: UserService
+) : UserRepository {
+ override suspend fun getUserName(): Result =
+ runCatching {
+ val userName = userService.getUser()
+ userName.result.toDomain()
+ }.recoverCatching {
+ UserResult(name = "123")
+ }
+
+ override suspend fun setUserName(name: String): Result =
+ runCatching {
+ userService.setUserName(name.toRequestDto())
+ }
+
+
+ override suspend fun setUserTheme(theme: List): Result =
+ runCatching {
+ userService.setUserTheme(theme.toRequestDto())
+ }
+}
\ No newline at end of file
diff --git a/data/user/src/main/java/com/umcspot/spot/user/service/UserService.kt b/data/user/src/main/java/com/umcspot/spot/user/service/UserService.kt
new file mode 100644
index 00000000..f357bb36
--- /dev/null
+++ b/data/user/src/main/java/com/umcspot/spot/user/service/UserService.kt
@@ -0,0 +1,26 @@
+package com.umcspot.spot.user.service
+
+import com.umcspot.spot.network.model.BaseResponse
+import com.umcspot.spot.network.model.NullResultResponse
+import com.umcspot.spot.user.dto.request.UserNameRequestDto
+import com.umcspot.spot.user.dto.request.UserThemeRequestDto
+import com.umcspot.spot.user.dto.response.UserResponseDto
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+
+interface UserService {
+ @GET("/api/members/name")
+ suspend fun getUser(
+ ): BaseResponse
+
+ @POST("/api/members/name")
+ suspend fun setUserName(
+ @Body request : UserNameRequestDto
+ ): NullResultResponse
+
+ @POST("/api/members/preferred-categories")
+ suspend fun setUserTheme(
+ @Body request : UserThemeRequestDto
+ ): NullResultResponse
+}
\ No newline at end of file
diff --git a/data/weather/src/main/java/com/umcspot/spot/weather/repositoryimpl/WeatherRepositoryImpl.kt b/data/weather/src/main/java/com/umcspot/spot/weather/repositoryimpl/WeatherRepositoryImpl.kt
index 6857dbb6..2aed3747 100644
--- a/data/weather/src/main/java/com/umcspot/spot/weather/repositoryimpl/WeatherRepositoryImpl.kt
+++ b/data/weather/src/main/java/com/umcspot/spot/weather/repositoryimpl/WeatherRepositoryImpl.kt
@@ -14,7 +14,7 @@ class WeatherRepositoryImpl @Inject constructor(
override suspend fun getWeather(request: Weather): Result =
runCatching {
val response = weatherService.getWeather(request.toData())
- response.data.toDomain()
+ response.result.toDomain()
}.recoverCatching {
// API 미연결/예외 시 더미로 복구
WeatherResult.dummyFrom()
diff --git a/domain/token/.gitignore b/domain/token/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/domain/token/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/domain/signup/build.gradle.kts b/domain/token/build.gradle.kts
similarity index 98%
rename from domain/signup/build.gradle.kts
rename to domain/token/build.gradle.kts
index 14f8d7b6..e56aaf9a 100644
--- a/domain/signup/build.gradle.kts
+++ b/domain/token/build.gradle.kts
@@ -1,6 +1,8 @@
plugins {
alias(libs.plugins.spot.android.java.library)
}
+
+
dependencies {
implementation(projects.core.model)
implementation(libs.bundles.coroutine)
diff --git a/domain/token/consumer-rules.pro b/domain/token/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/domain/token/proguard-rules.pro b/domain/token/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/domain/token/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/domain/token/src/main/java/com/umcspot/spot/token/model/TokenResult.kt b/domain/token/src/main/java/com/umcspot/spot/token/model/TokenResult.kt
new file mode 100644
index 00000000..a0490372
--- /dev/null
+++ b/domain/token/src/main/java/com/umcspot/spot/token/model/TokenResult.kt
@@ -0,0 +1,6 @@
+package com.umcspot.spot.token.model
+
+data class TokenResult (
+ val accessToken : String,
+ val refreshToken : String
+)
\ No newline at end of file
diff --git a/domain/token/src/main/java/com/umcspot/spot/token/model/Tokentype.kt b/domain/token/src/main/java/com/umcspot/spot/token/model/Tokentype.kt
new file mode 100644
index 00000000..ae06e8d9
--- /dev/null
+++ b/domain/token/src/main/java/com/umcspot/spot/token/model/Tokentype.kt
@@ -0,0 +1,7 @@
+package com.umcspot.spot.token.model
+
+import com.umcspot.spot.model.SocialLoginType
+
+data class TokenType (
+ val type : SocialLoginType
+)
\ No newline at end of file
diff --git a/domain/token/src/main/java/com/umcspot/spot/token/repository/TokenRepository.kt b/domain/token/src/main/java/com/umcspot/spot/token/repository/TokenRepository.kt
new file mode 100644
index 00000000..7df52445
--- /dev/null
+++ b/domain/token/src/main/java/com/umcspot/spot/token/repository/TokenRepository.kt
@@ -0,0 +1,8 @@
+package com.umcspot.spot.token.repository
+
+import com.umcspot.spot.model.SocialLoginType
+import com.umcspot.spot.token.model.TokenResult
+
+interface TokenRepository {
+ suspend fun finishSocialLogin(type : SocialLoginType, accessToken : String) : Result
+}
\ No newline at end of file
diff --git a/domain/user/.gitignore b/domain/user/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/domain/user/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/domain/user/build.gradle.kts b/domain/user/build.gradle.kts
new file mode 100644
index 00000000..58a06150
--- /dev/null
+++ b/domain/user/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins {
+ alias(libs.plugins.spot.android.java.library)
+}
+
+
+
+dependencies {
+ implementation(projects.core.model)
+ implementation(libs.bundles.coroutine)
+}
\ No newline at end of file
diff --git a/domain/user/src/main/java/com/umcspot/spot/user/model/UserResult.kt b/domain/user/src/main/java/com/umcspot/spot/user/model/UserResult.kt
new file mode 100644
index 00000000..b1fe7d50
--- /dev/null
+++ b/domain/user/src/main/java/com/umcspot/spot/user/model/UserResult.kt
@@ -0,0 +1,11 @@
+package com.umcspot.spot.user.model
+
+import com.umcspot.spot.model.StudyTheme
+
+data class UserResult (
+ val name : String
+)
+
+data class UserTheme(
+ val userThemes : List
+)
\ No newline at end of file
diff --git a/domain/user/src/main/java/com/umcspot/spot/user/model/UserTheme.kt b/domain/user/src/main/java/com/umcspot/spot/user/model/UserTheme.kt
new file mode 100644
index 00000000..cff7af27
--- /dev/null
+++ b/domain/user/src/main/java/com/umcspot/spot/user/model/UserTheme.kt
@@ -0,0 +1,7 @@
+package com.umcspot.spot.user.model
+
+import com.umcspot.spot.model.StudyTheme
+
+data class StudyThemeRequestBody(
+ val categories : List
+)
\ No newline at end of file
diff --git a/domain/user/src/main/java/com/umcspot/spot/user/repository/UserRepository.kt b/domain/user/src/main/java/com/umcspot/spot/user/repository/UserRepository.kt
new file mode 100644
index 00000000..6fdcac22
--- /dev/null
+++ b/domain/user/src/main/java/com/umcspot/spot/user/repository/UserRepository.kt
@@ -0,0 +1,11 @@
+package com.umcspot.spot.user.repository
+
+import com.umcspot.spot.model.StudyTheme
+import com.umcspot.spot.user.model.UserResult
+import com.umcspot.spot.user.model.UserTheme
+
+interface UserRepository {
+ suspend fun getUserName() : Result
+ suspend fun setUserName(name : String) : Result
+ suspend fun setUserTheme(theme : List) : Result
+}
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainActivity.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainActivity.kt
index 111c6531..c61949df 100644
--- a/feature/main/src/main/java/com/umcspot/spot/main/MainActivity.kt
+++ b/feature/main/src/main/java/com/umcspot/spot/main/MainActivity.kt
@@ -1,15 +1,21 @@
package com.umcspot.spot.main
+import android.content.Intent
import android.graphics.Color
+import android.net.Uri
import android.os.Bundle
+import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.activity.viewModels
import androidx.core.view.WindowCompat
import com.umcspot.spot.designsystem.theme.SpotTheme
-import com.umcspot.spot.landing.LandingScreen
+import com.umcspot.spot.landing.LandingViewModel
+import com.umcspot.spot.landing.navigation.Landing
import dagger.hilt.android.AndroidEntryPoint
+import kotlin.getValue
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@@ -21,6 +27,7 @@ class MainActivity : ComponentActivity() {
statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.dark(Color.TRANSPARENT)
)
+
setContent {
SpotTheme {
MainScreen()
diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt
index 81837466..d754b140 100644
--- a/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt
+++ b/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt
@@ -7,15 +7,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
+import androidx.navigation.navOptions
import com.umcspot.spot.alert.navigation.alertGraph
import com.umcspot.spot.alert.navigation.appliedAlertGraph
import com.umcspot.spot.category.navigation.categoryGraph
+import com.umcspot.spot.checkList.navigation.checkListGraph
import com.umcspot.spot.feature.board.navigation.boardGraph
import com.umcspot.spot.home.navigation.homeGraph
import com.umcspot.spot.jjim.navigation.jjimGraph
-import com.umcspot.spot.landing.landingGraph
+import com.umcspot.spot.landing.navigation.landingGraph
+import com.umcspot.spot.landing.navigation.navigateToSaving
+import com.umcspot.spot.landing.navigation.savingGraph
import com.umcspot.spot.model.QuickMenuType
import com.umcspot.spot.mypage.navigation.mypageGraph
+import com.umcspot.spot.signup.navigation.signupGraph
import com.umcspot.spot.study.my.navigation.myStudyGraph
import com.umcspot.spot.study.preferLocation.navigation.preferLocationStudyGraph
import com.umcspot.spot.study.recruiting.navigation.recruitingStudyFilterGraph
@@ -29,6 +34,11 @@ fun MainNavHost(
contentPadding: PaddingValues = PaddingValues(0.dp),
onRegisterScrollToTop: ((() -> Unit)?) -> Unit,
) {
+ val clearStackNavOptions = navOptions {
+ popUpTo(0) { inclusive = true }
+ launchSingleTop = true
+ restoreState = false
+ }
NavHost(
navController = navigator.navController,
startDestination = navigator.startDestination,
@@ -39,11 +49,27 @@ fun MainNavHost(
popExitTransition = { ExitTransition.None },
) {
landingGraph(
- onKakaoClick = {
- navigator.navigateToHomeAfterLogin()
- },
- onNaverClick = {
- navigator.navigateToHomeAfterLogin()
+ onLoginSuccess = {
+ navigator.navigateToSignUp(clearStackNavOptions)
+ }
+ )
+
+ signupGraph(
+ contentPadding = contentPadding,
+ navController = navigator.navController,
+ onNextClick = { navigator.navigateToCheckList() }
+ )
+
+ checkListGraph(
+ contentPadding = contentPadding,
+ navController = navigator.navController,
+ onNextClick = { navigator.navController.navigateToSaving() }
+ )
+
+ savingGraph(
+ contentPadding = contentPadding,
+ onFinished = {
+ navigator.navigateToHome(clearStackNavOptions)
}
)
diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt
index 48fffbf0..6fbcbe63 100644
--- a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt
+++ b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt
@@ -15,14 +15,19 @@ import com.umcspot.spot.alert.navigation.AppliedAlert
import com.umcspot.spot.alert.navigation.navigateToAlert
import com.umcspot.spot.alert.navigation.navigateToAppliedAlert
import com.umcspot.spot.category.navigation.navigateToCategory
+import com.umcspot.spot.checkList.navigation.CheckList
+import com.umcspot.spot.checkList.navigation.navigateToCheckList
import com.umcspot.spot.feature.board.navigation.Board
import com.umcspot.spot.feature.board.navigation.navigateToBoard
import com.umcspot.spot.home.navigation.navigateToHome
import com.umcspot.spot.jjim.navigation.navigateToJJim
-import com.umcspot.spot.landing.Landing
+import com.umcspot.spot.landing.navigation.Landing
+import com.umcspot.spot.landing.navigation.Saving
import com.umcspot.spot.mypage.navigation.navigateToMypage
-import com.umcspot.spot.study.recruiting.navigation.Recruiting
+import com.umcspot.spot.signup.navigation.SignUp
+import com.umcspot.spot.signup.navigation.navigateToSignUp
import com.umcspot.spot.study.my.navigation.navigateToMyStudy
+import com.umcspot.spot.study.recruiting.navigation.Recruiting
import com.umcspot.spot.study.preferLocation.navigation.PreferLocation
import com.umcspot.spot.study.preferLocation.navigation.navigateToPreferLocationStudy
import com.umcspot.spot.study.recruiting.navigation.RecruitingFilter
@@ -76,21 +81,15 @@ class MainNavigator(
}
@Composable
- fun isInLanding(): Boolean = inAnyGraph(Landing::class)
+ fun isInLanding(): Boolean = inAnyGraph(Landing::class, Saving::class)
- /** 상단 뒤로가기 TopBar 노출 조건 */
@Composable
- fun showBackTopBar(): Boolean =
- inAnyGraph(Alert::class, AppliedAlert::class, RecruitingFilter::class, RegisterStudy::class)
+ fun showBackTopBar(): Boolean = inAnyGraph(Alert::class, AppliedAlert::class, RecruitingFilter::class,
+ SignUp::class, CheckList::class,RegisterStudy::class)
- /** 스크롤-투-탑 FAB 노출 조건 */
@Composable
- fun showToTopFab(): Boolean = inAnyGraph(
- Alert::class, AppliedAlert::class, Recruiting::class,
- PreferLocation::class,
- )
+ fun showToTopFab(): Boolean = inAnyGraph(Alert::class, AppliedAlert::class, Recruiting::class,PreferLocation::class)
- /** 멀티 FAB(게시판 등) 노출 조건 */
@Composable
fun showMultipleFab(): Boolean = inAnyGraph(Board::class)
@@ -110,7 +109,6 @@ class MainNavigator(
return hierarchy.any { h -> graphs.any { k -> h.hasRoute(k) } }
}
-
fun navigateUp() {
navController.navigateUp()
}
@@ -119,6 +117,14 @@ class MainNavigator(
navController.popBackStack()
}
+ fun navigateToSignUp(navOptions: NavOptions? = null) {
+ navController.navigateToSignUp(navOptions)
+ }
+
+ fun navigateToCheckList(navOptions: NavOptions? = null) {
+ navController.navigateToCheckList(navOptions)
+ }
+
fun navigateToHome(navOptions: NavOptions? = null) {
navController.navigateToHome(navOptions)
}
diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt
index d5c03879..2e8e25a6 100644
--- a/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt
+++ b/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt
@@ -23,11 +23,13 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import com.umcspot.spot.alert.navigation.Alert
import com.umcspot.spot.alert.navigation.AppliedAlert
import com.umcspot.spot.alert.navigation.navigateToAlert
+import com.umcspot.spot.checkList.navigation.CheckList
import com.umcspot.spot.designsystem.component.FloatingMultipleButton
import com.umcspot.spot.designsystem.component.FloatingToUpButton
import com.umcspot.spot.designsystem.component.appBar.AppBarHome
import com.umcspot.spot.designsystem.component.appBar.BackTopBar
import com.umcspot.spot.main.component.MainBottomBar
+import com.umcspot.spot.signup.navigation.SignUp
import com.umcspot.spot.study.recruiting.navigation.RecruitingFilter
import com.umcspot.spot.study.register.navigation.RegisterStudy
import kotlinx.collections.immutable.toImmutableList
@@ -51,6 +53,8 @@ fun MainScreen(
dest?.hasRoute(Alert::class) == true -> "알림"
dest?.hasRoute(AppliedAlert::class) == true -> "신청한 알림"
dest?.hasRoute(RecruitingFilter::class) == true -> "모집중인 스터디"
+ dest?.hasRoute(SignUp::class) == true -> "회원가입"
+ dest?.hasRoute(CheckList::class) == true -> "체크리스트"
else -> ""
}
BackTopBar(
@@ -74,11 +78,9 @@ fun MainScreen(
FabStack(
showToTop = navigator.showToTopFab(),
onClickToTop = { scrollToTop?.invoke() },
-
showMultiple = navigator.showMultipleFab(),
onClickMultiple = { /* TODO */ },
-
- spacing = 12.dp,
+ spacing = 12.dp,
)
},
bottomBar = {
diff --git a/feature/signup/build.gradle.kts b/feature/signup/build.gradle.kts
index b14eeda7..79939de2 100644
--- a/feature/signup/build.gradle.kts
+++ b/feature/signup/build.gradle.kts
@@ -3,5 +3,21 @@ plugins {
}
android {
- namespace = "com.umcspot.spot.signup"
+ namespace = "com.umcspot.spot.user"
+}
+
+dependencies {
+ implementation(projects.domain.token)
+ implementation(projects.core.designsystem)
+ implementation(projects.domain.user)
+
+ implementation(libs.naver.oauth)
+ implementation(libs.androidx.browser) // jdk 17
+
+ implementation(libs.kakao.login)
+ implementation(libs.kakao.auth)
+ implementation(libs.kakao.common)
+
+ implementation(libs.naver.oauth)
+// implementation(libs.naver.jdk)
}
\ No newline at end of file
diff --git a/feature/signup/src/main/java/com/umcspot/spot/checkList/CheckListScreen.kt b/feature/signup/src/main/java/com/umcspot/spot/checkList/CheckListScreen.kt
new file mode 100644
index 00000000..d256a731
--- /dev/null
+++ b/feature/signup/src/main/java/com/umcspot/spot/checkList/CheckListScreen.kt
@@ -0,0 +1,67 @@
+package com.umcspot.spot.checkList
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.umcspot.spot.designsystem.component.button.TextButton
+import com.umcspot.spot.designsystem.component.study.section.ActivityThemeSection
+import com.umcspot.spot.designsystem.theme.SpotTheme
+import com.umcspot.spot.signup.SignUpViewModel
+
+@Composable
+fun CheckListScreen(
+ contentPadding: PaddingValues,
+ onNextClick: () -> Unit,
+ signUpViewModel: SignUpViewModel = hiltViewModel(),
+ viewmodel: CheckListViewModel = hiltViewModel()
+) {
+ val topPad = contentPadding.calculateTopPadding()
+ val bottomPad = contentPadding.calculateBottomPadding()
+
+
+ val themes by viewmodel.themes.collectAsStateWithLifecycle()
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(SpotTheme.colors.white)
+ .padding(top = topPad, start = 14.dp, end = 14.dp, bottom = bottomPad),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(Modifier.height(70.dp))
+
+ Text(
+ text = "내가 원하는 스터디를 선택해주세요",
+ style = SpotTheme.typography.h3,
+ color = SpotTheme.colors.black
+ )
+
+ Spacer(Modifier.height(50.dp))
+
+ ActivityThemeSection(
+ selectedThemes = themes,
+ onSelect = viewmodel::toggleTheme,
+ modifier = Modifier.fillMaxWidth(),
+ maxSelection = 10
+ )
+
+ Spacer(Modifier.weight(1f))
+
+ TextButton(
+ text = "다음",
+ enabled = themes.isNotEmpty(),
+ onClick = {
+ signUpViewModel.saveNameIfChanged()
+ viewmodel.submitThemes()
+ onNextClick()
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/signup/src/main/java/com/umcspot/spot/checkList/CheckListViewModel.kt b/feature/signup/src/main/java/com/umcspot/spot/checkList/CheckListViewModel.kt
new file mode 100644
index 00000000..d116a036
--- /dev/null
+++ b/feature/signup/src/main/java/com/umcspot/spot/checkList/CheckListViewModel.kt
@@ -0,0 +1,63 @@
+package com.umcspot.spot.checkList
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.umcspot.spot.model.StudyTheme
+import com.umcspot.spot.user.repository.UserRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.collections.immutable.PersistentList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toPersistentList
+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 javax.inject.Inject
+
+@HiltViewModel
+class CheckListViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ private val userRepository: UserRepository
+) : ViewModel() {
+
+ private val KEY_THEMES = "recruit_filter_themes"
+ private val MAX_SELECTION_COUNT = 10
+
+ private val _themes = MutableStateFlow>(
+ savedStateHandle.get>(KEY_THEMES)?.toPersistentList() ?: persistentListOf()
+ )
+
+ val themes: StateFlow> = _themes.asStateFlow()
+
+ fun toggleTheme(theme: StudyTheme) {
+ _themes.update { currentList ->
+
+ val newList = if (currentList.contains(theme)) {
+ currentList.remove(theme)
+ } else {
+ if (currentList.size < MAX_SELECTION_COUNT) {
+ currentList.add(theme)
+ } else {
+ currentList
+ }
+ }
+ saveToHandle(newList)
+ newList
+ }
+ }
+
+ private fun saveToHandle(list: List) {
+ savedStateHandle[KEY_THEMES] = ArrayList(list)
+ }
+
+ fun submitThemes() {
+ val selected = _themes.value
+ if (selected.isEmpty()) return
+
+ viewModelScope.launch {
+ userRepository.setUserTheme(selected)
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/signup/src/main/java/com/umcspot/spot/checkList/navigation/CheckListNavigation.kt b/feature/signup/src/main/java/com/umcspot/spot/checkList/navigation/CheckListNavigation.kt
new file mode 100644
index 00000000..7e3d46a8
--- /dev/null
+++ b/feature/signup/src/main/java/com/umcspot/spot/checkList/navigation/CheckListNavigation.kt
@@ -0,0 +1,41 @@
+package com.umcspot.spot.checkList.navigation
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.remember
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavHostController
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
+import com.umcspot.spot.checkList.CheckListScreen
+import com.umcspot.spot.navigation.Route
+import com.umcspot.spot.signup.SignUpViewModel
+import kotlinx.serialization.Serializable
+
+fun NavController.navigateToCheckList(navOptions: NavOptions? = null) {
+ navigate(CheckList, navOptions)
+}
+
+fun NavGraphBuilder.checkListGraph(
+ navController: NavHostController,
+ contentPadding : PaddingValues,
+ onNextClick: () -> Unit,
+) {
+ composable { backStackEntry ->
+
+ val parentEntry = remember(backStackEntry) {
+ navController.getBackStackEntry(navController.graph.id)
+ }
+ val signUpViewModel: SignUpViewModel = hiltViewModel(parentEntry)
+
+ CheckListScreen(
+ contentPadding = contentPadding,
+ signUpViewModel= signUpViewModel,
+ onNextClick = onNextClick
+ )
+ }
+}
+
+@Serializable
+data object CheckList : Route
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/umcspot/spot/landing/LandingScreen.kt b/feature/signup/src/main/java/com/umcspot/spot/landing/LandingScreen.kt
similarity index 70%
rename from feature/main/src/main/java/com/umcspot/spot/landing/LandingScreen.kt
rename to feature/signup/src/main/java/com/umcspot/spot/landing/LandingScreen.kt
index cb307cc2..e73a0a0d 100644
--- a/feature/main/src/main/java/com/umcspot/spot/landing/LandingScreen.kt
+++ b/feature/signup/src/main/java/com/umcspot/spot/landing/LandingScreen.kt
@@ -1,5 +1,6 @@
package com.umcspot.spot.landing
+import android.app.Activity
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -14,24 +15,61 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
import com.umcspot.spot.designsystem.R
import com.umcspot.spot.designsystem.component.button.KakaoStartButton
import com.umcspot.spot.designsystem.component.button.NaverStartButton
import com.umcspot.spot.designsystem.theme.B500
import com.umcspot.spot.designsystem.theme.SpotTheme
+import com.umcspot.spot.model.SocialLoginType
@Composable
fun LandingScreen(
- onKakaoClick : () -> Unit,
- onNaverClick : () -> Unit
+ onLoginSuccess: () -> Unit,
+ viewModel: LandingViewModel = hiltViewModel(),
+) {
+ val activity = LocalContext.current as? Activity
+
+ LaunchedEffect(Unit) {
+ viewModel.events.collect { ev ->
+ when (ev) {
+ is LandingViewModel.LoginEvent.LoginSucceeded -> {
+ onLoginSuccess()
+ }
+ is LandingViewModel.LoginEvent.ShowError -> {
+ }
+ }
+ }
+ }
+
+ LandingScreenContent(
+ onKakaoClick = {
+ activity?.let { act ->
+ viewModel.startSocialLogin(SocialLoginType.KAKAO, act)
+ }
+ },
+ onNaverClick = {
+ activity?.let { act ->
+ viewModel.startSocialLogin(SocialLoginType.NAVER, act)
+ }
+ },
+ )
+}
+
+@Composable
+fun LandingScreenContent(
+ onKakaoClick: () -> Unit,
+ onNaverClick: () -> Unit,
) {
Surface(
modifier = Modifier
@@ -46,7 +84,6 @@ fun LandingScreen(
.padding(horizontal = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
- // 상단 여백 + 중앙 컨텐츠
Spacer(Modifier.height(48.dp))
Column(
@@ -73,19 +110,14 @@ fun LandingScreen(
)
}
- // 하단 액션 버튼들
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 60.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
- KakaoStartButton(
- onClick = onKakaoClick
- )
- NaverStartButton(
- onClick = onNaverClick
- )
+ KakaoStartButton(onClick = onKakaoClick)
+ NaverStartButton(onClick = onNaverClick)
}
}
}
@@ -93,9 +125,9 @@ fun LandingScreen(
@Preview(showBackground = true)
@Composable
-fun LandingScreenPreview() {
+private fun LandingScreenPreview() {
SpotTheme {
- LandingScreen(
+ LandingScreenContent(
onKakaoClick = {},
onNaverClick = {}
)
diff --git a/feature/signup/src/main/java/com/umcspot/spot/landing/LoginViewmodel.kt b/feature/signup/src/main/java/com/umcspot/spot/landing/LoginViewmodel.kt
new file mode 100644
index 00000000..36c0b276
--- /dev/null
+++ b/feature/signup/src/main/java/com/umcspot/spot/landing/LoginViewmodel.kt
@@ -0,0 +1,122 @@
+package com.umcspot.spot.landing
+
+import android.app.Activity
+import android.content.ContentValues.TAG
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.util.Log
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kakao.sdk.user.UserApiClient
+import com.navercorp.nid.NidOAuth
+import com.navercorp.nid.oauth.util.NidOAuthCallback
+import com.umcspot.spot.model.SocialLoginType
+import com.umcspot.spot.token.repository.TokenRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class LandingViewModel @Inject constructor(
+ private val loginRepository: TokenRepository,
+ @ApplicationContext private val context: Context
+) : ViewModel() {
+
+ private var lastSocialLoginType: SocialLoginType? = null
+
+ sealed interface LoginEvent {
+ data object LoginSucceeded : LoginEvent
+ data class ShowError(val message: String) : LoginEvent
+ }
+
+ private val _events = MutableSharedFlow()
+ val events = _events.asSharedFlow()
+
+ fun startSocialLogin(
+ type: SocialLoginType,
+ activity: Activity,
+ ) = viewModelScope.launch {
+ lastSocialLoginType = type
+
+ if(type == SocialLoginType.KAKAO) {
+ try {
+ UserApiClient.instance.loginWithKakaoAccount(context) { token, error ->
+ if (error != null) {
+ Log.e(TAG, "로그인 실패", error)
+ } else if (token != null) {
+ Log.i(TAG, "로그인 성공 ${token.accessToken}")
+ viewModelScope.launch {
+ runCatching {
+ // 보통은 accessToken만 넘김
+ loginRepository.finishSocialLogin(
+ type = lastSocialLoginType!!,
+ accessToken = token.accessToken
+ )
+ }.onSuccess {
+ _events.emit(LoginEvent.LoginSucceeded)
+ }.onFailure { e ->
+ Log.e(TAG, "서버 로그인 실패", e)
+ _events.emit(LoginEvent.ShowError("서버 로그인 실패: ${e.message}"))
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+
+ }
+ } else if (type == SocialLoginType.NAVER) {
+ val nidOAuthCallback = object : NidOAuthCallback {
+ override fun onSuccess() {
+ val accessToken = NidOAuth.getAccessToken()
+
+ if (accessToken.isNullOrBlank()) {
+ Log.e(TAG, "네이버 로그인 성공했지만 accessToken이 null/blank")
+ viewModelScope.launch {
+ _events.emit(LoginEvent.ShowError("네이버 로그인 토큰을 가져오지 못했습니다."))
+ }
+ return
+ }
+
+ Log.i(TAG, "네이버 로그인 성공 accessToken = $accessToken")
+
+ // 카카오와 동일하게 서버 로그인 처리
+ viewModelScope.launch {
+ runCatching {
+ loginRepository.finishSocialLogin(
+ type = lastSocialLoginType!!,
+ accessToken = accessToken
+ )
+ }.onSuccess {
+ _events.emit(LoginEvent.LoginSucceeded)
+ }.onFailure { e ->
+ Log.e(TAG, "네이버 서버 로그인 실패", e)
+ _events.emit(LoginEvent.ShowError("서버 로그인 실패: ${e.message}"))
+ }
+ }
+ }
+
+ override fun onFailure(errorCode: String, errorDesc: String) {
+ Log.e(TAG, "네이버 로그인 실패: $errorCode, $errorDesc")
+ viewModelScope.launch {
+ _events.emit(LoginEvent.ShowError("네이버 로그인 실패: $errorDesc"))
+ }
+ }
+ }
+
+ try {
+ NidOAuth.requestLogin(activity, nidOAuthCallback)
+ } catch (e: Exception) {
+ Log.e(TAG, "네이버 로그인 요청 중 예외", e)
+ viewModelScope.launch {
+ _events.emit(LoginEvent.ShowError("네이버 로그인 오류: ${e.message}"))
+ }
+ }
+ }
+ }
+}
+
diff --git a/feature/signup/src/main/java/com/umcspot/spot/landing/SavingScreen.kt b/feature/signup/src/main/java/com/umcspot/spot/landing/SavingScreen.kt
new file mode 100644
index 00000000..3e7b45bf
--- /dev/null
+++ b/feature/signup/src/main/java/com/umcspot/spot/landing/SavingScreen.kt
@@ -0,0 +1,116 @@
+package com.umcspot.spot.landing
+
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.umcspot.spot.designsystem.R
+import com.umcspot.spot.designsystem.component.GageBar
+import com.umcspot.spot.designsystem.theme.B500
+import com.umcspot.spot.designsystem.theme.SpotTheme
+import com.umcspot.spot.ui.extension.screenHeightDp
+import com.umcspot.spot.ui.extension.screenWidthDp
+import kotlinx.coroutines.delay
+
+@Composable
+fun SavingScreen(
+ contentPadding: PaddingValues,
+ autoProgress: Boolean = true,
+ autoDurationMs: Int = 1800,
+ blockBackPress: Boolean = true,
+ onFinished: () -> Unit,
+) {
+ val topPad = contentPadding.calculateTopPadding()
+ val bottomPad = contentPadding.calculateBottomPadding()
+
+ BackHandler(enabled = blockBackPress) { /* no-op: 뒤로가기 무시 */ }
+
+ val internal = remember { Animatable(0f) }
+ var isDone by remember { mutableStateOf(false) }
+
+ LaunchedEffect(autoProgress) {
+ if (autoProgress) {
+ internal.snapTo(0f)
+ internal.animateTo(
+ targetValue = 1f,
+ animationSpec = tween(durationMillis = autoDurationMs, easing = LinearEasing)
+ )
+ isDone = true
+ delay(3000)
+ onFinished()
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(SpotTheme.colors.white)
+ .padding(top = topPad, bottom = bottomPad, start = 16.dp, end = 16.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .align(Alignment.Center)
+ .wrapContentSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Image(
+ painter = painterResource(R.drawable.spot_logo),
+ contentDescription = null,
+ modifier = Modifier.size(screenWidthDp(40.dp))
+ )
+ Spacer(Modifier.height(screenHeightDp(16.dp)))
+ Text(
+ text = "당신의 스터디 파트너 \n 스팟, SPOT",
+ style = SpotTheme.typography.h2,
+ color = SpotTheme.colors.B500,
+ textAlign = TextAlign.Center
+ )
+ }
+
+ Column(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .fillMaxWidth()
+ .navigationBarsPadding()
+ .padding(bottom = screenHeightDp(8.dp)),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = if (isDone) "등록 완료!" else "내 정보 저장 중..",
+ style = SpotTheme.typography.h4,
+ color = SpotTheme.colors.B500
+ )
+ Spacer(Modifier.height(screenHeightDp(8.dp)))
+ GageBar(
+ value = internal.value,
+ )
+ }
+ }
+}
diff --git a/feature/main/src/main/java/com/umcspot/spot/landing/LandingNavitation.kt b/feature/signup/src/main/java/com/umcspot/spot/landing/navigation/LandingNavigation.kt
similarity index 58%
rename from feature/main/src/main/java/com/umcspot/spot/landing/LandingNavitation.kt
rename to feature/signup/src/main/java/com/umcspot/spot/landing/navigation/LandingNavigation.kt
index 23f6810d..c04673ff 100644
--- a/feature/main/src/main/java/com/umcspot/spot/landing/LandingNavitation.kt
+++ b/feature/signup/src/main/java/com/umcspot/spot/landing/navigation/LandingNavigation.kt
@@ -1,14 +1,11 @@
-package com.umcspot.spot.landing
+package com.umcspot.spot.landing.navigation
-import androidx.compose.foundation.layout.PaddingValues
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
-import com.umcspot.spot.home.HomeScreen
-import com.umcspot.spot.model.QuickMenuType
-import com.umcspot.spot.navigation.MainTabRoute
import com.umcspot.spot.navigation.Route
+import com.umcspot.spot.landing.LandingScreen
import kotlinx.serialization.Serializable
fun NavController.navigateToLanding(navOptions: NavOptions? = null) {
@@ -16,13 +13,11 @@ fun NavController.navigateToLanding(navOptions: NavOptions? = null) {
}
fun NavGraphBuilder.landingGraph(
- onKakaoClick: () -> Unit,
- onNaverClick: () -> Unit
+ onLoginSuccess : () -> Unit
) {
composable {
LandingScreen(
- onKakaoClick = onKakaoClick,
- onNaverClick = onNaverClick
+ onLoginSuccess = onLoginSuccess
)
}
}
diff --git a/feature/signup/src/main/java/com/umcspot/spot/landing/navigation/SavingNavigation.kt b/feature/signup/src/main/java/com/umcspot/spot/landing/navigation/SavingNavigation.kt
new file mode 100644
index 00000000..46464483
--- /dev/null
+++ b/feature/signup/src/main/java/com/umcspot/spot/landing/navigation/SavingNavigation.kt
@@ -0,0 +1,29 @@
+package com.umcspot.spot.landing.navigation
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
+import com.umcspot.spot.landing.SavingScreen
+import com.umcspot.spot.navigation.Route
+import kotlinx.serialization.Serializable
+
+fun NavController.navigateToSaving(navOptions: NavOptions? = null) {
+ navigate(Saving, navOptions)
+}
+
+fun NavGraphBuilder.savingGraph(
+ contentPadding : PaddingValues,
+ onFinished : () -> Unit
+) {
+ composable {
+ SavingScreen(
+ contentPadding = contentPadding,
+ onFinished = onFinished
+ )
+ }
+}
+
+@Serializable
+data object Saving : Route
\ No newline at end of file
diff --git a/feature/signup/src/main/java/com/umcspot/spot/signup/AgreementModal.kt b/feature/signup/src/main/java/com/umcspot/spot/signup/AgreementModal.kt
new file mode 100644
index 00000000..2e8587b9
--- /dev/null
+++ b/feature/signup/src/main/java/com/umcspot/spot/signup/AgreementModal.kt
@@ -0,0 +1,271 @@
+package com.umcspot.spot.signup
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.HorizontalAlignmentLine
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import com.umcspot.spot.designsystem.R
+import com.umcspot.spot.designsystem.component.button.TextButton
+import com.umcspot.spot.designsystem.component.button.TextButtonM
+import com.umcspot.spot.designsystem.component.button.TextButtonS
+import com.umcspot.spot.designsystem.shapes.SpotShapes
+import com.umcspot.spot.designsystem.theme.SpotTheme
+import com.umcspot.spot.ui.extension.screenHeightDp
+
+@Composable
+fun PrivacyConsentDialog(
+ open: Boolean,
+ onAgree: () -> Unit,
+ onDismiss: () -> Unit,
+) {
+ if (!open) return
+
+ Dialog(onDismissRequest = onDismiss) {
+ Surface(
+ shape = SpotShapes.Hard,
+ tonalElevation = 2.dp,
+ color = SpotTheme.colors.white
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(20.dp)
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.Top
+ ) {
+ Spacer(Modifier.weight(1f))
+
+ IconButton(
+ onClick = onDismiss,
+ modifier = Modifier
+ .size(16.dp)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.dismiss),
+ contentDescription = "닫기",
+ modifier = Modifier.size(16.dp)
+ )
+ }
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = "개인정보 이용 및 활용 동의",
+ style = SpotTheme.typography.h2,
+ color = SpotTheme.colors.black,
+ )
+ }
+
+ Spacer(Modifier.height(screenHeightDp(20.dp)))
+
+ Surface(
+ shape = SpotShapes.Hard,
+ border = BorderStroke(1.dp, SpotTheme.colors.gray300),
+ color = SpotTheme.colors.white,
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 160.dp, max = 380.dp),
+ ) {
+ val scroll = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .padding(16.dp)
+ .verticalScroll(scroll)
+ ) {
+
+ Text(
+ text = "제1조 (개인정보 수집 및 이용 목적)",
+ style = SpotTheme.typography.regular_500,
+ color = SpotTheme.colors.black
+ )
+ Spacer(Modifier.height(6.dp))
+ val bullet = SpotTheme.typography.small_500
+ NumberedLine(1, "회원 가입 및 관리: 본인 확인, 회원 서비스 제공", bullet)
+ NumberedLine(2, "서비스 제공 및 운영: 커뮤니티 기능 제공, 맞춤형 콘텐츠 추천", bullet)
+ NumberedLine(3, "고객지원: 문의사항 응대 및 서비스 개선", bullet)
+ NumberedLine(4, "서비스 개선 및 분석: 이용 통계 분석, 부정 이용 방지", bullet)
+
+ Spacer(Modifier.height(12.dp))
+
+
+ Text(
+ text = "제2조 (수집하는 개인정보 항목)",
+ style = SpotTheme.typography.regular_500,
+ color = SpotTheme.colors.black
+ )
+ Spacer(Modifier.height(6.dp))
+ BulletLine("필수항목: 이름, 이메일, 생년월일, 성별", bullet)
+ BulletLine("자동 수집 항목: 접속 로그, 서비스 이용 기록, 기기 정보 등", bullet)
+
+ Spacer(Modifier.height(8.dp))
+ }
+ }
+
+ Spacer(Modifier.height(16.dp))
+
+
+ TextButtonM(
+ text = "동의",
+ onClick = onAgree
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun UniqueConsentDialog(
+ open: Boolean,
+ onAgree: () -> Unit,
+ onDismiss: () -> Unit,
+) {
+ if (!open) return
+
+ Dialog(onDismissRequest = onDismiss) {
+ Surface(
+ shape = SpotShapes.Hard,
+ tonalElevation = 2.dp,
+ color = SpotTheme.colors.white
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(20.dp)
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.Top
+ ) {
+ Spacer(Modifier.weight(1f))
+
+ IconButton(
+ onClick = onDismiss,
+ modifier = Modifier
+ .size(16.dp)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.dismiss),
+ contentDescription = "닫기",
+ modifier = Modifier.size(16.dp)
+ )
+ }
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = "고유식별정보 처리 동의",
+ style = SpotTheme.typography.h2,
+ color = SpotTheme.colors.black,
+ )
+ }
+
+ Spacer(Modifier.height(screenHeightDp(20.dp)))
+
+
+ Surface(
+ shape = SpotShapes.Hard,
+ border = BorderStroke(1.dp, SpotTheme.colors.gray300),
+ color = SpotTheme.colors.white,
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 160.dp, max = 380.dp),
+ ) {
+ val scroll = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .padding(16.dp)
+ .verticalScroll(scroll)
+ ) {
+
+ Text(
+ text = "제1조 (개인정보 수집 및 이용 목적)",
+ style = SpotTheme.typography.regular_500,
+ color = SpotTheme.colors.black
+ )
+ Spacer(Modifier.height(6.dp))
+ val bullet = SpotTheme.typography.small_500
+ NumberedLine(1, "회원 가입 및 관리: 본인 확인, 회원 서비스 제공", bullet)
+ NumberedLine(2, "서비스 제공 및 운영: 커뮤니티 기능 제공, 맞춤형 콘텐츠 추천", bullet)
+ NumberedLine(3, "고객지원: 문의사항 응대 및 서비스 개선", bullet)
+ NumberedLine(4, "서비스 개선 및 분석: 이용 통계 분석, 부정 이용 방지", bullet)
+
+ Spacer(Modifier.height(12.dp))
+
+
+ Text(
+ text = "제2조 (수집하는 개인정보 항목)",
+ style = SpotTheme.typography.regular_500,
+ color = SpotTheme.colors.black
+ )
+ Spacer(Modifier.height(6.dp))
+ BulletLine("필수항목: 이름, 이메일, 생년월일, 성별", bullet)
+ BulletLine("자동 수집 항목: 접속 로그, 서비스 이용 기록, 기기 정보 등", bullet)
+
+ Spacer(Modifier.height(8.dp))
+ }
+ }
+
+ Spacer(Modifier.height(16.dp))
+
+
+ TextButtonM(
+ text = "동의",
+ onClick = onAgree
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun NumberedLine(n: Int, text: String, style: androidx.compose.ui.text.TextStyle) {
+ Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) {
+ Text("$n.", style = style, color = SpotTheme.colors.black)
+ Spacer(Modifier.width(6.dp))
+ Text(text, style = style, color = SpotTheme.colors.black)
+ }
+}
+
+@Composable
+private fun BulletLine(text: String, style: androidx.compose.ui.text.TextStyle) {
+ Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) {
+ Text("•", style = style, color = SpotTheme.colors.black)
+ Spacer(Modifier.width(6.dp))
+ Text(text, style = style, color = SpotTheme.colors.black)
+ }
+}
diff --git a/feature/signup/src/main/java/com/umcspot/spot/signup/SignUpScreen.kt b/feature/signup/src/main/java/com/umcspot/spot/signup/SignUpScreen.kt
index 1c956c24..3f5f5f18 100644
--- a/feature/signup/src/main/java/com/umcspot/spot/signup/SignUpScreen.kt
+++ b/feature/signup/src/main/java/com/umcspot/spot/signup/SignUpScreen.kt
@@ -1,24 +1,385 @@
+@file:OptIn(ExperimentalMaterial3Api::class)
+
package com.umcspot.spot.signup
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.LocalIndication
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.statusBarsPadding
-import androidx.compose.material3.Scaffold
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuDefaults.outlinedTextFieldColors
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import com.umcspot.spot.designsystem.component.appBar.BackTopBar
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.umcspot.spot.designsystem.R
+import com.umcspot.spot.designsystem.component.button.MultiButton
+import com.umcspot.spot.designsystem.component.button.TextButton
+import com.umcspot.spot.designsystem.shapes.SpotShapes
+import com.umcspot.spot.designsystem.theme.B100
+import com.umcspot.spot.designsystem.theme.B500
+import com.umcspot.spot.designsystem.theme.G300
+import com.umcspot.spot.designsystem.theme.SpotTheme
+import com.umcspot.spot.ui.extension.screenHeightDp
+import com.umcspot.spot.ui.state.UiState
+
@Composable
fun SignUpScreen(
+ contentPadding: PaddingValues,
+ onNextClick: () -> Unit,
+ viewmodel: SignUpViewModel = hiltViewModel(),
+) {
+ val uiState by viewmodel.name.collectAsStateWithLifecycle()
+ val focusManager = LocalFocusManager.current
+ val topPad = contentPadding.calculateTopPadding()
+ val bottomPad = contentPadding.calculateBottomPadding()
+
+ var privacyChecked by rememberSaveable { mutableStateOf(false) }
+ var uniqueChecked by rememberSaveable { mutableStateOf(false) }
+
+ var showPrivacyDialog by rememberSaveable { mutableStateOf(false) }
+ var showUniqueDialog by rememberSaveable { mutableStateOf(false) }
+
+ LaunchedEffect(uiState.user) {
+ if (uiState.user is UiState.Empty) {
+ viewmodel.load()
+ }
+ }
+
+ when (val state = uiState.user) {
+ is UiState.Loading -> {
+ Text(text = "로딩 중...", color = Color.Gray)
+ }
+
+ is UiState.Failure -> {
+ Text(text = "에러: ${state.msg}", color = Color.Red)
+ }
+
+ is UiState.Empty -> {
+ Text(text = "데이터가 없습니다.")
+ }
+
+ is UiState.Success -> {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(SpotTheme.colors.white)
+ .padding(top = topPad, start = 14.dp, end = 14.dp, bottom = bottomPad)
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) { focusManager.clearFocus(force = true) }
+ ) {
+
+ Spacer(Modifier.height(screenHeightDp(68.dp)))
+ Text(
+ text = "스팟에서는 안전한 스터디 매칭을 위해\n실명 활동제를 도입하고 있어요.",
+ style = SpotTheme.typography.h3,
+ color = SpotTheme.colors.B500
+ )
+
+ Spacer(Modifier.height(screenHeightDp(33.dp)))
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ .padding(horizontal = 4.dp, vertical = 8.dp)
+ ) {
+ EditableNameRow(
+ name = state.data,
+ onNameChange = { viewmodel.setName(it) }
+ )
+ }
+
+ Spacer(Modifier.weight(1f))
+
+
+ AgreementConfirm(
+ privacyChecked = privacyChecked,
+ uniqueChecked = uniqueChecked,
+ onOpenPrivacyDialog = {
+ if (privacyChecked) privacyChecked = false else showPrivacyDialog = true
+ },
+ onOpenUniqueDialog = {
+ if (uniqueChecked) uniqueChecked = false else showUniqueDialog = true
+ },
+ )
+
+ Spacer(Modifier.height(screenHeightDp(24.dp)))
+
+ TextButton(
+ text = "다음",
+ enabled = privacyChecked && uniqueChecked,
+ onClick = onNextClick
+ )
+ }
+
+ PrivacyConsentDialog(
+ open = showPrivacyDialog,
+ onAgree = {
+ privacyChecked = true
+ showPrivacyDialog = false
+ },
+ onDismiss = { showPrivacyDialog = false }
+ )
+ UniqueConsentDialog(
+ open = showUniqueDialog,
+ onAgree = {
+ uniqueChecked = true
+ showUniqueDialog = false
+ },
+ onDismiss = { showUniqueDialog = false }
+ )
+ }
+ }
+}
+
+@Composable
+fun AgreementConfirm(
+ privacyChecked: Boolean,
+ uniqueChecked: Boolean,
+ onOpenPrivacyDialog: () -> Unit,
+ onOpenUniqueDialog: () -> Unit,
+) {
+ Column {
+ Text(
+ text = "약관 동의",
+ style = SpotTheme.typography.h3
+ )
+ Spacer(Modifier.height(screenHeightDp(7.dp)))
+
+ ConsentItem(
+ title = "개인정보 이용 및 활용 동의",
+ checked = privacyChecked,
+ onClick = onOpenPrivacyDialog
+ )
+ Spacer(Modifier.height(screenHeightDp(4.dp)))
+
+ ConsentItem(
+ title = "고유식별정보 처리 동의",
+ checked = uniqueChecked,
+ onClick = onOpenUniqueDialog
+ )
+ }
+}
+
+@Composable
+fun EditableNameRow(
+ modifier: Modifier = Modifier,
+ name: String,
+ onNameChange: (String) -> Unit,
) {
-// Scaffold (
-// topBar = BackTopBar(
-// title = "회원가입",
-// onBackClick = { },
-// modifier = Modifier
-// .statusBarsPadding()
-// )
-// ) {
-//
-// }
-}
\ No newline at end of file
+ var editing by rememberSaveable { mutableStateOf(false) }
+ var draft by rememberSaveable { mutableStateOf(name) }
+ val focusRequester = remember { FocusRequester() }
+ val focusManager = LocalFocusManager.current
+ val keyboard = LocalSoftwareKeyboardController.current
+ var hadFocus by remember { mutableStateOf(false) }
+
+ LaunchedEffect(editing) {
+ if (editing) {
+ hadFocus = false
+ focusRequester.requestFocus()
+ keyboard?.show()
+ }
+ }
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ ) {
+ if (editing) {
+ Row(
+ modifier = Modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+
+ OutlinedTextField(
+ colors = outlinedTextFieldColors(
+ focusedBorderColor = SpotTheme.colors.B500,
+ unfocusedBorderColor = Color.Transparent,
+ focusedLabelColor = SpotTheme.colors.B500,
+ unfocusedLabelColor = Color.Transparent,
+ cursorColor = SpotTheme.colors.B500,
+ focusedTextColor = SpotTheme.colors.black,
+ unfocusedTextColor = SpotTheme.colors.black,
+ focusedContainerColor = SpotTheme.colors.white,
+ unfocusedContainerColor = SpotTheme.colors.white
+ ),
+ value = draft,
+ onValueChange = { new ->
+ when {
+ new.length <= 15 -> {
+ draft = new
+ }
+
+ new.length > draft.length -> {
+ val last = new.last()
+ draft = draft.take(14) + last
+ }
+
+ else -> {
+ draft = new.take(15)
+ }
+ }
+ },
+ singleLine = true,
+ shape = SpotShapes.Hard,
+ textStyle = SpotTheme.typography.h2,
+ placeholder = { Text("이름을 입력하세요", style = SpotTheme.typography.h2) },
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ keyboardActions = KeyboardActions(onDone = {
+
+ focusManager.clearFocus(force = true)
+ }),
+ modifier = Modifier
+ .weight(1f)
+ .focusRequester(focusRequester)
+ .onFocusChanged { state ->
+ if (hadFocus && !state.isFocused && editing) {
+ val final = draft.trim()
+ if (final.isNotEmpty()) {
+ onNameChange(final)
+ } else {
+ draft = name
+ }
+ editing = false
+ keyboard?.hide()
+ }
+ hadFocus = state.isFocused
+ }
+ )
+ }
+ Spacer(Modifier.height(screenHeightDp(8.dp)))
+
+ Text(
+ text = "공백 포함 15자까지 입력 가능해요.",
+ style = SpotTheme.typography.regular_500,
+ color = SpotTheme.colors.B500,
+ modifier = Modifier.padding(start = 10.dp)
+ )
+ } else {
+ Row(
+ modifier = modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = name,
+ style = SpotTheme.typography.h2
+ )
+
+ MultiButton(
+ text = "수정",
+ painter = painterResource(R.drawable.write),
+ modifier = Modifier.width(85.dp),
+ onClick = {
+ draft = name
+ editing = true
+ }
+ )
+
+ }
+ Spacer(Modifier.height(8.dp))
+ Text(
+ text = "실명이 맞나요? 이름을 확인해주세요.",
+ style = SpotTheme.typography.regular_500,
+ color = SpotTheme.colors.gray400
+ )
+ }
+ }
+}
+
+@Composable
+private fun ConsentItem(
+ title: String,
+ checked: Boolean,
+ onClick: () -> Unit,
+) {
+ val border = if (checked) SpotTheme.colors.B500 else SpotTheme.colors.G300
+ val checkTint = if (checked) SpotTheme.colors.B500 else SpotTheme.colors.black
+ val background = if (checked) SpotTheme.colors.B100 else SpotTheme.colors.white
+
+ Surface(
+ shape = SpotShapes.Soft,
+ color = background,
+ border = BorderStroke(1.dp, border),
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 52.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = LocalIndication.current,
+ role = Role.Checkbox,
+ onClick = onClick
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 14.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = title,
+ style = SpotTheme.typography.regular_500,
+ color = SpotTheme.colors.black,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Icon(
+ painter = painterResource(R.drawable.success_default),
+ modifier = Modifier.size(15.dp),
+ contentDescription = if (checked) "동의됨" else "미동의",
+ tint = checkTint
+ )
+ }
+ }
+}
diff --git a/feature/signup/src/main/java/com/umcspot/spot/signup/SignUpViewModel.kt b/feature/signup/src/main/java/com/umcspot/spot/signup/SignUpViewModel.kt
index 17044c1a..44f9386c 100644
--- a/feature/signup/src/main/java/com/umcspot/spot/signup/SignUpViewModel.kt
+++ b/feature/signup/src/main/java/com/umcspot/spot/signup/SignUpViewModel.kt
@@ -1,15 +1,91 @@
package com.umcspot.spot.signup
+import android.util.Log
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.umcspot.spot.ui.state.UiState
+import com.umcspot.spot.user.repository.UserRepository
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 javax.inject.Inject
@HiltViewModel
class SignUpViewModel @Inject constructor(
-// private val signUpRepository: SignUpRepository
+ private val userRepository: UserRepository
) : ViewModel() {
+ data class SignupUiState(
+ val user: UiState = UiState.Empty,
+ val originalName: String? = null, // 서버에서 가져온 원래 이름
+ val currentName: String? = null // 현재 수정 중인 이름
+ )
+ private val _name = MutableStateFlow(SignupUiState())
+ val name: StateFlow = _name.asStateFlow()
+
fun load() {
+ _name.update { it.copy(user = UiState.Loading) }
+
+ viewModelScope.launch {
+ val result = userRepository.getUserName()
+
+ val newState = result.fold(
+ onSuccess = { userResult ->
+ UiState.Success(userResult.name)
+ },
+ onFailure = { e ->
+ UiState.Failure(e.message ?: e.toString())
+ }
+ )
+
+ _name.update {
+ it.copy(
+ user = newState,
+ originalName = (newState as? UiState.Success)?.data,
+ currentName = (newState as? UiState.Success)?.data
+ )
+ }
+ }
+ }
+ fun setName(newName: String) {
+ _name.update {
+ it.copy(
+ user = UiState.Success(newName),
+ currentName = newName
+ )
+ }
+ }
+
+ fun saveNameIfChanged() {
+ val (original, current) = getNamePair()
+
+ if (current.isNullOrBlank() || current == original) return
+
+ viewModelScope.launch {
+ userRepository.setUserName(current)
+ .onSuccess {
+ Log.d("ChangeName", "이름 변경 성공")
+ // 서버에도 저장됐으니, 로컬 state도 맞춰주기
+ _name.update {
+ it.copy(
+ user = UiState.Success(current),
+ originalName = current,
+ currentName = current
+ )
+ }
+ }
+ .onFailure { e ->
+ Log.e("ChangeName", "이름 변경 실패", e)
+ }
+ }
+ }
+
+ fun getNamePair(): Pair {
+ Log.d("NamePair", "${_name.value.originalName} ::: ${_name.value.currentName}")
+ return _name.value.originalName to _name.value.currentName
}
}
\ No newline at end of file
diff --git a/feature/signup/src/main/java/com/umcspot/spot/signup/navigation/SignUpNavigation.kt b/feature/signup/src/main/java/com/umcspot/spot/signup/navigation/SignUpNavigation.kt
index 2a73d28e..f2896053 100644
--- a/feature/signup/src/main/java/com/umcspot/spot/signup/navigation/SignUpNavigation.kt
+++ b/feature/signup/src/main/java/com/umcspot/spot/signup/navigation/SignUpNavigation.kt
@@ -1,21 +1,38 @@
package com.umcspot.spot.signup.navigation
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.remember
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import com.umcspot.spot.navigation.Route
import com.umcspot.spot.signup.SignUpScreen
+import com.umcspot.spot.signup.SignUpViewModel
import kotlinx.serialization.Serializable
fun NavController.navigateToSignUp(navOptions: NavOptions? = null) {
navigate(SignUp, navOptions)
}
-fun NavGraphBuilder.signupGraph() {
- composable {
- SignUpScreen(
+fun NavGraphBuilder.signupGraph(
+ navController: NavHostController,
+ contentPadding : PaddingValues,
+ onNextClick : () -> Unit
+) {
+ composable { backStackEntry ->
+ val parentEntry = remember(backStackEntry) {
+ // 🔹 NavHost 루트 그래프 기준으로 ViewModel 스코프
+ navController.getBackStackEntry(navController.graph.id)
+ }
+ val signUpViewModel: SignUpViewModel = hiltViewModel(parentEntry)
+ SignUpScreen(
+ contentPadding = contentPadding,
+ viewmodel = signUpViewModel,
+ onNextClick = onNextClick
)
}
}
diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt
index a85abf99..b6993f51 100644
--- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt
+++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -15,8 +16,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
@@ -26,7 +25,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
@@ -39,12 +37,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.umcspot.spot.designsystem.component.button.TextButton
import com.umcspot.spot.designsystem.component.button.TextToggleButton
import com.umcspot.spot.designsystem.component.study.section.ActivityThemeSection
-import com.umcspot.spot.designsystem.component.study.section.ActivityTypeSection
import com.umcspot.spot.designsystem.theme.SpotTheme
import com.umcspot.spot.model.ActivityType
import com.umcspot.spot.model.FeeRange
import com.umcspot.spot.model.StudyTheme
import com.umcspot.spot.ui.extension.screenHeightDp
+import com.umcspot.spot.ui.extension.screenWidthDp
+import kotlinx.collections.immutable.ImmutableList
@Composable
fun RecruitingStudyFilterScreen(
@@ -52,16 +51,15 @@ fun RecruitingStudyFilterScreen(
onAcceptFilterClick: () -> Unit,
vm: RecruitingStudyFilterViewModel = hiltViewModel(),
) {
- val activityType by vm.activity.collectAsStateWithLifecycle()
- val fee by vm.fee.collectAsStateWithLifecycle()
- val theme by vm.theme.collectAsStateWithLifecycle()
- val acceptEnabled by vm.notNull.collectAsStateWithLifecycle()
+ val activities by vm.activities.collectAsStateWithLifecycle()
+ val fees by vm.fees.collectAsStateWithLifecycle()
+ val themes by vm.themes.collectAsStateWithLifecycle()
+ val acceptEnabled by vm.notNull.collectAsStateWithLifecycle()
val topPad = contentPadding.calculateTopPadding()
val bottomPad = contentPadding.calculateBottomPadding()
- // 적용 이벤트 수신
LaunchedEffect(Unit) {
vm.events.collect { ev ->
when (ev) {
@@ -73,13 +71,13 @@ fun RecruitingStudyFilterScreen(
RecruitingStudyFilterScreenContent(
modifier = Modifier
.padding(top = topPad, bottom = bottomPad),
- activityType = activityType,
- fee = fee,
- theme = theme,
+ selectedActivities = activities,
+ selectedFees = fees,
+ selectedThemes = themes,
buttonEnabled = acceptEnabled,
- onSetActivity = vm::setActivity,
- onSetFee = vm::setFee,
- onSetTheme = vm::setTheme,
+ onToggleActivity = vm::toggleActivity,
+ onToggleFee = vm::toggleFee,
+ onToggleTheme = vm::toggleTheme,
onReset = vm::reset,
onApply = vm::apply
)
@@ -87,13 +85,13 @@ fun RecruitingStudyFilterScreen(
@Composable
fun RecruitingStudyFilterScreenContent(
- activityType: ActivityType?, // ✅ 단일 값 (nullable)
- fee: FeeRange?, // ✅ 단일 값 (nullable)
- theme: StudyTheme?, // ✅ 단일 값 (nullable)
+ selectedActivities: ImmutableList,
+ selectedFees: ImmutableList,
+ selectedThemes: ImmutableList,
buttonEnabled: Boolean,
- onSetActivity: (ActivityType) -> Unit, // ✅ set* 로직 (같은 값 다시 누르면 해제는 VM이 처리)
- onSetFee: (FeeRange?) -> Unit,
- onSetTheme: (StudyTheme) -> Unit,
+ onToggleActivity: (ActivityType) -> Unit,
+ onToggleFee: (FeeRange) -> Unit,
+ onToggleTheme: (StudyTheme) -> Unit,
onReset: () -> Unit,
onApply: () -> Unit,
modifier: Modifier = Modifier
@@ -106,7 +104,7 @@ fun RecruitingStudyFilterScreenContent(
Column(
modifier = modifier
.fillMaxSize()
- .verticalScroll(rememberScrollState()) // ✅ 스크롤
+ .verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
) {
Text(
@@ -115,22 +113,23 @@ fun RecruitingStudyFilterScreenContent(
color = SpotTheme.colors.black
)
- Spacer(modifier = Modifier.height(10.dp))
+ Spacer(modifier = Modifier.height(screenHeightDp(10.dp)))
+
- ActivityTypeSection(
- activityType = activityType,
- onSelect = onSetActivity
+ ActivityTypeMultiSection(
+ selectedTypes = selectedActivities,
+ onToggle = onToggleActivity
)
- Spacer(modifier = Modifier.height(30.dp))
+ Spacer(modifier = Modifier.height(screenHeightDp(30.dp)))
ActivityFeeSection(
- activityFee = fee,
- onSelect = onSetFee
+ selectedFees = selectedFees,
+ onToggle = onToggleFee
)
- Spacer(modifier = Modifier.height(30.dp))
+ Spacer(modifier = Modifier.height(screenHeightDp(30.dp)))
Text(
text = "스터디 테마",
@@ -140,19 +139,19 @@ fun RecruitingStudyFilterScreenContent(
Spacer(modifier = Modifier.height(screenHeightDp(20.dp)))
-
ActivityThemeSection(
- activityTheme = theme,
- onSelect = onSetTheme
+ selectedThemes = selectedThemes,
+ onSelect = onToggleTheme,
+ maxSelection = 10
)
- Spacer(modifier = Modifier.height(20.dp))
+ Spacer(modifier = Modifier.height(screenHeightDp(20.dp)))
ResetFilterText(
onClick = onReset
)
- Spacer(Modifier.height(80.dp))
+ Spacer(Modifier.height(screenHeightDp(80.dp)))
}
Box(
@@ -161,9 +160,8 @@ fun RecruitingStudyFilterScreenContent(
.fillMaxWidth()
.navigationBarsPadding()
.padding(horizontal = 16.dp, vertical = 12.dp)
- .zIndex(1f) // 항상 앞
+ .zIndex(1f)
) {
-
TextButton(
text = "검색 결과 보기",
enabled = buttonEnabled,
@@ -173,10 +171,30 @@ fun RecruitingStudyFilterScreenContent(
}
}
+@Composable
+fun ActivityTypeMultiSection(
+ selectedTypes: ImmutableList,
+ onToggle: (ActivityType) -> Unit
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ ActivityType.entries.forEach { type ->
+ TextToggleButton(
+ modifier = Modifier.weight(1f),
+ text = type.label,
+ checked = selectedTypes.contains(type),
+ onClick = { onToggle(type) }
+ )
+ }
+ }
+}
+
@Composable
fun ActivityFeeSection(
- activityFee: FeeRange?,
- onSelect: (FeeRange) -> Unit // 누르면 VM의 setActivity 호출
+ selectedFees: ImmutableList,
+ onToggle: (FeeRange) -> Unit
) {
Column(
modifier = Modifier
@@ -198,15 +216,14 @@ fun ActivityFeeSection(
TextToggleButton(
text = fee.label,
width = 71.dp,
- checked = activityFee == fee,
- onClick = { onSelect(fee) },
+ checked = selectedFees.contains(fee),
+ onClick = { onToggle(fee) },
)
}
}
}
}
-
@Composable
fun ResetFilterText(
onClick: () -> Unit,
@@ -223,10 +240,9 @@ fun ResetFilterText(
.semantics { role = Role.Button }
.clickable(
interactionSource = remember { MutableInteractionSource() },
- indication = null, // 리플 없애려면 유지, 리플 원하면 제거
+ indication = null,
onClick = onClick
)
- .padding(vertical = 4.dp) // 터치 여유
+ .padding(vertical = 4.dp)
)
-}
-
+}
\ No newline at end of file
diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterViewmodel.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterViewmodel.kt
index fba9f5b3..269eff7c 100644
--- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterViewmodel.kt
+++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterViewmodel.kt
@@ -2,8 +2,14 @@ package com.umcspot.spot.study.recruiting
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
+import com.umcspot.spot.model.ActivityType
+import com.umcspot.spot.model.FeeRange
+import com.umcspot.spot.model.StudyTheme
import dagger.hilt.android.lifecycle.HiltViewModel
-import javax.inject.Inject
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.PersistentList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
@@ -11,98 +17,101 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import java.io.Serializable
-import com.umcspot.spot.model.ActivityType
-import com.umcspot.spot.model.FeeRange
-import com.umcspot.spot.model.StudyTheme
+import javax.inject.Inject
@HiltViewModel
class RecruitingStudyFilterViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
- // ── 저장 키
- private val KEY_ACTIVITY = "recruit_filter_activity"
- private val KEY_FEE = "recruit_filter_fee"
- private val KEY_THEME = "recruit_filter_theme"
+ private val KEY_ACTIVITIES = "recruit_filter_activities"
+ private val KEY_FEES = "recruit_filter_fees"
+ private val KEY_THEMES = "recruit_filter_themes"
- // ── 단일 값 StateFlow
- private val _activity = MutableStateFlow(savedStateHandle.get(KEY_ACTIVITY))
- val activity: StateFlow = _activity.asStateFlow()
+ private val _activities = MutableStateFlow>(
+ savedStateHandle.get>(KEY_ACTIVITIES)?.toPersistentList() ?: persistentListOf()
+ )
+ val activities: StateFlow> = _activities.asStateFlow()
- private val _fee = MutableStateFlow(savedStateHandle.get(KEY_FEE))
- val fee: StateFlow = _fee.asStateFlow()
+ private val _fees = MutableStateFlow>(
+ savedStateHandle.get>(KEY_FEES)?.toPersistentList() ?: persistentListOf()
+ )
+ val fees: StateFlow> = _fees.asStateFlow()
- private val _theme = MutableStateFlow(savedStateHandle.get(KEY_THEME))
- val theme: StateFlow = _theme.asStateFlow()
+ private val _themes = MutableStateFlow>(
+ savedStateHandle.get>(KEY_THEMES)?.toPersistentList() ?: persistentListOf()
+ )
+ val themes: StateFlow> = _themes.asStateFlow()
- // ✅ 하나라도 선택되었는지 여부
private fun calcNotNull(): Boolean =
- (_activity.value != null) || (_fee.value != null) || (_theme.value != null)
+ _activities.value.isNotEmpty() || _fees.value.isNotEmpty() || _themes.value.isNotEmpty()
private val _notNull = MutableStateFlow(calcNotNull())
val notNull: StateFlow = _notNull.asStateFlow()
- // ── 1회성 이벤트
sealed interface Event : Serializable {
data class Applied(val filter: RecruitingStudyFilter) : Event
}
private val _events = MutableSharedFlow(extraBufferCapacity = 1)
val events: SharedFlow = _events
- // ── 합쳐서 쓰는 DTO (단일값 버전)
data class RecruitingStudyFilter(
- val activity: ActivityType?,
- val fee: FeeRange?,
- val theme: StudyTheme?
+ val activities: List,
+ val fees: List,
+ val themes: List
) : Serializable {
- fun isEmpty() = activity == null && fee == null && theme == null
+ fun isEmpty() = activities.isEmpty() && fees.isEmpty() && themes.isEmpty()
}
- fun setActivity(type: ActivityType) {
- _activity.value = _activity.value?.let { cur -> if (cur == type) null else type } ?: type
- savedStateHandle[KEY_ACTIVITY] = _activity.value
- _notNull.value = calcNotNull() // ✅ 갱신
+ fun toggleActivity(type: ActivityType) {
+ _activities.update { current ->
+
+ val newList = if (current.contains(type)) current.remove(type) else current.add(type)
+ savedStateHandle[KEY_ACTIVITIES] = ArrayList(newList)
+ newList
+ }
+ _notNull.value = calcNotNull()
}
- fun setFee(fee: FeeRange?) {
- _fee.value = fee
- savedStateHandle[KEY_FEE] = fee
- _notNull.value = calcNotNull() // ✅ 갱신
+ fun toggleFee(fee: FeeRange) {
+ _fees.update { current ->
+ val newList = if (current.contains(fee)) current.remove(fee) else current.add(fee)
+ savedStateHandle[KEY_FEES] = ArrayList(newList)
+ newList
+ }
+ _notNull.value = calcNotNull()
}
- fun setTheme(theme: StudyTheme) {
- _theme.value = _theme.value?.let { cur -> if (cur == theme) null else theme } ?: theme
- savedStateHandle[KEY_THEME] = _theme.value
- _notNull.value = calcNotNull() // ✅ 갱신
+ fun toggleTheme(theme: StudyTheme) {
+ _themes.update { current ->
+ val newList = if (current.contains(theme)) current.remove(theme) else current.add(theme)
+ savedStateHandle[KEY_THEMES] = ArrayList(newList)
+ newList
+ }
+ _notNull.value = calcNotNull()
}
-
fun reset() {
- _activity.value = null
- _fee.value = null
- _theme.value = null
- savedStateHandle[KEY_ACTIVITY] = null
- savedStateHandle[KEY_FEE] = null
- savedStateHandle[KEY_THEME] = null
- _notNull.value = calcNotNull() // ✅ 갱신
+ _activities.value = persistentListOf()
+ _fees.value = persistentListOf()
+ _themes.value = persistentListOf()
+
+ savedStateHandle[KEY_ACTIVITIES] = null
+ savedStateHandle[KEY_FEES] = null
+ savedStateHandle[KEY_THEMES] = null
+
+ _notNull.value = false
}
fun apply() {
_events.tryEmit(
Event.Applied(
RecruitingStudyFilter(
- activity = _activity.value,
- fee = _fee.value,
- theme = _theme.value
+ activities = _activities.value,
+ fees = _fees.value,
+ themes = _themes.value
)
)
)
}
-
- fun getCurrentFilter(): RecruitingStudyFilter =
- RecruitingStudyFilter(
- activity = _activity.value,
- fee = _fee.value,
- theme = _theme.value
- )
-}
+}
\ No newline at end of file
diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt
index 3d502875..4090f315 100644
--- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt
+++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt
@@ -1,5 +1,6 @@
package com.umcspot.spot.study.recruiting
+import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -57,6 +58,7 @@ import com.umcspot.spot.designsystem.shapes.SpotShapes
import com.umcspot.spot.designsystem.theme.B500
import com.umcspot.spot.designsystem.theme.G300
import com.umcspot.spot.study.recruiting.RecruitingStudyViewModel
+import timber.log.Timber
@Composable
fun RecruitingStudyScreen(
@@ -71,7 +73,6 @@ fun RecruitingStudyScreen(
var showSortSheet by remember { mutableStateOf(false) }
-
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d446b4ad..a2342695 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -90,7 +90,7 @@ timber = "5.0.1"
coil = "2.7.0"
lottie = "6.4.1"
jsoup = "1.17.2"
-kakao-login = "2.19.0"
+kakao = "2.23.0"
process-pheonix = "3.0.0"
preference = "1.2.1"
collapsing-toolbar = "2.3.5"
@@ -98,6 +98,11 @@ runtimeAndroid = "1.7.8"
flexibleBottomSheet = "0.1.5"
annotationJvm = "1.9.1"
+# naver login
+browser = "1.8.0"
+naver-oauth = "5.11.0"
+datastoreCore = "1.1.7"
+
[plugins]
# Gradle Plugins
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
@@ -251,7 +256,11 @@ coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
lottie = { group = "com.airbnb.android", name = "lottie", version.ref = "lottie" }
lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottie" }
jsoup = { group = "org.jsoup", name = "jsoup", version.ref = "jsoup" }
-kakao-login = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao-login" }
+kakao-login = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao" }
+kakao-auth = { group = "com.kakao.sdk", name = "v2-auth", version.ref = "kakao" }
+kakao-common = { group = "com.kakao.sdk", name = "v2-all", version.ref = "kakao" }
+
+
process-phoenix = { module = "com.jakewharton:process-phoenix", version.ref = "process-pheonix" }
collapsing-toolbar = { group = "me.onebone", name = "toolbar-compose", version.ref = "collapsing-toolbar" }
androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" }
@@ -260,6 +269,12 @@ flexible-bottomsheet = { group = "com.github.skydoves", name = "flexible-bottoms
#CalendarLibrary
kizitonwose-calendar-compose = { module = "com.kizitonwose.calendar:compose", version.ref = "kizitonwose-calendar" }
+#naver login
+naver-oauth = { module = "com.navercorp.nid:oauth", version.ref = "naver-oauth" }
+naver-jdk = { module = "com.navercorp.nid:oauth-jdk8", version.ref = "naver-oauth" }
+androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
+datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" }
+
[bundles]
coil = ["coil", "coil-svg", "coil-gif"]
firebase = ["firebase-analytics", "firebase-database", "firebase-messaging", "firebase-remoteConfig"]
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 571267c0..19e2ed9a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -18,6 +18,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven { url = java.net.URI("https://devrepo.kakao.com/nexus/content/groups/public/") }
}
}
@@ -50,4 +51,7 @@ include(":domain:alert")
include(":data:board")
include(":feature:study")
include(":feature:signup")
-include(":domain:signup")
+include(":domain:user")
+include(":data:user")
+include(":data:login")
+include(":domain:token")