-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/fcm 토큰 사용자 기기별 관리 추가 #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "feat/FCM-\uD1A0\uD070-\uC0AC\uC6A9\uC790-\uAE30\uAE30\uBCC4-\uAD00\uB9AC-\uCD94\uAC00"
Changes from all commits
8c18f1e
55048b4
eac16fc
4e30ff2
bee13e9
e5162d9
93fa607
759322d
c5f8d8b
3ae6c8c
269fd8c
b3b93aa
9e37261
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,9 +3,14 @@ | |
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| public interface PushSubscriptionRepository extends JpaRepository<PushSubscription, Long> { | ||
| Optional<PushSubscription> findByMemberIdAndDeviceId(Long memberId, String deviceId); | ||
|
|
||
| List<PushSubscription> findAllByMemberId(Long memberId); | ||
|
|
||
| void deleteByToken(String token); | ||
|
|
||
| void deleteByMemberIdAndDeviceId(Long memberId, String deviceId); | ||
|
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,6 +13,8 @@ | |||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import java.util.Optional; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Slf4j | ||||||||||||||||||||
| @Service | ||||||||||||||||||||
| public class FcmService implements PushService { | ||||||||||||||||||||
|
|
@@ -44,20 +46,31 @@ public void sendPushMessage(String token, Notification notification) { | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Override | ||||||||||||||||||||
| public boolean isSubscribed(Long memberId, String deviceId) { | ||||||||||||||||||||
| return pushSubscriptionRepository.findByMemberIdAndDeviceId(memberId, deviceId).isPresent(); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+49
to
+52
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| @Override | ||||||||||||||||||||
| public String getVapidPublicKey() { | ||||||||||||||||||||
| return vapidPublicKey; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Override | ||||||||||||||||||||
| @Transactional | ||||||||||||||||||||
| public void subscribe(Long memberId, String token) { | ||||||||||||||||||||
| pushSubscriptionRepository.save(new PushSubscription(memberId, token)); | ||||||||||||||||||||
| public void subscribe(Long memberId, String deviceId, String token) { | ||||||||||||||||||||
| Optional<PushSubscription> byMemberIdAndDeviceId = pushSubscriptionRepository.findByMemberIdAndDeviceId(memberId, deviceId); | ||||||||||||||||||||
| if (byMemberIdAndDeviceId.isPresent()) { | ||||||||||||||||||||
| PushSubscription existingSubscription = byMemberIdAndDeviceId.get(); | ||||||||||||||||||||
| existingSubscription.updateToken(token); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| pushSubscriptionRepository.save(new PushSubscription(memberId, deviceId, token)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Override | ||||||||||||||||||||
| @Transactional | ||||||||||||||||||||
| public void unsubscribe(Long memberId, String token) { | ||||||||||||||||||||
| pushSubscriptionRepository.delete(new PushSubscription(memberId, token)); | ||||||||||||||||||||
| public void unsubscribe(Long memberId, String deviceId, String token) { | ||||||||||||||||||||
| pushSubscriptionRepository.deleteByMemberIdAndDeviceId(memberId, deviceId); | ||||||||||||||||||||
|
||||||||||||||||||||
| pushSubscriptionRepository.deleteByMemberIdAndDeviceId(memberId, deviceId); | |
| pushSubscriptionRepository.findByMemberIdAndDeviceId(memberId, deviceId) | |
| .ifPresent(subscription -> { | |
| if (token != null && token.equals(subscription.getToken())) { | |
| pushSubscriptionRepository.deleteByMemberIdAndDeviceId(memberId, deviceId); | |
| } else { | |
| log.warn("unsubscribe 요청의 토큰이 저장된 값과 일치하지 않습니다. memberId={}, deviceId={}", memberId, deviceId); | |
| } | |
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,20 +1,16 @@ | ||||||||||||||||
| package me.pinitnotification.interfaces.notification; | ||||||||||||||||
|
|
||||||||||||||||
| import me.pinitnotification.application.push.PushService; | ||||||||||||||||
| import me.pinitnotification.domain.member.MemberId; | ||||||||||||||||
| import io.swagger.v3.oas.annotations.Operation; | ||||||||||||||||
| import io.swagger.v3.oas.annotations.Parameter; | ||||||||||||||||
| import io.swagger.v3.oas.annotations.media.Content; | ||||||||||||||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||||||||||||||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||||||||||||||||
| import io.swagger.v3.oas.annotations.responses.ApiResponses; | ||||||||||||||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||||||||||||||
| import me.pinitnotification.application.push.PushService; | ||||||||||||||||
| import me.pinitnotification.domain.member.MemberId; | ||||||||||||||||
| import me.pinitnotification.interfaces.notification.dto.PushTokenRequest; | ||||||||||||||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||||||||||||||
| import org.springframework.web.bind.annotation.PostMapping; | ||||||||||||||||
| import org.springframework.web.bind.annotation.RequestBody; | ||||||||||||||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||||||||||||||
| import org.springframework.web.bind.annotation.RestController; | ||||||||||||||||
| import org.springframework.web.bind.annotation.*; | ||||||||||||||||
|
||||||||||||||||
| import org.springframework.web.bind.annotation.*; | |
| import org.springframework.web.bind.annotation.GetMapping; | |
| import org.springframework.web.bind.annotation.PostMapping; | |
| import org.springframework.web.bind.annotation.RequestBody; | |
| import org.springframework.web.bind.annotation.RequestMapping; | |
| import org.springframework.web.bind.annotation.RequestParam; | |
| import org.springframework.web.bind.annotation.RestController; |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문제점: isSubscribed API의 deviceId 파라미터에 대한 검증이 없습니다.
영향: null 또는 빈 값이 전달되면 데이터베이스 쿼리가 예상치 못한 결과를 반환할 수 있습니다.
수정 제안: @RequestParam에 @notblank 어노테이션을 추가하여 빈 값을 방지하세요.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ | |
|
|
||
| @Schema(description = "푸시 토큰 요청 바디") | ||
| public record PushTokenRequest( | ||
| @Schema(description = "사용자의 디바이스 식별자. UUID 형식으로 제공됩니다.", example = "123e4567-e89b-12d3-a456-426614174000") | ||
| String deviceId, | ||
|
Comment on lines
+7
to
+8
|
||
| @Schema(description = "클라이언트에서 발급받은 FCM 푸시 토큰", example = "fcm-token-example") | ||
| String token | ||
| ) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |||||
|
|
||||||
| vapid: | ||||||
| keys: | ||||||
| public: BA7L1jqFAyxo_KZZZBkUeqC1uY6iSP1NvyNVqV-L_dmqEnHA__5cNs87WJ5QDiH8UC3hgLQu99mbbxlwZZf7J4U | ||||||
| public: BF8QQIULasLr94n0l0xbv43yZeNICudM5lpQN08VYn2g5VjBPU0wM98HypyRmEb-y0ARRsiZ_wcgSMIC-nq-x20 | ||||||
|
||||||
| public: BF8QQIULasLr94n0l0xbv43yZeNICudM5lpQN08VYn2g5VjBPU0wM98HypyRmEb-y0ARRsiZ_wcgSMIC-nq-x20 | |
| public: ${VAPID_PUBLIC_KEY} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,12 +16,7 @@ | |
| import java.time.ZoneOffset; | ||
| import java.util.List; | ||
|
|
||
| import static org.mockito.Mockito.any; | ||
| import static org.mockito.Mockito.anyString; | ||
| import static org.mockito.Mockito.eq; | ||
| import static org.mockito.Mockito.never; | ||
| import static org.mockito.Mockito.verify; | ||
| import static org.mockito.Mockito.when; | ||
| import static org.mockito.Mockito.*; | ||
|
||
|
|
||
| @ExtendWith(MockitoExtension.class) | ||
| class NotificationDispatchSchedulerTest { | ||
|
|
@@ -49,7 +44,7 @@ void dispatchDueNotifications_sendsAndDeletesPastNotifications() { | |
|
|
||
| when(notificationRepository.findAll()).thenReturn(List.of(past, future)); | ||
| when(pushSubscriptionRepository.findAllByMemberId(1L)) | ||
| .thenReturn(List.of(new PushSubscription(1L, "token-1"), new PushSubscription(1L, "token-2"))); | ||
| .thenReturn(List.of(new PushSubscription(1L, "device-1", "token-1"), new PushSubscription(1L, "device-2", "token-2"))); | ||
|
|
||
| scheduler.dispatchDueNotifications(); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문제점:
PushSubscription엔티티에 토큰을 업데이트할 수 있는 setter 메서드가 없습니다.영향: FCM 토큰이 갱신되는 경우 기존 구독의 토큰을 업데이트할 수 없어, 잘못된 토큰으로 푸시 알림을 전송하게 됩니다.
수정 제안: 토큰을 업데이트할 수 있는 public 메서드(예:
updateToken(String token))를 추가하세요.