-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: 디바이스 관련 엔티티 정의 * chore: DB 마이그레이션 파일 작성 * rename: PushNotificationEventHandler -> PushMessageEventHandler * feat: deviceIdentifier 추가 * refactor: DTO 및 디바이스 엔티티 추가에 따른 수정 * feat: Device 엔티티 추가 및 Member 엔티티 수정 * refactor: 함수 분리 및 로직 변경에 따른 수정 * test: DeviceSubscription 관련 테스트 작성 * remove: 불필요한 코드 삭제 * refactor: MissionRetryPushMessageService 분리 * feat: 멀티 디바이스 및 다중 로그인 지원을 위한 redis 저장소 수정 * refactor: deprecatedDeviceToken -> deviceIdentifier * feat: 디바이스 일급 컬렉션 작성 * chore: 디바이스 관련 에러 코드 추가 * move: firebase -> device * refactor: MissionMemberService 로직의 구독 부분 DeviceSubscriptionService로 분리 * refactor: 구독 의존성 개선 * refactor: data 메시지 -> notification+data 혼합 메시지 * test: 메시지 타입 변경에 따른 테스트 수정 * refactor: deviceIdentifier & osType 추가 * refactor: member, device 도메인 분리 * chore: 유효성 검사 추가 * fix: 유효성 검사 수정 * test: MissionRetryMessageRepository 테스트 * refactor: 이전 버전 지원, 함수 중복 제거
- Loading branch information
Showing
71 changed files
with
1,653 additions
and
606 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 2 additions & 1 deletion
3
src/main/java/com/nexters/goalpanzi/application/auth/dto/request/AppleLoginCommand.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package com.nexters.goalpanzi.application.auth.dto.request; | ||
|
||
public record AppleLoginCommand( | ||
String identityToken | ||
String identityToken, | ||
String deviceIdentifier | ||
) { | ||
} |
3 changes: 2 additions & 1 deletion
3
src/main/java/com/nexters/goalpanzi/application/auth/dto/request/GoogleLoginCommand.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package com.nexters.goalpanzi.application.auth.dto.request; | ||
|
||
public record GoogleLoginCommand( | ||
String email | ||
String email, | ||
String deviceIdentifier | ||
) { | ||
} |
7 changes: 7 additions & 0 deletions
7
src/main/java/com/nexters/goalpanzi/application/auth/event/LoginEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.nexters.goalpanzi.application.auth.event; | ||
|
||
public record LoginEvent( | ||
Long memberId, | ||
String deviceIdentifier | ||
) { | ||
} |
68 changes: 68 additions & 0 deletions
68
src/main/java/com/nexters/goalpanzi/application/device/DeviceService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package com.nexters.goalpanzi.application.device; | ||
|
||
import com.nexters.goalpanzi.application.device.dto.request.UpdateDeviceTokenCommand; | ||
import com.nexters.goalpanzi.application.device.dto.request.UpdatePushActivationStatusCommand; | ||
import com.nexters.goalpanzi.application.device.event.UpdateDeviceTokenEvent; | ||
import com.nexters.goalpanzi.application.device.event.UpdatePushActivationStatusEvent; | ||
import com.nexters.goalpanzi.domain.device.Device; | ||
import com.nexters.goalpanzi.domain.device.OsType; | ||
import com.nexters.goalpanzi.domain.device.repository.DeviceRepository; | ||
import com.nexters.goalpanzi.domain.member.Member; | ||
import com.nexters.goalpanzi.domain.member.repository.MemberRepository; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.ApplicationEventPublisher; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Transactional(readOnly = true) | ||
@RequiredArgsConstructor | ||
@Service | ||
public class DeviceService { | ||
|
||
private final MemberRepository memberRepository; | ||
private final DeviceRepository deviceRepository; | ||
|
||
private final ApplicationEventPublisher eventPublisher; | ||
|
||
@Transactional | ||
public void updateDeviceToken(final UpdateDeviceTokenCommand command) { | ||
if (command.deviceIdentifier().isBlank() // TODO: 추후 앞의 조건 삭제 | ||
|| !deviceRepository.existsByDeviceIdentifier(command.deviceIdentifier())) { | ||
createDevice(command.memberId(), command.deviceIdentifier(), command.deviceToken(), command.osType()); | ||
} else { | ||
updateDevice(command.memberId(), command.deviceIdentifier(), command.deviceToken()); | ||
} | ||
} | ||
|
||
private void createDevice(final Long memberId, final String deviceIdentifier, final String deviceToken, final OsType osType) { | ||
Member member = memberRepository.getMember(memberId); | ||
Device device = deviceRepository.save(new Device(member, deviceIdentifier, deviceToken, osType)); | ||
|
||
eventPublisher.publishEvent( | ||
new UpdateDeviceTokenEvent(memberId, device.getId(), null) | ||
); | ||
} | ||
|
||
private void updateDevice(final Long memberId, final String deviceIdentifier, final String deviceToken) { | ||
Device device = deviceRepository.getDevice(memberId, deviceIdentifier); | ||
|
||
String deprecatedToken = device.getDeviceToken(); | ||
device.updateDeviceToken(deviceToken); | ||
device.updatePushActivationStatus(true); | ||
|
||
eventPublisher.publishEvent( | ||
new UpdateDeviceTokenEvent(memberId, device.getId(), deprecatedToken) | ||
); | ||
} | ||
|
||
@Transactional | ||
public void updatePushActivationStatus(final UpdatePushActivationStatusCommand command) { | ||
Device device = deviceRepository.getDevice(command.memberId(), command.deviceIdentifier()); | ||
|
||
device.updatePushActivationStatus(command.pushActivationStatus()); | ||
|
||
eventPublisher.publishEvent( | ||
new UpdatePushActivationStatusEvent(command.memberId(), device.getId(), command.pushActivationStatus(), device.getDeviceToken()) | ||
); | ||
} | ||
} |
238 changes: 238 additions & 0 deletions
238
src/main/java/com/nexters/goalpanzi/application/device/DeviceSubscriptionService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
package com.nexters.goalpanzi.application.device; | ||
|
||
import com.nexters.goalpanzi.application.firebase.TopicGenerator; | ||
import com.nexters.goalpanzi.application.mission.event.CancelMissionRetryPushMessageEvent; | ||
import com.nexters.goalpanzi.application.mission.event.ReserveMissionRetryPushMessageEvent; | ||
import com.nexters.goalpanzi.application.mission.event.UnsubscribeFromMissionEvent; | ||
import com.nexters.goalpanzi.application.mission.event.UpdateMissionRetryPushMessageEvent; | ||
import com.nexters.goalpanzi.domain.device.Device; | ||
import com.nexters.goalpanzi.domain.device.DeviceSubscription; | ||
import com.nexters.goalpanzi.domain.device.Devices; | ||
import com.nexters.goalpanzi.domain.device.repository.DeviceRepository; | ||
import com.nexters.goalpanzi.domain.device.repository.DeviceSubscriptionRepository; | ||
import com.nexters.goalpanzi.domain.member.Member; | ||
import com.nexters.goalpanzi.domain.mission.Mission; | ||
import com.nexters.goalpanzi.domain.mission.MissionMember; | ||
import com.nexters.goalpanzi.domain.mission.MissionStatus; | ||
import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository; | ||
import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; | ||
import com.nexters.goalpanzi.infrastructure.firebase.TopicSubscriber; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.ApplicationEventPublisher; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
import static com.nexters.goalpanzi.domain.mission.MissionStatus.*; | ||
|
||
@Transactional(readOnly = true) | ||
@RequiredArgsConstructor | ||
@Service | ||
public class DeviceSubscriptionService { | ||
|
||
private final DeviceRepository deviceRepository; | ||
private final DeviceSubscriptionRepository deviceSubscriptionRepository; | ||
private final MissionRepository missionRepository; | ||
private final MissionMemberRepository missionMemberRepository; | ||
|
||
private final ApplicationEventPublisher eventPublisher; | ||
private final TopicSubscriber topicSubscriber; | ||
|
||
private static final List<MissionStatus> SUBSCRIBABLE_MISSION_STATUS | ||
= List.of(CREATED, IN_PROGRESS, PENDING_COMPLETION); | ||
|
||
/** | ||
* <b>새로운 미션 참여 시 미션 구독 시작</b><br> | ||
* 멤버의 디바이스 중 푸시가 활성화된 디바이스를 대상으로 미션 구독 | ||
* | ||
* @param memberId 멤버 아이디 | ||
* @param mission 새롭게 참여한 미션 | ||
*/ | ||
@Transactional | ||
public void subscribeToMission(final Long memberId, final Mission mission) { | ||
Devices devices = new Devices( | ||
deviceRepository.findAllByMemberId(memberId) | ||
); | ||
|
||
devices.getActivatedDevices() | ||
.forEach(device -> | ||
deviceSubscriptionRepository.save(new DeviceSubscription(device, mission)) | ||
); | ||
topicSubscriber.subscribeToTopic( | ||
devices.getActivatedDeviceTokens(), TopicGenerator.getTopic(mission.getId()) | ||
); | ||
} | ||
|
||
/** | ||
* <b>미션 취소/종료 시 미션 구독 취소</b><br> | ||
* 해당 미션을 구독한 디바이스를 대상으로 구독 취소 | ||
* | ||
* @param missionId 취소/종료된 미션 | ||
*/ | ||
@Transactional | ||
public void unsubscribeFromMission(final Long missionId) { | ||
List<String> deviceTokens = findTopicSubscribers(missionId); | ||
|
||
deviceSubscriptionRepository.deleteAllByMissionId(missionId); | ||
topicSubscriber.unsubscribeFromTopic(deviceTokens, TopicGenerator.getTopic(missionId)); | ||
} | ||
|
||
private List<String> findTopicSubscribers(final Long missionId) { | ||
List<DeviceSubscription> subscriptions = deviceSubscriptionRepository.findAllWithDeviceAndMissionByMissionId(missionId); | ||
|
||
return subscriptions.stream() | ||
.map(it -> it.getDevice().getDeviceToken()) | ||
.toList(); | ||
} | ||
|
||
/** | ||
* <b>디바이스 토큰을 갱신하거나 푸시 알림 활성화 시 새로운 디바이스 토큰으로 내 미션 구독 시작</b><br> | ||
* + UpdateMissionRetryPushMessageEvent를 통해 예약된 메시지의 디바이스 토큰 갱신 | ||
* | ||
* @param memberId 멤버 아이디 | ||
* @param deviceId 디바이스 아이디 | ||
*/ | ||
@Transactional | ||
public void subscribeToMyMissions(final Long memberId, final Long deviceId) { | ||
Device device = deviceRepository.getDevice(deviceId); | ||
List<String> topics = findMySubscribedTopics(deviceId); | ||
List<Mission> missions = missionRepository.findAllById( | ||
findMySubscribableMission(memberId, topics) | ||
); | ||
|
||
topics.forEach(topic -> | ||
topicSubscriber.subscribeToTopic(List.of(device.getDeviceToken()), topic) | ||
); | ||
missions.forEach(mission -> { | ||
deviceSubscriptionRepository.save(new DeviceSubscription(device, mission)); | ||
|
||
String topic = TopicGenerator.getTopic(mission.getId()); | ||
topicSubscriber.subscribeToTopic(List.of(device.getDeviceToken()), topic); | ||
}); | ||
eventPublisher.publishEvent( | ||
new UpdateMissionRetryPushMessageEvent(memberId, device.getDeviceToken()) | ||
); | ||
} | ||
|
||
/** | ||
* <b>로그인 시 기존 디바이스가 구독한 미션 구독 취소</b> | ||
* + CancelMissionRetryPushMessageEvent를 통해 예약된 메시지 취소 | ||
* | ||
* @param memberId 멤버 아이디 | ||
* @param deviceIdentifier 디바이스 식별자 | ||
*/ | ||
@Transactional | ||
public void unsubscribeFromMyMissions(final Long memberId, final String deviceIdentifier) { | ||
Devices devices = new Devices( | ||
deviceRepository.findAllByDeviceIdentifier(deviceIdentifier) | ||
); | ||
List<String> topics = devices.getActivatedDevices().stream() | ||
.flatMap(device -> findMySubscribedTopics(device.getId()).stream()) | ||
.toList(); | ||
|
||
topics.forEach(topic -> | ||
topicSubscriber.unsubscribeFromTopic(devices.getActivatedDeviceTokens(), topic) | ||
); | ||
eventPublisher.publishEvent( | ||
new CancelMissionRetryPushMessageEvent(memberId) | ||
); | ||
} | ||
|
||
/** | ||
* <b>디바이스 토큰을 갱신하거나 푸시 비활성화 시 기존 디바이스 토큰으로 구독한 미션 구독 취소</b><br> | ||
* + CancelMissionRetryPushMessageEvent를 통해 예약된 메시지 취소 | ||
* | ||
* @param memberId 멤버 아이디 | ||
* @param deviceId 디바이스 아이디 | ||
* @param deviceToken 기존 디바이스 토큰 | ||
*/ | ||
@Transactional | ||
public void unsubscribeFromMyMissions(final Long memberId, final Long deviceId, final String deviceToken) { | ||
List<String> topics = findMySubscribedTopics(deviceId); | ||
|
||
topics.forEach(topic -> | ||
topicSubscriber.unsubscribeFromTopic(List.of(deviceToken), topic) | ||
); | ||
eventPublisher.publishEvent( | ||
new CancelMissionRetryPushMessageEvent(memberId) | ||
); | ||
} | ||
|
||
private List<String> findMySubscribedTopics(final Long deviceId) { | ||
List<DeviceSubscription> subscriptions = deviceSubscriptionRepository.findAllWithMissionAndDeviceByDeviceId(deviceId); | ||
|
||
return subscriptions.stream() | ||
.map(it -> TopicGenerator.getTopic(it.getMission().getId())) | ||
.toList(); | ||
} | ||
|
||
private List<Long> findMySubscribableMission(final Long memberId, List<String> topicFilter) { | ||
List<MissionMember> missionMembers = missionMemberRepository.findAllWithMissionByMemberId(memberId); | ||
List<MissionMember> filteredMissionMembers = missionMembers.stream() | ||
.filter(this::isSubscribableMission) | ||
.filter(it -> isAlreadySubscribedMission(topicFilter, it.getMission().getId())) | ||
.toList(); | ||
|
||
return filteredMissionMembers.stream() | ||
.map(it -> it.getMission().getId()) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private boolean isSubscribableMission(final MissionMember missionMember) { | ||
return SUBSCRIBABLE_MISSION_STATUS.contains(missionMember.getMissionStatus()); | ||
} | ||
|
||
private boolean isAlreadySubscribedMission(List<String> filter, final Long missionId) { | ||
return filter.contains(TopicGenerator.getTopic(missionId)); | ||
} | ||
|
||
/** | ||
* <b>취소/완료 상태의 미션을 찾아 이벤트 게시</b><br> | ||
* - 취소/완료 상태 : UnsubscribeFromMissionEvent를 게시하여 미션 구독 취소<br> | ||
* - 완료 상태 : ReserveMissionRetryPushMessageEvent를 게시하여 메시지 예약 | ||
*/ | ||
@Transactional | ||
public void unsubscribeFromUselessMissions() { | ||
List<Mission> missions = missionRepository.getInProgressMissions(); | ||
|
||
missions.forEach(mission -> { | ||
List<MissionMember> missionMembers = missionMemberRepository.findAllWithMemberByMissionId(mission.getId()); | ||
|
||
if (isCancelledMission(missionMembers) || isCompletedMission(missionMembers)) { | ||
eventPublisher.publishEvent( | ||
new UnsubscribeFromMissionEvent(mission.getId()) | ||
); | ||
if (isCompletedMission(missionMembers)) { | ||
missionMembers.forEach(missionMember -> | ||
reserveRetryPushMessageForMember(missionMember.getMember()) | ||
); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
private boolean isCancelledMission(final List<MissionMember> missionMembers) { | ||
return missionMembers.stream() | ||
.anyMatch(it -> it.getMissionStatus().equals(CANCELED)); | ||
} | ||
|
||
private boolean isCompletedMission(final List<MissionMember> missionMembers) { | ||
return missionMembers.stream() | ||
.anyMatch(it -> it.getMissionStatus().equals(COMPLETED)); | ||
} | ||
|
||
private void reserveRetryPushMessageForMember(final Member member) { | ||
Devices devices = new Devices( | ||
deviceRepository.findAllByMemberId(member.getId()) | ||
); | ||
|
||
devices.getActivatedDeviceTokens() | ||
.forEach(deviceToken -> | ||
eventPublisher.publishEvent( | ||
new ReserveMissionRetryPushMessageEvent(member.getId(), deviceToken) | ||
) | ||
); | ||
} | ||
} |
Oops, something went wrong.