From d627f7f3c3c429ec9f5d63526857a4b53393d97c Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 11 Sep 2025 21:47:36 +0900 Subject: [PATCH 001/102] =?UTF-8?q?[feat]:=20Firebase=20Cloud=20Messaging?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=84=A4=EC=A0=95=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 8 ++++++++ gradle/libs.versions.toml | 2 ++ 3 files changed, 11 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8d113e29..244acc0d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -103,6 +103,7 @@ dependencies { implementation(libs.foundation) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.datastore.preferences) + implementation(libs.firebase.messaging) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2fe2f65a..bef34395 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -47,5 +47,13 @@ + + + + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a14d89d1..6b8ef7fd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,7 @@ retrofitKotlinSerializationConverter = "1.0.0" androidxComposeNavigation = "2.8.2" lifecycleRuntimeCompose = "2.10.0-alpha01" datastorePreferences = "1.1.7" +firebaseMessaging = "25.0.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -62,6 +63,7 @@ retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit retrofit-kotlin-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinSerializationConverter" } androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" } +firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version.ref = "firebaseMessaging" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From 381ed36de98251bc00a307cb49eff52ac9fc700f Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 11 Sep 2025 21:51:39 +0900 Subject: [PATCH 002/102] =?UTF-8?q?[feat]:=20Firebase=20Cloud=20Messaging?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84=20(?= =?UTF-8?q?=EC=9D=BC=EB=8B=A8=20=EC=95=8C=EB=A6=BC=EC=9D=84=20=ED=83=AD?= =?UTF-8?q?=ED=95=98=EB=A9=B4=20=EB=A9=94=EC=9D=B8=20=EC=97=91=ED=8B=B0?= =?UTF-8?q?=EB=B9=84=ED=8B=B0=EB=A1=9C=20=EC=9D=B4=EB=8F=99)=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/MyFirebaseMessagingService.kt | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/MyFirebaseMessagingService.kt diff --git a/app/src/main/java/com/texthip/thip/MyFirebaseMessagingService.kt b/app/src/main/java/com/texthip/thip/MyFirebaseMessagingService.kt new file mode 100644 index 00000000..97776983 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/MyFirebaseMessagingService.kt @@ -0,0 +1,94 @@ +package com.texthip.thip + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.NotificationCompat +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import com.texthip.thip.data.manager.FcmTokenManager +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class MyFirebaseMessagingService : FirebaseMessagingService() { + @Inject + lateinit var fcmTokenManager: FcmTokenManager + + companion object { + private const val TAG = "FCM" + private const val CHANNEL_ID = "thip_notifications" + private const val CHANNEL_NAME = "THIP 알림" + private const val CHANNEL_DESCRIPTION = "THIP 앱의 푸시 알림" + } + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + super.onMessageReceived(remoteMessage) + + Log.d(TAG, "From: ${remoteMessage.from}") + + if (remoteMessage.data.isNotEmpty()) { + Log.d(TAG, "Message data payload: ${remoteMessage.data}") + } + + remoteMessage.notification?.let { + Log.d(TAG, "Message Notification Body: ${it.body}") + showNotification(it.title, it.body) + } + } + + override fun onNewToken(token: String) { + super.onNewToken(token) + Log.d(TAG, "Refreshed token: $token") + + // FCM 토큰 관리자를 통해 처리 + CoroutineScope(Dispatchers.IO).launch { + fcmTokenManager.handleNewToken(token) + } + } + + private fun showNotification(title: String?, messageBody: String?) { + val intent = Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + + val pendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) + + createNotificationChannel() + + val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentTitle(title ?: "THIP") + .setContentText(messageBody) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(0, notificationBuilder.build()) + } + + private fun createNotificationChannel() { + val channel = NotificationChannel( + CHANNEL_ID, + CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + description = CHANNEL_DESCRIPTION + } + + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } +} \ No newline at end of file From 7300b20a303119bda150e284a22285403e6edcaa Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 11 Sep 2025 21:53:26 +0900 Subject: [PATCH 003/102] =?UTF-8?q?[feat]:=20FCM=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20request=20=EA=B5=AC=ED=98=84=20(#131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/notification/request/FcmTokenRequest.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/notification/request/FcmTokenRequest.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/notification/request/FcmTokenRequest.kt b/app/src/main/java/com/texthip/thip/data/model/notification/request/FcmTokenRequest.kt new file mode 100644 index 00000000..613ce547 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/notification/request/FcmTokenRequest.kt @@ -0,0 +1,10 @@ +package com.texthip.thip.data.model.notification.request + +import kotlinx.serialization.Serializable + +@Serializable +data class FcmTokenRequest( + val deviceId: String, + val fcmToken: String, + val platformType: String = "ANDROID" +) \ No newline at end of file From 0230ab96d1f863057913e3e7e9ca5fe188170b44 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 11 Sep 2025 21:54:55 +0900 Subject: [PATCH 004/102] =?UTF-8?q?[feat]:=20FCM=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20service,=20repository=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/data/di/ServiceModule.kt | 6 +++++ .../data/repository/NotificationRepository.kt | 27 +++++++++++++++++++ .../thip/data/service/NotificationService.kt | 13 +++++++++ 3 files changed, 46 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/repository/NotificationRepository.kt create mode 100644 app/src/main/java/com/texthip/thip/data/service/NotificationService.kt diff --git a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt index 4d8b0fe8..eba74fd8 100644 --- a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt +++ b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt @@ -7,6 +7,7 @@ import com.texthip.thip.data.service.CommentsService import com.texthip.thip.data.service.FeedService import com.texthip.thip.data.service.RoomsService import com.texthip.thip.data.service.UserService +import com.texthip.thip.data.service.NotificationService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -56,4 +57,9 @@ object ServiceModule { @Singleton fun provideFeedService(retrofit: Retrofit): FeedService = retrofit.create(FeedService::class.java) + + @Provides + @Singleton + fun provideNotificationService(retrofit: Retrofit): NotificationService = + retrofit.create(NotificationService::class.java) } diff --git a/app/src/main/java/com/texthip/thip/data/repository/NotificationRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/NotificationRepository.kt new file mode 100644 index 00000000..42a4740c --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/repository/NotificationRepository.kt @@ -0,0 +1,27 @@ +package com.texthip.thip.data.repository + +import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.notification.request.FcmTokenRequest +import com.texthip.thip.data.service.NotificationService +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NotificationRepository @Inject constructor( + private val notificationService: NotificationService +) { + suspend fun registerFcmToken( + deviceId: String, + fcmToken: String + ): Result { + return runCatching { + val request = FcmTokenRequest( + deviceId = deviceId, + fcmToken = fcmToken, + platformType = "ANDROID" + ) + val response = notificationService.registerFcmToken(request) + response.handleBaseResponse().getOrThrow() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/NotificationService.kt b/app/src/main/java/com/texthip/thip/data/service/NotificationService.kt new file mode 100644 index 00000000..acc3b1c8 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/service/NotificationService.kt @@ -0,0 +1,13 @@ +package com.texthip.thip.data.service + +import com.texthip.thip.data.model.notification.request.FcmTokenRequest +import com.texthip.thip.data.model.base.BaseResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface NotificationService { + @POST("notifications/fcm-tokens") + suspend fun registerFcmToken( + @Body request: FcmTokenRequest + ): BaseResponse +} \ No newline at end of file From 8db0e02fe8c8d57adf455e0547acf7e81e1dd8ab Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 11 Sep 2025 21:55:28 +0900 Subject: [PATCH 005/102] =?UTF-8?q?[feat]:=20FCM=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=A7=A4=EB=8B=88=EC=A0=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/manager/FcmTokenManager.kt | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt diff --git a/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt b/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt new file mode 100644 index 00000000..57287b4a --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt @@ -0,0 +1,89 @@ +package com.texthip.thip.data.manager + +import android.annotation.SuppressLint +import android.content.Context +import android.provider.Settings +import android.util.Log +import com.google.firebase.messaging.FirebaseMessaging +import com.texthip.thip.data.repository.NotificationRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FcmTokenManager @Inject constructor( + private val tokenManager: TokenManager, + private val notificationRepository: NotificationRepository, + private val context: Context +) { + + suspend fun handleNewToken(newToken: String) { + val storedToken = tokenManager.getFcmTokenOnce() + + if (storedToken != newToken) { + Log.d("FCM", "Token updated") + + // 새 토큰 저장 + tokenManager.saveFcmToken(newToken) + + // 서버에 전송 + sendTokenToServer(newToken) + } + } + + suspend fun sendCurrentTokenIfExists() { + val storedFcmToken = tokenManager.getFcmTokenOnce() + + if (storedFcmToken != null) { + sendTokenToServer(storedFcmToken) + } else { + // 저장된 토큰이 없으면 Firebase에서 직접 가져와서 저장하고 전송 + fetchAndSendCurrentToken() + } + } + + private fun fetchAndSendCurrentToken() { + try { + FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> + if (!task.isSuccessful) { + Log.w("FCM", "Failed to fetch token", task.exception) + return@addOnCompleteListener + } + + val token = task.result + // 토큰을 저장하고 서버로 전송 (비동기) + CoroutineScope(Dispatchers.IO).launch { + tokenManager.saveFcmToken(token) + sendTokenToServer(token) + } + } + } catch (e: Exception) { + Log.e("FCM", "Error fetching FCM token", e) + } + } + + private suspend fun sendTokenToServer(token: String) { + runCatching { + val deviceId = getDeviceId() + notificationRepository.registerFcmToken(deviceId, token) + }.onSuccess { + it.onSuccess { + Log.d("FCM", "Token sent successfully") + }.onFailure { exception -> + Log.e("FCM", "Failed to send token", exception) + } + }.onFailure { exception -> + Log.e("FCM", "Error sending token", exception) + } + } + + @SuppressLint("HardwareIds") + private fun getDeviceId(): String { + return Settings.Secure.getString( + context.contentResolver, + Settings.Secure.ANDROID_ID + ) ?: "unknown_device" + } +} \ No newline at end of file From 8539a5d2886632d117f1c06d0dbbea702d3b2e4e Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 11 Sep 2025 22:00:10 +0900 Subject: [PATCH 006/102] =?UTF-8?q?[feat]:=20FCM=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=A7=A4=EB=8B=88=EC=A0=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/manager/FcmTokenManager.kt | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt b/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt index 57287b4a..a48e822d 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt @@ -4,29 +4,39 @@ import android.annotation.SuppressLint import android.content.Context import android.provider.Settings import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey import com.google.firebase.messaging.FirebaseMessaging import com.texthip.thip.data.repository.NotificationRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @Singleton class FcmTokenManager @Inject constructor( - private val tokenManager: TokenManager, + private val dataStore: DataStore, private val notificationRepository: NotificationRepository, private val context: Context ) { + + companion object { + private val FCM_TOKEN_KEY = stringPreferencesKey("fcm_token") + } suspend fun handleNewToken(newToken: String) { - val storedToken = tokenManager.getFcmTokenOnce() + val storedToken = getFcmTokenOnce() if (storedToken != newToken) { Log.d("FCM", "Token updated") // 새 토큰 저장 - tokenManager.saveFcmToken(newToken) + saveFcmToken(newToken) // 서버에 전송 sendTokenToServer(newToken) @@ -34,7 +44,7 @@ class FcmTokenManager @Inject constructor( } suspend fun sendCurrentTokenIfExists() { - val storedFcmToken = tokenManager.getFcmTokenOnce() + val storedFcmToken = getFcmTokenOnce() if (storedFcmToken != null) { sendTokenToServer(storedFcmToken) @@ -55,7 +65,7 @@ class FcmTokenManager @Inject constructor( val token = task.result // 토큰을 저장하고 서버로 전송 (비동기) CoroutineScope(Dispatchers.IO).launch { - tokenManager.saveFcmToken(token) + saveFcmToken(token) sendTokenToServer(token) } } @@ -64,6 +74,15 @@ class FcmTokenManager @Inject constructor( } } + // FCM 토큰 로컬 저장 관리 + private suspend fun saveFcmToken(token: String) { + dataStore.edit { prefs -> prefs[FCM_TOKEN_KEY] = token } + } + + private suspend fun getFcmTokenOnce(): String? { + return dataStore.data.map { prefs -> prefs[FCM_TOKEN_KEY] }.first() + } + private suspend fun sendTokenToServer(token: String) { runCatching { val deviceId = getDeviceId() From e8bfcc71dfde4cf6a04b07a221beef2990f23b9b Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 11 Sep 2025 22:00:27 +0900 Subject: [PATCH 007/102] =?UTF-8?q?[feat]:=20FCM=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=A7=A4=EB=8B=88=EC=A0=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/texthip/thip/data/di/DataStoreModule.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/data/di/DataStoreModule.kt b/app/src/main/java/com/texthip/thip/data/di/DataStoreModule.kt index ab84768c..5f704668 100644 --- a/app/src/main/java/com/texthip/thip/data/di/DataStoreModule.kt +++ b/app/src/main/java/com/texthip/thip/data/di/DataStoreModule.kt @@ -22,4 +22,10 @@ object DataStoreModule { fun provideDataStore(@ApplicationContext context: Context): DataStore { return context.dataStore } + + @Provides + @Singleton + fun provideContext(@ApplicationContext context: Context): Context { + return context + } } From 66e1319a7aafd44a54158a6508ed9211cacd0f71 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 11 Sep 2025 22:00:43 +0900 Subject: [PATCH 008/102] =?UTF-8?q?[feat]:=20FCM=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20API=20=ED=99=94=EB=A9=B4=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/signin/viewmodel/LoginViewModel.kt | 19 ++++++++++++++++--- .../ui/signin/viewmodel/SignupViewModel.kt | 13 ++++++++++++- .../ui/signin/viewmodel/SplashViewModel.kt | 14 ++++++++++++-- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/LoginViewModel.kt b/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/LoginViewModel.kt index ff33d531..81ef9008 100644 --- a/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/LoginViewModel.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.manager.FcmTokenManager import com.texthip.thip.data.manager.TokenManager import com.texthip.thip.data.model.auth.response.AuthResponse import com.texthip.thip.data.repository.AuthRepository @@ -24,7 +25,8 @@ sealed interface LoginUiState { @HiltViewModel class LoginViewModel @Inject constructor( private val authRepository: AuthRepository, - private val tokenManager: TokenManager + private val tokenManager: TokenManager, + private val fcmTokenManager: FcmTokenManager ) : ViewModel() { private val _uiState = MutableStateFlow(LoginUiState.Idle) @@ -40,10 +42,12 @@ class LoginViewModel @Inject constructor( if (response != null) { if (response.isNewUser) { tokenManager.saveTempToken(response.token) // 신규 유저는 임시 토큰으로 저장 + _uiState.update { LoginUiState.Success(response) } } else { tokenManager.saveToken(response.token) // 기존 유저는 정식 토큰으로 저장 + // 기존 유저의 경우 FCM 토큰 전송 후 Success 상태 업데이트 + sendFcmTokenAndUpdateState(response) } - _uiState.update { LoginUiState.Success(response) } } else { _uiState.update { LoginUiState.Error("서버로부터 응답을 받지 못했습니다.") } } @@ -67,10 +71,12 @@ class LoginViewModel @Inject constructor( if (response != null) { if (response.isNewUser) { tokenManager.saveTempToken(response.token) // 신규 유저는 임시 토큰으로 저장 + _uiState.update { LoginUiState.Success(response) } } else { tokenManager.saveToken(response.token) // 기존 유저는 정식 토큰으로 저장 + // 기존 유저의 경우 FCM 토큰 전송 후 Success 상태 업데이트 + sendFcmTokenAndUpdateState(response) } - _uiState.update { LoginUiState.Success(response) } } else { _uiState.update { LoginUiState.Error("서버로부터 응답을 받지 못했습니다.") } } @@ -88,4 +94,11 @@ class LoginViewModel @Inject constructor( fun clearLoginState() { _uiState.update { LoginUiState.Idle } } + + private fun sendFcmTokenAndUpdateState(response: AuthResponse) { + viewModelScope.launch { + fcmTokenManager.sendCurrentTokenIfExists() + _uiState.update { LoginUiState.Success(response) } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupViewModel.kt index a89243fc..6026eae5 100644 --- a/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupViewModel.kt @@ -4,6 +4,7 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.R +import com.texthip.thip.data.manager.FcmTokenManager import com.texthip.thip.data.manager.TokenManager import com.texthip.thip.data.model.base.ThipApiFailureException import com.texthip.thip.data.model.users.request.SignupRequest @@ -31,7 +32,8 @@ data class SignupUiState( @HiltViewModel class SignupViewModel @Inject constructor( private val userRepository: UserRepository, - private val tokenManager: TokenManager + private val tokenManager: TokenManager, + private val fcmTokenManager: FcmTokenManager ) : ViewModel() { private val _uiState = MutableStateFlow(SignupUiState()) @@ -139,6 +141,9 @@ class SignupViewModel @Inject constructor( tokenManager.saveToken(signupResponse.accessToken) tokenManager.deleteTempToken() + // 회원가입 완료 후 FCM 토큰 전송 + sendFcmToken() + _uiState.update { it.copy(isLoading = false, isSignupSuccess = true) } } } @@ -151,4 +156,10 @@ class SignupViewModel @Inject constructor( } } } + + private fun sendFcmToken() { + viewModelScope.launch { + fcmTokenManager.sendCurrentTokenIfExists() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SplashViewModel.kt b/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SplashViewModel.kt index d0abc258..021bdb50 100644 --- a/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SplashViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SplashViewModel.kt @@ -2,9 +2,10 @@ package com.texthip.thip.ui.signin.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.manager.FcmTokenManager import com.texthip.thip.data.manager.TokenManager import dagger.hilt.android.lifecycle.HiltViewModel -import jakarta.inject.Inject +import javax.inject.Inject import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -18,7 +19,8 @@ sealed interface SplashDestination { @HiltViewModel class SplashViewModel @Inject constructor( - private val tokenManager: TokenManager + private val tokenManager: TokenManager, + private val fcmTokenManager: FcmTokenManager ) : ViewModel() { private val _destination = MutableStateFlow(SplashDestination.Loading) @@ -37,8 +39,16 @@ class SplashViewModel @Inject constructor( if (token.isNullOrBlank()) { _destination.value = SplashDestination.NavigateToLogin } else { + // 자동 로그인 시 FCM 토큰 전송 + sendFcmToken() _destination.value = SplashDestination.NavigateToHome } } } + + private fun sendFcmToken() { + viewModelScope.launch { + fcmTokenManager.sendCurrentTokenIfExists() + } + } } \ No newline at end of file From b8be3b70b6c36980bef695018fcfcf135abbc85f Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 11 Sep 2025 22:01:08 +0900 Subject: [PATCH 009/102] =?UTF-8?q?[feat]:=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=88=98=EC=A0=95=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=82=A0=EC=A7=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/mypage/screen/MypageNotificationEditScreen.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt index 10a75645..551ed438 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt @@ -30,10 +30,12 @@ import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.ToggleSwitchButton import com.texthip.thip.ui.common.modal.ToastWithDate import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar -import com.texthip.thip.ui.common.topappbar.InputTopAppBar import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography import kotlinx.coroutines.delay +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale @Composable fun NotificationScreen( @@ -41,6 +43,7 @@ fun NotificationScreen( ) { var isChecked by rememberSaveable { mutableStateOf(true) } var toastMessage by rememberSaveable { mutableStateOf(null) } + var toastDateTime by rememberSaveable { mutableStateOf("") } LaunchedEffect(toastMessage) { if (toastMessage != null) { @@ -69,7 +72,7 @@ fun NotificationScreen( message = stringResource( if (message == "push_on") R.string.push_on else R.string.push_off ), - date = "2025년 6월 29일 22시 30분", + date = toastDateTime, modifier = Modifier.fillMaxWidth() ) } @@ -113,6 +116,8 @@ fun NotificationScreen( onToggleChange = { isChecked = it toastMessage = if (it) "push_on" else "push_off" + val dateFormat = SimpleDateFormat("yyyy년 M월 d일 H시 m분", Locale.KOREAN) + toastDateTime = dateFormat.format(Date()) } ) } From 7bde62eb60cb3e02d7adcc8d4f9d5863e77bfd4f Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 11 Sep 2025 22:04:35 +0900 Subject: [PATCH 010/102] =?UTF-8?q?[feat]:=20FCM=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=A7=A4=EB=8B=88=EC=A0=80=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/data/manager/FcmTokenManager.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt b/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt index a48e822d..7a437248 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt @@ -84,18 +84,14 @@ class FcmTokenManager @Inject constructor( } private suspend fun sendTokenToServer(token: String) { - runCatching { - val deviceId = getDeviceId() - notificationRepository.registerFcmToken(deviceId, token) - }.onSuccess { - it.onSuccess { + val deviceId = getDeviceId() + notificationRepository.registerFcmToken(deviceId, token) + .onSuccess { Log.d("FCM", "Token sent successfully") - }.onFailure { exception -> + } + .onFailure { exception -> Log.e("FCM", "Failed to send token", exception) } - }.onFailure { exception -> - Log.e("FCM", "Error sending token", exception) - } } @SuppressLint("HardwareIds") From 8ce347df757b0b67f90ba29ebb5ce7b9675a560e Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 12 Sep 2025 17:51:12 +0900 Subject: [PATCH 011/102] =?UTF-8?q?[feat]:=20Firebase=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 2 +- .../texthip/thip/{ => service}/MyFirebaseMessagingService.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) rename app/src/main/java/com/texthip/thip/{ => service}/MyFirebaseMessagingService.kt (97%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bef34395..bfa3d942 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,7 +49,7 @@ diff --git a/app/src/main/java/com/texthip/thip/MyFirebaseMessagingService.kt b/app/src/main/java/com/texthip/thip/service/MyFirebaseMessagingService.kt similarity index 97% rename from app/src/main/java/com/texthip/thip/MyFirebaseMessagingService.kt rename to app/src/main/java/com/texthip/thip/service/MyFirebaseMessagingService.kt index 97776983..ea33b3a0 100644 --- a/app/src/main/java/com/texthip/thip/MyFirebaseMessagingService.kt +++ b/app/src/main/java/com/texthip/thip/service/MyFirebaseMessagingService.kt @@ -1,14 +1,15 @@ -package com.texthip.thip +package com.texthip.thip.service import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent -import android.content.Context import android.content.Intent import android.util.Log import androidx.core.app.NotificationCompat import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import com.texthip.thip.MainActivity +import com.texthip.thip.R import com.texthip.thip.data.manager.FcmTokenManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope From 392b15f104eb4c448ecd704cf081ff9b2430d12c Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 12 Sep 2025 18:03:05 +0900 Subject: [PATCH 012/102] =?UTF-8?q?[chore]:=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=A4=84=20=EB=B0=94=EA=BF=88?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/texthip/thip/MainActivity.kt | 3 +- .../main/java/com/texthip/thip/MainScreen.kt | 1 + .../java/com/texthip/thip/ThipApplication.kt | 4 +- .../com/texthip/thip/data/manager/Genre.kt | 4 +- .../texthip/thip/data/manager/TokenManager.kt | 6 +- .../thip/data/repository/BookRepository.kt | 30 +++++--- .../thip/data/repository/FeedRepository.kt | 75 +++++++++++-------- .../data/repository/RecentSearchRepository.kt | 8 +- .../thip/data/repository/RoomsRepository.kt | 25 +++++-- .../thip/data/repository/UserRepository.kt | 20 ++--- 10 files changed, 108 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/MainActivity.kt b/app/src/main/java/com/texthip/thip/MainActivity.kt index 4e86f830..9b441a96 100644 --- a/app/src/main/java/com/texthip/thip/MainActivity.kt +++ b/app/src/main/java/com/texthip/thip/MainActivity.kt @@ -22,6 +22,7 @@ import javax.inject.Inject class MainActivity : ComponentActivity() { @Inject lateinit var tokenManager: TokenManager + @Inject lateinit var authStateManager: AuthStateManager @@ -48,7 +49,7 @@ fun RootNavHost(authStateManager: AuthStateManager) { } } } - + NavHost( navController = navController, startDestination = CommonRoutes.Splash diff --git a/app/src/main/java/com/texthip/thip/MainScreen.kt b/app/src/main/java/com/texthip/thip/MainScreen.kt index 1b4ef1dd..77084060 100644 --- a/app/src/main/java/com/texthip/thip/MainScreen.kt +++ b/app/src/main/java/com/texthip/thip/MainScreen.kt @@ -38,6 +38,7 @@ fun MainScreen( MainTabRoutes.Feed -> { feedReselectionTrigger += 1 } + else -> { // 다른 탭들은 향후 확장 가능 } diff --git a/app/src/main/java/com/texthip/thip/ThipApplication.kt b/app/src/main/java/com/texthip/thip/ThipApplication.kt index 58da5b7b..b27e7032 100644 --- a/app/src/main/java/com/texthip/thip/ThipApplication.kt +++ b/app/src/main/java/com/texthip/thip/ThipApplication.kt @@ -8,7 +8,7 @@ import javax.inject.Inject @HiltAndroidApp -class ThipApplication : Application(){ +class ThipApplication : Application() { @Inject lateinit var tokenManager: TokenManager @@ -18,7 +18,7 @@ class ThipApplication : Application(){ // 카카오 SDK 초기화 try { KakaoSdk.init(this, BuildConfig.NATIVE_APP_KEY) - }catch (e: Exception){ + } catch (e: Exception) { e.printStackTrace() } } diff --git a/app/src/main/java/com/texthip/thip/data/manager/Genre.kt b/app/src/main/java/com/texthip/thip/data/manager/Genre.kt index 817cb49a..43fd51c7 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/Genre.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/Genre.kt @@ -1,8 +1,6 @@ package com.texthip.thip.data.manager -/** - * 도서 장르를 나타내는 enum class - */ + enum class Genre( val displayKey: String, val apiCategory: String, diff --git a/app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt b/app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt index db370f35..44b061f0 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt @@ -18,7 +18,7 @@ class TokenManager @Inject constructor( companion object { private val APP_TOKEN_KEY = stringPreferencesKey("app_token") // 정식 액세스토큰 private val TEMP_TOKEN_KEY = stringPreferencesKey("temp_token") // 임시 토큰 - private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token") + //private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token") } // ====== 정식 토큰 ====== @@ -54,13 +54,13 @@ class TokenManager @Inject constructor( } // ====== Refresh 토큰 (추후 확장용) ====== - suspend fun saveRefreshToken(token: String) { + /*suspend fun saveRefreshToken(token: String) { dataStore.edit { prefs -> prefs[REFRESH_TOKEN_KEY] = token } } suspend fun getRefreshTokenOnce(): String? { return dataStore.data.map { prefs -> prefs[REFRESH_TOKEN_KEY] }.first() - } + }*/ suspend fun clearTokens() { dataStore.edit { prefs -> prefs.clear() } diff --git a/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt index b4fb7072..2111476e 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt @@ -19,11 +19,15 @@ class BookRepository @Inject constructor( ) { /** 저장된 책 또는 모임 책 목록 조회 */ - suspend fun getBooks(type: String, cursor: String? = null): Result = runCatching { - bookService.getBooks(type, cursor) - .handleBaseResponse() - .getOrThrow() - } + suspend fun getBooks( + type: String, + cursor: String? = null + ): Result = + runCatching { + bookService.getBooks(type, cursor) + .handleBaseResponse() + .getOrThrow() + } /** 책 검색 */ suspend fun searchBooks( @@ -37,21 +41,27 @@ class BookRepository @Inject constructor( } /** 인기 책 조회 */ - suspend fun getMostSearchedBooks(): Result = runCatching { + suspend fun getMostSearchedBooks( + ): Result = runCatching { bookService.getMostSearchedBooks() .handleBaseResponse() .getOrThrow() } /** 책 상세 조회 */ - suspend fun getBookDetail(isbn: String): Result = runCatching { + suspend fun getBookDetail( + isbn: String + ): Result = runCatching { bookService.getBookDetail(isbn) .handleBaseResponse() .getOrThrow() } /** 책 저장/저장취소 */ - suspend fun saveBook(isbn: String, type: Boolean): Result = runCatching { + suspend fun saveBook( + isbn: String, + type: Boolean + ): Result = runCatching { bookService.saveBook(isbn, BookSaveRequest(type)) .handleBaseResponse() .getOrThrow() @@ -67,7 +77,9 @@ class BookRepository @Inject constructor( .getOrThrow() } - suspend fun getSavedBooks(cursor: String? = null): Result = runCatching { + suspend fun getSavedBooks( + cursor: String? = null + ): Result = runCatching { bookService.getSavedBooks(cursor) .handleBaseResponse() .getOrThrow() diff --git a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt index 93e2d12d..88e6d6f3 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt @@ -85,45 +85,48 @@ class FeedRepository @Inject constructor( } /** 이미지들을 S3에 업로드하고 CloudFront URL 목록 반환 */ - private suspend fun uploadImagesToS3(imageUris: List): List = withContext(Dispatchers.IO) { - val validImagePairs = imageUris.map { uri -> - async { - imageUploadHelper.getImageMetadata(uri)?.let { metadata -> - uri to metadata + private suspend fun uploadImagesToS3( + imageUris: List + ): List = + withContext(Dispatchers.IO) { + val validImagePairs = imageUris.map { uri -> + async { + imageUploadHelper.getImageMetadata(uri)?.let { metadata -> + uri to metadata + } } - } - }.awaitAll().filterNotNull() + }.awaitAll().filterNotNull() - if (validImagePairs.isEmpty()) return@withContext emptyList() + if (validImagePairs.isEmpty()) return@withContext emptyList() - val presignedUrlRequest = validImagePairs.map { it.second } - - val presignedResponse = feedService.getPresignedUrls(presignedUrlRequest) - .handleBaseResponse() - .getOrThrow() ?: throw Exception("Failed to get presigned URLs") + val presignedUrlRequest = validImagePairs.map { it.second } - // 개수 검증 - if (validImagePairs.size != presignedResponse.presignedUrls.size) { - throw Exception("Presigned URL count mismatch: expected ${validImagePairs.size}, got ${presignedResponse.presignedUrls.size}") - } + val presignedResponse = feedService.getPresignedUrls(presignedUrlRequest) + .handleBaseResponse() + .getOrThrow() ?: throw Exception("Failed to get presigned URLs") + + // 개수 검증 + if (validImagePairs.size != presignedResponse.presignedUrls.size) { + throw Exception("개수가 올바르지 않습니다: expected ${validImagePairs.size}, got ${presignedResponse.presignedUrls.size}") + } - val uploadedImageUrls = mutableListOf() + val uploadedImageUrls = mutableListOf() - validImagePairs.forEachIndexed { index, (uri, _) -> - val presignedInfo = presignedResponse.presignedUrls[index] + validImagePairs.forEachIndexed { index, (uri, _) -> + val presignedInfo = presignedResponse.presignedUrls[index] - imageUploadHelper.uploadImageToS3( - uri = uri, - presignedUrl = presignedInfo.presignedUrl - ).onSuccess { - uploadedImageUrls.add(presignedInfo.fileUrl) - }.onFailure { exception -> - throw Exception("Failed to upload image ${index + 1}: ${exception.message}") + imageUploadHelper.uploadImageToS3( + uri = uri, + presignedUrl = presignedInfo.presignedUrl + ).onSuccess { + uploadedImageUrls.add(presignedInfo.fileUrl) + }.onFailure { exception -> + throw Exception("Failed to upload image ${index + 1}: ${exception.message}") + } } - } - uploadedImageUrls - } + uploadedImageUrls + } /** 전체 피드 목록 조회 */ @@ -134,7 +137,9 @@ class FeedRepository @Inject constructor( } /** 내 피드 목록 조회 */ - suspend fun getMyFeeds(cursor: String? = null): Result = runCatching { + suspend fun getMyFeeds( + cursor: String? = null + ): Result = runCatching { feedService.getMyFeeds(cursor) .handleBaseResponse() .getOrThrow() @@ -159,7 +164,9 @@ class FeedRepository @Inject constructor( } /** 피드 상세 조회 */ - suspend fun getFeedDetail(feedId: Long): Result = runCatching { + suspend fun getFeedDetail( + feedId: Long + ): Result = runCatching { feedService.getFeedDetail(feedId) .handleBaseResponse() .getOrThrow() @@ -186,7 +193,9 @@ class FeedRepository @Inject constructor( } - suspend fun getFeedUsersInfo(userId: Long) = runCatching { + suspend fun getFeedUsersInfo( + userId: Long + ) = runCatching { feedService.getFeedUsersInfo(userId) .handleBaseResponse() .getOrThrow() diff --git a/app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt index 1b8e7067..3cc32ded 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt @@ -12,14 +12,18 @@ class RecentSearchRepository @Inject constructor( ) { /** 최근 검색어 조회 */ - suspend fun getRecentSearches(type: String): Result = runCatching { + suspend fun getRecentSearches( + type: String + ): Result = runCatching { recentSearchService.getRecentSearches(type) .handleBaseResponse() .getOrThrow() } /** 최근 검색어 삭제 */ - suspend fun deleteRecentSearch(recentSearchId: Int): Result = runCatching { + suspend fun deleteRecentSearch( + recentSearchId: Int + ): Result = runCatching { recentSearchService.deleteRecentSearch(recentSearchId) .handleBaseResponse() .getOrThrow() diff --git a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt index 9fb33aae..cf4208e4 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt @@ -44,7 +44,9 @@ class RoomsRepository @Inject constructor( } /** 내가 참여 중인 모임방 목록 조회 */ - suspend fun getMyJoinedRooms(cursor: String? = null): Result = runCatching { + suspend fun getMyJoinedRooms( + cursor: String? = null + ): Result = runCatching { val response = roomsService.getJoinedRooms(cursor) .handleBaseResponse() .getOrThrow() @@ -56,7 +58,9 @@ class RoomsRepository @Inject constructor( } /** 카테고리별 모임방 섹션 조회 (마감임박/인기) */ - suspend fun getRoomSections(genre: Genre? = null): Result = runCatching { + suspend fun getRoomSections( + genre: Genre? = null + ): Result = runCatching { val selectedGenre = genre ?: genreManager.getDefaultGenre() val apiCategory = genreManager.mapGenreToApiCategory(selectedGenre) @@ -76,14 +80,18 @@ class RoomsRepository @Inject constructor( } /** 모집중인 모임방 상세 정보 조회 */ - suspend fun getRoomRecruiting(roomId: Int): Result = runCatching { + suspend fun getRoomRecruiting( + roomId: Int + ): Result = runCatching { roomsService.getRoomRecruiting(roomId) .handleBaseResponse() .getOrThrow() } /** 새 모임방 생성 */ - suspend fun createRoom(request: CreateRoomRequest): Result = runCatching { + suspend fun createRoom( + request: CreateRoomRequest + ): Result = runCatching { val response = roomsService.createRoom(request) .handleBaseResponse() .getOrThrow() @@ -92,7 +100,10 @@ class RoomsRepository @Inject constructor( } /** 모임방 참여 또는 취소 */ - suspend fun joinOrCancelRoom(roomId: Int, type: String): Result = runCatching { + suspend fun joinOrCancelRoom( + roomId: Int, + type: String + ): Result = runCatching { val request = RoomJoinRequest(type = type) val response = roomsService.joinOrCancelRoom(roomId, request) .handleBaseResponse() @@ -116,7 +127,9 @@ class RoomsRepository @Inject constructor( } /** 모집 마감 */ - suspend fun closeRoom(roomId: Int): Result = runCatching { + suspend fun closeRoom( + roomId: Int + ): Result = runCatching { val response = roomsService.closeRoom(roomId) .handleBaseResponse() .getOrThrow() diff --git a/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt index dc7dfe02..60a32c84 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt @@ -63,11 +63,14 @@ class UserRepository @Inject constructor( .getOrThrow() } - suspend fun checkNickname(nickname: String): Result = runCatching { + suspend fun checkNickname( + nickname: String + ): Result = runCatching { userService.checkNickname(NicknameRequest(nickname)) .handleBaseResponse() .getOrThrow() } + suspend fun getAliasChoices(): Result = runCatching { userService.getAliasChoices() .handleBaseResponse() @@ -83,13 +86,10 @@ class UserRepository @Inject constructor( .handleBaseResponse() .getOrThrow() } -/* - suspend fun updateProfile(request: ProfileUpdateRequest): Result = runCatching { - userService.updateProfile(request) - .handleBaseResponse() - .getOrThrow() - }*/ - suspend fun updateProfile(request: ProfileUpdateRequest): Result { + + suspend fun updateProfile( + request: ProfileUpdateRequest + ): Result { return try { val response = userService.updateProfile(request) response.handleBaseResponse().getOrThrow() @@ -112,7 +112,9 @@ class UserRepository @Inject constructor( } - suspend fun signup(request: SignupRequest): Result { + suspend fun signup( + request: SignupRequest + ): Result { Log.d("SignupDebug", "UserRepository.signup() 호출됨. 요청 닉네임: ${request.nickname}") val tempToken = tokenManager.getTempTokenOnce() From e2069513f42a93af637d25d5f6062b6c2c7b10f3 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 14 Sep 2025 20:50:37 +0900 Subject: [PATCH 013/102] =?UTF-8?q?[refactor]:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=ED=83=88=ED=87=B4=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/mypage/screen/MypageLeavethipScreen.kt | 11 +++++++---- app/src/main/res/values/strings.xml | 14 ++++++++------ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt index e5d621c2..9af26080 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt @@ -104,25 +104,28 @@ fun DeleteAccountScreen( Text( text = buildAnnotatedString { append(stringResource(R.string.leave_thip_notice_1) + " ") + withStyle(style = SpanStyle(color = Red)) { + append(stringResource(R.string.leave_thip_notice_1_2)) + } + append(stringResource(R.string.leave_thip_notice_1_3)) withStyle(style = SpanStyle(color = Red)) { append(stringResource(R.string.leave_thip_notice_2)) } - append(" ") append(stringResource(R.string.leave_thip_notice_3)) }, - style = typography.copy_r400_s14, + style = typography.feedcopy_r400_s14_h20, color = colors.White ) Spacer(modifier = Modifier.height(20.dp)) Text( text = stringResource(R.string.leave_thip_notice_4), - style = typography.copy_r400_s14, + style = typography.feedcopy_r400_s14_h20, color = colors.White ) Spacer(modifier = Modifier.height(20.dp)) Text( text = stringResource(R.string.leave_thip_notice_5), - style = typography.copy_r400_s14, + style = typography.feedcopy_r400_s14_h20, color = colors.White ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 45d294a7..889a8971 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,12 +111,14 @@ THIP 떠나기 탈퇴 주의사항 - 회원탈퇴 시 계정정보는 - 복구 불가능 - 하며 90일 이후 재가입이 가능합니다. - -을 사유로 개인정보를 -개월 간 보존합니다. - 등록된 기록 및 게시물은 자동으로 삭제되지 않습니다.(or 삭제됩니다.) - 주의사항을 확인하였으며 이에 동의합니다. + 회원탈퇴 시 계정 및 활동 데이터는 + 즉시 삭제 + 되며, + 복구가 불가능 + 합니다. + 백업 및 로그 역시 보안 저장 후 최대 90일 내 자동 삭제됩니다. + 법령상 보존 의무가 있는 정보는 해당 기간 동안 보관됩니다. + 주의사항을 이해하였으며 이에 동의합니다. 탈퇴 완료 다음에 또 만나요! From 530aeba52810118fadde4ee1b5e41cf319bcf20f Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 14 Sep 2025 20:51:02 +0900 Subject: [PATCH 014/102] =?UTF-8?q?[refactor]:=20=EB=82=B4=20=EB=9D=B1=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=EC=9D=B4=20=EC=97=86=EC=9D=84=EB=95=8C=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=20=EC=88=98=EC=A0=95=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/ui/feed/component/MySubscribelistBar.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt b/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt index 8037a1b9..1343d3d0 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt @@ -134,6 +134,7 @@ fun MySubscribeBarlist( private fun EmptyMySubscriptionBar() { Box( modifier = Modifier + .padding(top = 8.dp) .fillMaxWidth() .height(42.dp) .clip(RoundedCornerShape(12.dp)) From 513e726d202244f795de7348d168a050546a08c7 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 14 Sep 2025 21:27:43 +0900 Subject: [PATCH 015/102] =?UTF-8?q?[feat]:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=ED=91=B8=EC=8B=9C=EC=95=8C=EB=A6=BC=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20API=20Response,=20Ser?= =?UTF-8?q?vice,=20Repository=20=EA=B5=AC=ED=98=84=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/appInsightsSettings.xml | 7 ++++--- .../response/NotificationEnableStateResponse.kt | 8 ++++++++ .../thip/data/repository/NotificationRepository.kt | 14 +++++++++++++- .../thip/data/service/NotificationService.kt | 8 ++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/texthip/thip/data/model/notification/response/NotificationEnableStateResponse.kt diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml index 1b7cae7d..9846d3e5 100644 --- a/.idea/appInsightsSettings.xml +++ b/.idea/appInsightsSettings.xml @@ -1,6 +1,7 @@ +