test #316
@@ -3,6 +3,9 @@ package kr.co.vividnext.sodalive.content.comment
 | 
				
			|||||||
import kr.co.vividnext.sodalive.common.ApiResponse
 | 
					import kr.co.vividnext.sodalive.common.ApiResponse
 | 
				
			||||||
import kr.co.vividnext.sodalive.common.SodaException
 | 
					import kr.co.vividnext.sodalive.common.SodaException
 | 
				
			||||||
import kr.co.vividnext.sodalive.member.Member
 | 
					import kr.co.vividnext.sodalive.member.Member
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.member.MemberService
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.useraction.ActionType
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.useraction.UserActionService
 | 
				
			||||||
import org.springframework.data.domain.Pageable
 | 
					import org.springframework.data.domain.Pageable
 | 
				
			||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
 | 
					import org.springframework.security.core.annotation.AuthenticationPrincipal
 | 
				
			||||||
import org.springframework.web.bind.annotation.GetMapping
 | 
					import org.springframework.web.bind.annotation.GetMapping
 | 
				
			||||||
@@ -14,7 +17,11 @@ import org.springframework.web.bind.annotation.RequestParam
 | 
				
			|||||||
import org.springframework.web.bind.annotation.RestController
 | 
					import org.springframework.web.bind.annotation.RestController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@RestController
 | 
					@RestController
 | 
				
			||||||
