From 205cfe08991bc7ba4c612dffeeaae8a1a3ea26ef Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 13 Mar 2026 22:57:15 +0900 Subject: [PATCH 1/2] =?UTF-8?q?docs(push-notification):=20=ED=91=B8?= =?UTF-8?q?=EC=8B=9C=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=A0=80=EC=9E=A5=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84=20=EC=9E=91=EC=97=85=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/20260313_푸시시스템카테고리저장정책보완.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/20260313_푸시시스템카테고리저장정책보완.md diff --git a/docs/20260313_푸시시스템카테고리저장정책보완.md b/docs/20260313_푸시시스템카테고리저장정책보완.md new file mode 100644 index 00000000..949719da --- /dev/null +++ b/docs/20260313_푸시시스템카테고리저장정책보완.md @@ -0,0 +1,15 @@ +- [x] 리뷰 결과 요약 및 수정 범위 확정 +- [x] FcmEvent 저장 조건 제거 및 서비스 계층으로 정책 이동 +- [x] PushNotificationService에서 SYSTEM 저장 제외 보장 +- [x] category null 회귀 방지 테스트 추가 +- [x] 검증 실행 (LSP, 테스트, 빌드) + +## 검증 기록 + +### 1차 구현 +- 무엇을: `SYSTEM` 카테고리 저장 제외 정책을 Listener에서 Service로 이동하고, `category = null` 회귀를 막는 테스트를 추가했다. +- 왜: 현재 Listener 조건은 `category != null`을 요구해 타입 기반 카테고리 보정(`resolveCategory`)을 우회할 수 있어, 비SYSTEM 이벤트의 저장 누락 위험이 있었다. +- 어떻게: + - `lsp_diagnostics` 실행: Kotlin LSP 미설정으로 불가(환경상 `.kt` 진단 서버 없음). + - `./gradlew test --tests kr.co.vividnext.sodalive.fcm.notification.PushNotificationServiceTest` 실행: 성공. + - `./gradlew build` 실행: 성공. -- 2.49.1 From 7251939107fee1506c005987cc9119e42e3fe8a4 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 13 Mar 2026 22:57:37 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(fcm):=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=A0=9C=EC=99=B8=20=EC=A0=95=EC=B1=85?= =?UTF-8?q?=EC=9D=84=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../can/charge/event/ChargeEventService.kt | 4 +- .../notification/PushNotificationService.kt | 1 + .../PushNotificationServiceTest.kt | 56 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/event/ChargeEventService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/event/ChargeEventService.kt index 8d71de0d..1b644e5b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/event/ChargeEventService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/event/ChargeEventService.kt @@ -79,7 +79,7 @@ class ChargeEventService( applicationEventPublisher.publishEvent( FcmEvent( type = FcmEventType.INDIVIDUAL, - category = PushNotificationCategory.MESSAGE, + category = PushNotificationCategory.SYSTEM, title = chargeEvent.title, messageKey = "can.charge.event.additional_can_paid", args = listOf(additionalCan), @@ -103,7 +103,7 @@ class ChargeEventService( applicationEventPublisher.publishEvent( FcmEvent( type = FcmEventType.INDIVIDUAL, - category = PushNotificationCategory.MESSAGE, + category = PushNotificationCategory.SYSTEM, titleKey = "can.charge.event.first_title", messageKey = "can.charge.event.additional_can_paid", args = listOf(additionalCan), diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationService.kt index ba4ce54c..ec34350c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationService.kt @@ -48,6 +48,7 @@ class PushNotificationService( if (recipientMemberIds.isEmpty()) return val category = resolveCategory(fcmEvent) ?: return + if (category == PushNotificationCategory.SYSTEM) return val senderSnapshot = resolveSenderSnapshot(fcmEvent) val deepLink = FcmService.buildDeepLink( serverEnv = serverEnv, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationServiceTest.kt index 8848711d..9a47c9e6 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationServiceTest.kt @@ -78,6 +78,62 @@ class PushNotificationServiceTest { Mockito.verify(pushNotificationListRepository, Mockito.never()).save(Mockito.any(PushNotificationList::class.java)) } + @Test + fun shouldNotSaveWhenResolvedCategoryIsSystem() { + // given: 이벤트 category가 null이고 타입 기반 보정 결과가 SYSTEM인 상황을 준비한다. + val event = FcmEvent( + type = FcmEventType.INDIVIDUAL, + category = null, + recipients = listOf(1L) + ) + val pushTokens = listOf(PushTokenInfo(token = "token-1", deviceType = "aos", languageCode = "ko")) + + Mockito.`when`(pushTokenRepository.findMemberIdsByTokenIn(listOf("token-1"))).thenReturn(listOf(1L)) + + // when: 알림 적재를 실행한다. + service.saveNotification( + fcmEvent = event, + languageCode = "ko", + translatedMessage = "시스템 알림", + recipientPushTokens = pushTokens + ) + + // then: SYSTEM 카테고리 보정 결과에 따라 저장이 발생하지 않아야 한다. + Mockito.verify(pushNotificationListRepository, Mockito.never()).save(Mockito.any(PushNotificationList::class.java)) + } + + @Test + fun shouldSaveWhenCategoryIsNullAndResolvedCategoryIsNonSystem() { + // given: 이벤트 category가 null이어도 타입 기반 보정 결과가 LIVE면 저장되어야 한다. + val event = FcmEvent( + type = FcmEventType.START_LIVE, + category = null, + roomId = 11L, + creatorId = 20L, + deepLinkValue = FcmDeepLinkValue.LIVE, + deepLinkId = 11L + ) + val pushTokens = listOf(PushTokenInfo(token = "token-a", deviceType = "aos", languageCode = "ko")) + + Mockito.`when`(pushTokenRepository.findMemberIdsByTokenIn(listOf("token-a"))).thenReturn(listOf(10L)) + Mockito.`when`(pushNotificationListRepository.save(Mockito.any(PushNotificationList::class.java))) + .thenAnswer { invocation -> invocation.getArgument(0) } + + // when: 알림 적재를 실행한다. + service.saveNotification( + fcmEvent = event, + languageCode = "ko", + translatedMessage = "라이브가 시작되었습니다.", + recipientPushTokens = pushTokens + ) + + // then: 보정된 LIVE 카테고리로 저장되어야 한다. + val captor = ArgumentCaptor.forClass(PushNotificationList::class.java) + Mockito.verify(pushNotificationListRepository).save(captor.capture()) + val saved = captor.value + assertEquals(PushNotificationCategory.LIVE, saved.category) + } + @Test fun shouldSaveChunkedRecipientsAndSenderSnapshotWhenEventIsValid() { // given: 1001명의 수신자를 가진 유효 이벤트를 준비한다. -- 2.49.1