diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt index 3b0a9ab..1f96de1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt @@ -4,6 +4,9 @@ import com.google.firebase.messaging.AndroidConfig import com.google.firebase.messaging.ApnsConfig import com.google.firebase.messaging.Aps import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.messaging.FirebaseMessagingException +import com.google.firebase.messaging.Message +import com.google.firebase.messaging.MessagingErrorCode import com.google.firebase.messaging.MulticastMessage import com.google.firebase.messaging.Notification import org.slf4j.LoggerFactory @@ -82,8 +85,54 @@ class FcmService { } val response = FirebaseMessaging.getInstance().sendEachForMulticast(multicastMessage.build()) - logger.info("보내기 성공: ${response.successCount}") - logger.info("보내기 실패: ${response.failureCount}") + logger.info("[FCM] ✅ 성공: ${response.successCount}") + logger.info("[FCM] ❌ 실패: ${response.failureCount}") + } + } + + fun sendPointGranted(token: String, point: Int) { + val data = mapOf( + "type" to "POINT_GRANTED", + "point" to point.toString(), + "message" to "${point}포인트가 지급되었습니다!" + ) + + var attempts = 0 + val maxAttempts = 3 + + while (attempts < maxAttempts) { + try { + val message = Message.builder() + .setToken(token) + .putAllData(data) + .build() + + val response = FirebaseMessaging.getInstance().send(message) + logger.info("[FCM] ✅ 성공 (attempt ${attempts + 1}): messageId=$response") + return // 성공 시 즉시 종료 + } catch (e: FirebaseMessagingException) { + attempts++ + + // "registration-token-not-registered" 예외 코드 확인 + if (e.messagingErrorCode == MessagingErrorCode.UNREGISTERED) { + logger.error("[FCM] ❌ 실패: 토큰이 등록되지 않음 (등록 해제됨) → 재시도 안함") + return + } + + logger.error("[FCM] ❌ 실패 (attempt $attempts): ${e.errorCode} - ${e.message}") + + if (attempts >= maxAttempts) { + logger.error("[FCM] ❌ 최종 실패: 전송 불가") + } + } catch (e: Exception) { + // Firebase 이외의 예외도 잡기 + attempts++ + logger.error("[FCM] ❌ 실패 (attempt $attempts): ${e.message}") + + if (attempts >= maxAttempts) { + logger.error("[FCM] ❌ 최종 실패: 알 수 없는 오류") + } + } } } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index e26be41..0da7a69 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -65,7 +65,8 @@ class MemberController( userActionService.recordAction( memberId = response.memberId, - actionType = ActionType.SIGN_UP + actionType = ActionType.SIGN_UP, + pushToken = request.pushToken ) return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) @@ -340,7 +341,7 @@ class MemberController( } val token = authHeader.substring(7) - val response = googleAuthService.authenticate(token, request.container, request.marketingPid) + val response = googleAuthService.authenticate(token, request.container, request.marketingPid, request.pushToken) if (!response.marketingPid.isNullOrBlank()) { trackingService.saveTrackingHistory( @@ -350,6 +351,12 @@ class MemberController( ) } + userActionService.recordAction( + memberId = response.memberId, + actionType = ActionType.SIGN_UP, + pushToken = request.pushToken + ) + return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) } @@ -363,7 +370,7 @@ class MemberController( } val token = authHeader.substring(7) - val response = kakaoAuthService.authenticate(token, request.container, request.marketingPid) + val response = kakaoAuthService.authenticate(token, request.container, request.marketingPid, request.pushToken) if (!response.marketingPid.isNullOrBlank()) { trackingService.saveTrackingHistory( @@ -373,6 +380,12 @@ class MemberController( ) } + userActionService.recordAction( + memberId = response.memberId, + actionType = ActionType.SIGN_UP, + pushToken = request.pushToken + ) + return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 4308bc6..0564d82 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -125,6 +125,7 @@ class MemberService( gender = Gender.NONE, container = request.container ) + member.pushToken = request.pushToken if (!request.marketingPid.isNullOrBlank()) { member.activePid = request.marketingPid @@ -780,7 +781,12 @@ class MemberService( } @Transactional - fun findOrRegister(googleUserInfo: GoogleUserInfo, container: String, marketingPid: String?): Member { + fun findOrRegister( + googleUserInfo: GoogleUserInfo, + container: String, + marketingPid: String?, + pushToken: String? + ): Member { val findMember = repository.findByGoogleId(googleUserInfo.sub) if (findMember != null) { if (findMember.isActive) { @@ -810,6 +816,7 @@ class MemberService( provider = MemberProvider.GOOGLE, container = container ) + member.pushToken = pushToken if (!marketingPid.isNullOrBlank()) { member.activePid = marketingPid @@ -823,7 +830,12 @@ class MemberService( } @Transactional - fun findOrRegister(kakaoUserInfo: KakaoUserInfo, container: String, marketingPid: String?): Member { + fun findOrRegister( + kakaoUserInfo: KakaoUserInfo, + container: String, + marketingPid: String?, + pushToken: String? + ): Member { val findMember = repository.findByKakaoId(kakaoUserInfo.id) if (findMember != null) { if (findMember.isActive) { @@ -853,6 +865,7 @@ class MemberService( provider = MemberProvider.KAKAO, container = container ) + member.pushToken = pushToken if (!marketingPid.isNullOrBlank()) { member.activePid = marketingPid diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt index da16dc4..0895b91 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt @@ -7,4 +7,8 @@ data class LoginRequest( val isCreator: Boolean = false ) -data class SocialLoginRequest(val container: String, val marketingPid: String? = null) +data class SocialLoginRequest( + val container: String, + val pushToken: String? = null, + val marketingPid: String? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt index de4a6fa..03ccc0f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt @@ -16,6 +16,7 @@ data class SignUpRequest( data class SignUpRequestV2( val email: String, val password: String, + val pushToken: String? = null, val marketingPid: String? = null, val isAgreeTermsOfService: Boolean, val isAgreePrivacyPolicy: Boolean, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleAuthService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleAuthService.kt index 495e269..0d39c74 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleAuthService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleAuthService.kt @@ -19,10 +19,15 @@ class GoogleAuthService( @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) { - fun authenticate(idToken: String, container: String, marketingPid: String?): SocialLoginResponse { + fun authenticate( + idToken: String, + container: String, + marketingPid: String?, + pushToken: String? + ): SocialLoginResponse { val googleUserInfo = googleService.getUserInfo(idToken) ?: throw SodaException("구글 로그인을 하지 못했습니다. 다시 시도해 주세요") - val member = memberService.findOrRegister(googleUserInfo, container, marketingPid) + val member = memberService.findOrRegister(googleUserInfo, container, marketingPid, pushToken) val principal = MemberAdapter(member) val authToken = GoogleAuthenticationToken(idToken, principal.authorities) authToken.setPrincipal(principal) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoAuthService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoAuthService.kt index 49525bf..026aa79 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoAuthService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoAuthService.kt @@ -19,10 +19,15 @@ class KakaoAuthService( @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) { - fun authenticate(accessToken: String, container: String, marketingPid: String?): SocialLoginResponse { + fun authenticate( + accessToken: String, + container: String, + marketingPid: String?, + pushToken: String? + ): SocialLoginResponse { val kakaoUserInfo = kakaoService.getUserInfo(accessToken) ?: throw SodaException("카카오 로그인을 하지 못했습니다. 다시 시도해 주세요") - val member = memberService.findOrRegister(kakaoUserInfo, container, marketingPid) + val member = memberService.findOrRegister(kakaoUserInfo, container, marketingPid, pushToken) val principal = MemberAdapter(member) val authToken = KakaoAuthenticationToken(accessToken, principal.authorities) authToken.setPrincipal(principal) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionService.kt index 1128897..b3f0236 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/useraction/UserActionService.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.useraction import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kr.co.vividnext.sodalive.fcm.FcmService import kr.co.vividnext.sodalive.point.MemberPoint import kr.co.vividnext.sodalive.point.MemberPointRepository import kr.co.vividnext.sodalive.point.PointGrantLog @@ -16,12 +17,14 @@ class UserActionService( private val repository: UserActionLogRepository, private val policyRepository: PointRewardPolicyRepository, private val grantLogRepository: PointGrantLogRepository, - private val memberPointRepository: MemberPointRepository + private val memberPointRepository: MemberPointRepository, + + private val fcmService: FcmService ) { private val coroutineScope = CoroutineScope(Dispatchers.IO) - fun recordAction(memberId: Long, actionType: ActionType) { + fun recordAction(memberId: Long, actionType: ActionType, pushToken: String?) { coroutineScope.launch { val now = LocalDateTime.now() repository.save(UserActionLog(memberId, actionType)) @@ -56,6 +59,10 @@ class UserActionService( policyId = policy.id!! ) ) + + if (pushToken != null) { + fcmService.sendPointGranted(pushToken, policy.pointAmount) + } } } }