class AudioContentCommentController(private val service: AudioContentCommentService) {
 | 
					class AudioContentCommentController(
 | 
				
			||||||
 | 
					    private val service: AudioContentCommentService,
 | 
				
			||||||
 | 
					    private val memberService: MemberService,
 | 
				
			||||||
 | 
					    private val userActionService: UserActionService
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
    @PostMapping("/audio-content/comment")
 | 
					    @PostMapping("/audio-content/comment")
 | 
				
			||||||
    fun registerComment(
 | 
					    fun registerComment(
 | 
				
			||||||
        @RequestBody request: RegisterCommentRequest,
 | 
					        @RequestBody request: RegisterCommentRequest,
 | 
				
			||||||
@@ -22,7 +29,6 @@ class AudioContentCommentController(private val service: AudioContentCommentServ
 | 
				
			|||||||
    ) = run {
 | 
					    ) = run {
 | 
				
			||||||
        if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
 | 
					        if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ApiResponse.ok(
 | 
					 | 
				
			||||||
        service.registerComment(
 | 
					        service.registerComment(
 | 
				
			||||||
            comment = request.comment,
 | 
					            comment = request.comment,
 | 
				
			||||||
            audioContentId = request.contentId,
 | 
					            audioContentId = request.contentId,
 | 
				
			||||||
@@ -30,7 +36,20 @@ class AudioContentCommentController(private val service: AudioContentCommentServ
 | 
				
			|||||||
            isSecret = request.isSecret,
 | 
					            isSecret = request.isSecret,
 | 
				
			||||||
            member = member
 | 
					            member = member
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            val memberId = member.id!!
 | 
				
			||||||
 | 
					            val pushTokenList = memberService.getPushTokenList(recipient = memberId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            userActionService.recordAction(
 | 
				
			||||||
 | 
					                memberId = member.id!!,
 | 
				
			||||||
 | 
					                actionType = ActionType.CONTENT_COMMENT,
 | 
				
			||||||
 | 
					                pushTokenList = pushTokenList
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					        } catch (_: Exception) {
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ApiResponse.ok(Unit, "")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @PutMapping("/audio-content/comment")
 | 
					    @PutMapping("/audio-content/comment")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,8 +4,6 @@ import com.google.firebase.messaging.AndroidConfig
 | 
				
			|||||||
import com.google.firebase.messaging.ApnsConfig
 | 
					import com.google.firebase.messaging.ApnsConfig
 | 
				
			||||||
import com.google.firebase.messaging.Aps
 | 
					import com.google.firebase.messaging.Aps
 | 
				
			||||||
import com.google.firebase.messaging.FirebaseMessaging
 | 
					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.MessagingErrorCode
 | 
				
			||||||
import com.google.firebase.messaging.MulticastMessage
 | 
					import com.google.firebase.messaging.MulticastMessage
 | 
				
			||||||
import com.google.firebase.messaging.Notification
 | 
					import com.google.firebase.messaging.Notification
 | 
				
			||||||
@@ -38,7 +36,7 @@ class FcmService(private val pushTokenService: PushTokenService) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        while (attempt <= maxAttempts && targets.isNotEmpty()) {
 | 
					        while (attempt <= maxAttempts && targets.isNotEmpty()) {
 | 
				
			||||||
            val multicastMessage = MulticastMessage.builder()
 | 
					            val multicastMessage = MulticastMessage.builder()
 | 
				
			||||||
                .addAllTokens(tokens)
 | 
					                .addAllTokens(targets)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            multicastMessage.setAndroidConfig(
 | 
					            multicastMessage.setAndroidConfig(
 | 
				
			||||||
                AndroidConfig.builder()
 | 
					                AndroidConfig.builder()
 | 
				
			||||||
@@ -117,51 +115,69 @@ class FcmService(private val pushTokenService: PushTokenService) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun sendPointGranted(token: String, point: Int) {
 | 
					    fun sendPointGranted(tokens: List<String>, point: Int) {
 | 
				
			||||||
 | 
					        if (tokens.isEmpty()) return
 | 
				
			||||||
        val data = mapOf(
 | 
					        val data = mapOf(
 | 
				
			||||||
            "type" to "POINT_GRANTED",
 | 
					            "type" to "POINT_GRANTED",
 | 
				
			||||||
            "point" to point.toString(),
 | 
					            "point" to point.toString(),
 | 
				
			||||||
            "message" to "${point}포인트가 지급되었습니다!"
 | 
					            "message" to "${point}포인트가 지급되었습니다!"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var targets = tokens
 | 
				
			||||||
        var attempts = 0
 | 
					        var attempts = 0
 | 
				
			||||||
        val maxAttempts = 3
 | 
					        val maxAttempts = 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        while (attempts < maxAttempts) {
 | 
					        while (attempts <= maxAttempts && targets.isNotEmpty()) {
 | 
				
			||||||
            try {
 | 
					            val multicastMessage = MulticastMessage.builder()
 | 
				
			||||||
                val message = Message.builder()
 | 
					                .addAllTokens(targets)
 | 
				
			||||||
                    .setToken(token)
 | 
					 | 
				
			||||||
                .putAllData(data)
 | 
					                .putAllData(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            multicastMessage.setAndroidConfig(
 | 
				
			||||||
 | 
					                AndroidConfig.builder()
 | 
				
			||||||
 | 
					                    .setPriority(AndroidConfig.Priority.HIGH)
 | 
				
			||||||
                    .build()
 | 
					                    .build()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                val response = FirebaseMessaging.getInstance().send(message)
 | 
					            multicastMessage.setApnsConfig(
 | 
				
			||||||
                logger.info("[FCM] ✅ 성공 (attempt ${attempts + 1}): messageId=$response")
 | 
					                ApnsConfig.builder()
 | 
				
			||||||
                return // 성공 시 즉시 종료
 | 
					                    .setAps(
 | 
				
			||||||
            } catch (e: FirebaseMessagingException) {
 | 
					                        Aps.builder()
 | 
				
			||||||
                attempts++
 | 
					                            .setSound("default")
 | 
				
			||||||
 | 
					                            .build()
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .build()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // "registration-token-not-registered" 예외 코드 확인
 | 
					            val response = FirebaseMessaging.getInstance().sendEachForMulticast(multicastMessage.build())
 | 
				
			||||||
                if (e.messagingErrorCode == MessagingErrorCode.UNREGISTERED) {
 | 
					            val failedTokens = mutableListOf<String>()
 | 
				
			||||||
                    logger.error("[FCM] ❌ 실패: 토큰이 등록되지 않음 (등록 해제됨) → 재시도 안함")
 | 
					
 | 
				
			||||||
 | 
					            response.responses.forEachIndexed { index, res ->
 | 
				
			||||||
 | 
					                if (!res.isSuccessful) {
 | 
				
			||||||
 | 
					                    val exception = res.exception
 | 
				
			||||||
 | 
					                    val token = targets[index]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (exception?.messagingErrorCode == MessagingErrorCode.UNREGISTERED) {
 | 
				
			||||||
 | 
					                        logger.error("[FCM] ❌ UNREGISTERED → $token")
 | 
				
			||||||
                        // DB에서 삭제
 | 
					                        // DB에서 삭제
 | 
				
			||||||
                        pushTokenService.unregisterInvalidToken(token)
 | 
					                        pushTokenService.unregisterInvalidToken(token)
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        logger.error("[FCM] ❌ 실패: $token / ${exception?.messagingErrorCode}")
 | 
				
			||||||
 | 
					                        failedTokens.add(token)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (failedTokens.isEmpty()) {
 | 
				
			||||||
 | 
					                logger.info("[FCM] ✅ 전체 전송 성공")
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                logger.error("[FCM] ❌ 실패 (attempt $attempts): ${e.errorCode} - ${e.message}")
 | 
					            targets = failedTokens
 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (attempts >= maxAttempts) {
 | 
					 | 
				
			||||||
                    logger.error("[FCM] ❌ 최종 실패: 전송 불가")
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } catch (e: Exception) {
 | 
					 | 
				
			||||||
                // Firebase 이외의 예외도 잡기
 | 
					 | 
				
			||||||
            attempts++
 | 
					            attempts++
 | 
				
			||||||
                logger.error("[FCM] ❌ 실패 (attempt $attempts): ${e.message}")
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (attempts >= maxAttempts) {
 | 
					        if (targets.isNotEmpty()) {
 | 
				
			||||||
                    logger.error("[FCM] ❌ 최종 실패: 알 수 없는 오류")
 | 
					            logger.error("[FCM] ❌ 최종 실패 대상 ${targets.size}명 → $targets")
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -66,7 +66,11 @@ class MemberController(
 | 
				
			|||||||
        userActionService.recordAction(
 | 
					        userActionService.recordAction(
 | 
				
			||||||
            memberId = response.memberId,
 | 
					            memberId = response.memberId,
 | 
				
			||||||
            actionType = ActionType.SIGN_UP,
 | 
					            actionType = ActionType.SIGN_UP,
 | 
				
			||||||
            pushToken = request.pushToken
 | 
					            pushTokenList = if (request.pushToken != null) {
 | 
				
			||||||
 | 
					                listOf(request.pushToken)
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                emptyList()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse)
 | 
					        return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse)
 | 
				
			||||||
@@ -355,7 +359,11 @@ class MemberController(
 | 
				
			|||||||
            userActionService.recordAction(
 | 
					            userActionService.recordAction(
 | 
				
			||||||
                memberId = response.memberId,
 | 
					                memberId = response.memberId,
 | 
				
			||||||
                actionType = ActionType.SIGN_UP,
 | 
					                actionType = ActionType.SIGN_UP,
 | 
				
			||||||
                pushToken = request.pushToken
 | 
					                pushTokenList = if (request.pushToken != null) {
 | 
				
			||||||
 | 
					                    listOf(request.pushToken)
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    emptyList()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -386,7 +394,11 @@ class MemberController(
 | 
				
			|||||||
            userActionService.recordAction(
 | 
					            userActionService.recordAction(
 | 
				
			||||||
                memberId = response.memberId,
 | 
					                memberId = response.memberId,
 | 
				
			||||||
                actionType = ActionType.SIGN_UP,
 | 
					                actionType = ActionType.SIGN_UP,
 | 
				
			||||||
                pushToken = request.pushToken
 | 
					                pushTokenList = if (request.pushToken != null) {
 | 
				
			||||||
 | 
					                    listOf(request.pushToken)
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    emptyList()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,6 +64,8 @@ interface MemberQueryRepository {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    fun existsByNickname(nickname: String): Boolean
 | 
					    fun existsByNickname(nickname: String): Boolean
 | 
				
			||||||
    fun findNicknamesWithPrefix(prefix: String): List<String>
 | 
					    fun findNicknamesWithPrefix(prefix: String): List<String>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun getPushTokenList(memberId: Long): List<String>
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Repository
 | 
					@Repository
 | 
				
			||||||
@@ -507,4 +509,17 @@ class MemberQueryRepositoryImpl(
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
            .fetch()
 | 
					            .fetch()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun getPushTokenList(memberId: Long): List<String> {
 | 
				
			||||||
 | 
					        val where = member.isActive.isTrue
 | 
				
			||||||
 | 
					            .and(member.email.notIn("admin@sodalive.net"))
 | 
				
			||||||
 | 
					            .and(member.id.eq(memberId))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return queryFactory
 | 
				
			||||||
 | 
					            .select(pushToken.token)
 | 
				
			||||||
 | 
					            .from(member)
 | 
				
			||||||
 | 
					            .innerJoin(pushToken).on(member.id.eq(pushToken.member.id))
 | 
				
			||||||
 | 
					            .where(where)
 | 
				
			||||||
 | 
					            .fetch()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -907,6 +907,12 @@ class MemberService(
 | 
				
			|||||||
        return MemberResolveResult(member = member, isNew = true)
 | 
					        return MemberResolveResult(member = member, isNew = true)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun getPushTokenList(recipient: Long): List<String> {
 | 
				
			||||||
 | 
					        return repository.getPushTokenList(recipient)
 | 
				
			||||||
 | 
					            .toSet()
 | 
				
			||||||
 | 
					            .toList()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun checkEmail(email: String) {
 | 
					    private fun checkEmail(email: String) {
 | 
				
			||||||
        val member = repository.findByEmail(email)
 | 
					        val member = repository.findByEmail(email)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.member.auth
 | 
				
			|||||||
import kr.co.vividnext.sodalive.common.ApiResponse
 | 
					import kr.co.vividnext.sodalive.common.ApiResponse
 | 
				
			||||||
import kr.co.vividnext.sodalive.common.SodaException
 | 
					import kr.co.vividnext.sodalive.common.SodaException
 | 
				
			||||||
import kr.co.vividnext.sodalive.member.Member
 | 
					import kr.co.vividnext.sodalive.member.Member
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.member.MemberService
 | 
				
			||||||
import kr.co.vividnext.sodalive.useraction.ActionType
 | 
					import kr.co.vividnext.sodalive.useraction.ActionType
 | 
				
			||||||
import kr.co.vividnext.sodalive.useraction.UserActionService
 | 
					import kr.co.vividnext.sodalive.useraction.UserActionService
 | 
				
			||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
 | 
					import org.springframework.security.core.annotation.AuthenticationPrincipal
 | 
				
			||||||
@@ -15,6 +16,7 @@ import org.springframework.web.bind.annotation.RestController
 | 
				
			|||||||
@RequestMapping("/auth")
 | 
					@RequestMapping("/auth")
 | 
				
			||||||
class AuthController(
 | 
					class AuthController(
 | 
				
			||||||
    private val service: AuthService,
 | 
					    private val service: AuthService,
 | 
				
			||||||
 | 
					    private val memberService: MemberService,
 | 
				
			||||||
    private val userActionService: UserActionService
 | 
					    private val userActionService: UserActionService
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
    @PostMapping
 | 
					    @PostMapping
 | 
				
			||||||
@@ -33,11 +35,16 @@ class AuthController(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        val authResponse = service.authenticate(authenticateData, member.id!!)
 | 
					        val authResponse = service.authenticate(authenticateData, member.id!!)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            val memberId = member.id!!
 | 
				
			||||||
 | 
					            val pushTokenList = memberService.getPushTokenList(recipient = memberId)
 | 
				
			||||||
            userActionService.recordAction(
 | 
					            userActionService.recordAction(
 | 
				
			||||||
                memberId = member.id!!,
 | 
					                memberId = member.id!!,
 | 
				
			||||||
                actionType = ActionType.USER_AUTHENTICATION,
 | 
					                actionType = ActionType.USER_AUTHENTICATION,
 | 
				
			||||||
            pushToken = member.pushToken
 | 
					                pushTokenList = pushTokenList
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					        } catch (_: Exception) {
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ApiResponse.ok(authResponse)
 | 
					        ApiResponse.ok(authResponse)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,12 @@ class UserActionService(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private val coroutineScope = CoroutineScope(Dispatchers.IO)
 | 
					    private val coroutineScope = CoroutineScope(Dispatchers.IO)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun recordAction(memberId: Long, actionType: ActionType, orderId: Long? = null, pushToken: String? = null) {
 | 
					    fun recordAction(
 | 
				
			||||||
 | 
					        memberId: Long,
 | 
				
			||||||
 | 
					        actionType: ActionType,
 | 
				
			||||||
 | 
					        orderId: Long? = null,
 | 
				
			||||||
 | 
					        pushTokenList: List<String> = emptyList()
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
        coroutineScope.launch {
 | 
					        coroutineScope.launch {
 | 
				
			||||||
            val now = LocalDateTime.now()
 | 
					            val now = LocalDateTime.now()
 | 
				
			||||||
            repository.save(UserActionLog(memberId, actionType))
 | 
					            repository.save(UserActionLog(memberId, actionType))
 | 
				
			||||||
@@ -73,8 +78,8 @@ class UserActionService(
 | 
				
			|||||||
                    )
 | 
					                    )
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (pushToken != null) {
 | 
					                if (pushTokenList.isNotEmpty()) {
 | 
				
			||||||
                    fcmService.sendPointGranted(pushToken, policy.pointAmount)
 | 
					                    fcmService.sendPointGranted(pushTokenList, policy.pointAmount)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user