-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #77 from EFUB4-Jukebox/develop
[Deploy] 배포 v0.3.0
- Loading branch information
Showing
62 changed files
with
1,223 additions
and
261 deletions.
There are no files selected for viewing
33 changes: 33 additions & 0 deletions
33
src/main/java/sws/songpin/domain/alarm/controller/AlarmController.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,33 @@ | ||
package sws.songpin.domain.alarm.controller; | ||
|
||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.*; | ||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
import sws.songpin.domain.alarm.service.AlarmService; | ||
import sws.songpin.domain.alarm.service.EmitterService; | ||
|
||
@Tag(name = "Alarm", description = "Alarm 관련 API입니다.") | ||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/alarms") | ||
public class AlarmController { | ||
private final EmitterService emitterService; | ||
private final AlarmService alarmService; | ||
|
||
@Operation(summary = "알림 구독", description = "알림을 구독합니다.") | ||
@PostMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) | ||
public ResponseEntity<SseEmitter> subscribe() { | ||
return ResponseEntity.ok(emitterService.subscribe()); | ||
} | ||
|
||
// 신규 알림 목록 읽어오기 API | ||
@Operation(summary = "알림 목록 조회 및 읽음 처리", description = "알림 목록을 조회하고 읽음 처리합니다.") | ||
@PatchMapping("/list") | ||
public ResponseEntity<?> getUnreadAlarms() { | ||
return ResponseEntity.ok(alarmService.getAlarmList()); | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
src/main/java/sws/songpin/domain/alarm/dto/response/AlarmListResponseDto.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,11 @@ | ||
package sws.songpin.domain.alarm.dto.response; | ||
|
||
import java.util.List; | ||
|
||
public record AlarmListResponseDto( | ||
List<AlarmUnitDto> alarmList | ||
) { | ||
public static AlarmListResponseDto fromAlarmUnitDto(List<AlarmUnitDto> alarmUnitDtos) { | ||
return new AlarmListResponseDto(alarmUnitDtos); | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
src/main/java/sws/songpin/domain/alarm/dto/response/AlarmUnitDto.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,21 @@ | ||
package sws.songpin.domain.alarm.dto.response; | ||
|
||
import sws.songpin.domain.alarm.entity.Alarm; | ||
|
||
import java.time.LocalDateTime; | ||
|
||
public record AlarmUnitDto( | ||
Boolean isRead, | ||
String message, | ||
LocalDateTime createdTime, | ||
Long senderId | ||
) { | ||
public static AlarmUnitDto from(Alarm alarm) { | ||
return new AlarmUnitDto( | ||
alarm.getIsRead(), | ||
alarm.getMessage(), | ||
alarm.getCreatedTime(), | ||
alarm.getSender().getMemberId() | ||
); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/sws/songpin/domain/alarm/dto/ssedata/AlarmDefaultDataDto.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,13 @@ | ||
package sws.songpin.domain.alarm.dto.ssedata; | ||
|
||
import sws.songpin.domain.member.entity.Member; | ||
|
||
public record AlarmDefaultDataDto( | ||
Long memberId | ||
) { | ||
public static AlarmDefaultDataDto from (Member member) { | ||
return new AlarmDefaultDataDto( | ||
member.getMemberId() | ||
); | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
src/main/java/sws/songpin/domain/alarm/dto/ssedata/AlarmFollowDataDto.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,20 @@ | ||
package sws.songpin.domain.alarm.dto.ssedata; | ||
|
||
import sws.songpin.domain.alarm.entity.AlarmType; | ||
import sws.songpin.domain.member.entity.Member; | ||
|
||
public record AlarmFollowDataDto( | ||
AlarmType alarmType, | ||
Long senderId, | ||
String senderNickname, | ||
String senderHandle | ||
) { | ||
public static AlarmFollowDataDto from (Member member) { | ||
return new AlarmFollowDataDto( | ||
AlarmType.FOLLOW, | ||
member.getMemberId(), | ||
member.getNickname(), | ||
member.getHandle() | ||
); | ||
} | ||
} |
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
14 changes: 14 additions & 0 deletions
14
src/main/java/sws/songpin/domain/alarm/entity/AlarmType.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,14 @@ | ||
package sws.songpin.domain.alarm.entity; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
public enum AlarmType { | ||
FOLLOW("{0}(@{1})님이 팔로우했어요."), | ||
DEFAULT("{0}(@{1})님이 보낸 알림이에요.") | ||
; | ||
|
||
private final String messagePattern; | ||
} |
12 changes: 12 additions & 0 deletions
12
src/main/java/sws/songpin/domain/alarm/repository/AlarmRepository.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,12 @@ | ||
package sws.songpin.domain.alarm.repository; | ||
|
||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.domain.Slice; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import sws.songpin.domain.alarm.entity.Alarm; | ||
import sws.songpin.domain.member.entity.Member; | ||
|
||
public interface AlarmRepository extends JpaRepository<Alarm, Long> { | ||
Slice<Alarm> findByReceiverOrderByCreatedTimeDesc(Member receiver, Pageable pageable); | ||
Boolean existsByReceiverAndIsReadFalse(Member member); | ||
} |
26 changes: 26 additions & 0 deletions
26
src/main/java/sws/songpin/domain/alarm/repository/EmitterRepository.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,26 @@ | ||
package sws.songpin.domain.alarm.repository; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Repository; | ||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
|
||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
|
||
@Repository | ||
@RequiredArgsConstructor | ||
public class EmitterRepository { | ||
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>(); | ||
|
||
public void save(Long id, SseEmitter emitter) { | ||
emitters.put(id, emitter); | ||
} | ||
|
||
public void delete(Long memberId) { | ||
emitters.remove(memberId); | ||
} | ||
|
||
public SseEmitter get(Long memberId) { | ||
return emitters.get(memberId); | ||
} | ||
} |
70 changes: 70 additions & 0 deletions
70
src/main/java/sws/songpin/domain/alarm/service/AlarmService.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,70 @@ | ||
package sws.songpin.domain.alarm.service; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.data.domain.PageRequest; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.domain.Slice; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
import sws.songpin.domain.alarm.dto.response.AlarmUnitDto; | ||
import sws.songpin.domain.alarm.dto.ssedata.AlarmFollowDataDto; | ||
import sws.songpin.domain.alarm.dto.response.AlarmListResponseDto; | ||
import sws.songpin.domain.alarm.entity.Alarm; | ||
import sws.songpin.domain.alarm.entity.AlarmType; | ||
import sws.songpin.domain.alarm.repository.AlarmRepository; | ||
import sws.songpin.domain.member.entity.Member; | ||
import sws.songpin.domain.member.service.MemberService; | ||
|
||
import java.text.MessageFormat; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
|
||
@Service | ||
@Transactional | ||
@RequiredArgsConstructor | ||
public class AlarmService { | ||
private final AlarmRepository alarmRepository; | ||
private final EmitterService emitterService; | ||
private final MemberService memberService; | ||
|
||
// 일반 알림 생성 (message, sender는 필수 요소 x) | ||
public void createAlarm(AlarmType alarmType, String message, Member sender, Member receiver, Object data) { | ||
Alarm alarm = Alarm.builder() | ||
.alarmType(alarmType) | ||
.message(message) // nullable | ||
.sender(sender) // nullable | ||
.receiver(receiver) | ||
.isRead(false) | ||
.build(); | ||
alarmRepository.save(alarm); | ||
emitterService.notify(sender.getMemberId(), data, "new alarm"); | ||
} | ||
|
||
// 팔로우 알림 생성 | ||
public void createFollowAlarm(Member follower, Member following) { | ||
String alarmMessage = MessageFormat.format(AlarmType.FOLLOW.getMessagePattern(), follower.getNickname(), follower.getHandle()); | ||
createAlarm(AlarmType.FOLLOW, alarmMessage, follower, following, AlarmFollowDataDto.from(follower)); | ||
} | ||
|
||
// 최근 알림 목록 읽어오기 | ||
public AlarmListResponseDto getAlarmList(){ | ||
List<AlarmUnitDto> alarmUnitDtos = getAndReadAlarms(); | ||
return AlarmListResponseDto.fromAlarmUnitDto(alarmUnitDtos); | ||
} | ||
|
||
private List<AlarmUnitDto> getAndReadAlarms() { | ||
List<AlarmUnitDto> alarmList = new ArrayList<>(); | ||
Member member = memberService.getCurrentMember(); | ||
Pageable pageable = PageRequest.of(0, 30); | ||
Slice<Alarm> alarmSlice = alarmRepository.findByReceiverOrderByCreatedTimeDesc(member, pageable); | ||
if (alarmSlice != null && alarmSlice.hasContent()) { | ||
for (Alarm alarm : alarmSlice) { | ||
alarmList.add(AlarmUnitDto.from(alarm)); | ||
alarm.readAlarm(); | ||
} | ||
alarmRepository.saveAll(alarmSlice); | ||
} | ||
return alarmList; | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
src/main/java/sws/songpin/domain/alarm/service/EmitterService.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,73 @@ | ||
package sws.songpin.domain.alarm.service; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
import sws.songpin.domain.alarm.dto.ssedata.AlarmDefaultDataDto; | ||
import sws.songpin.domain.alarm.repository.AlarmRepository; | ||
import sws.songpin.domain.alarm.repository.EmitterRepository; | ||
import sws.songpin.domain.member.entity.Member; | ||
import sws.songpin.global.auth.CustomUserDetails; | ||
|
||
import java.io.IOException; | ||
|
||
@Slf4j | ||
@Service | ||
@Transactional | ||
@RequiredArgsConstructor | ||
public class EmitterService { | ||
private final EmitterRepository emitterRepository; | ||
private final AlarmRepository alarmRepository; | ||
|
||
private static final Long DEFAULT_TIMEOUT = 5L * 1000; // 5초 | ||
|
||
public SseEmitter subscribe() { | ||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); | ||
Member member = ((CustomUserDetails) authentication.getPrincipal()).getMember(); | ||
SseEmitter emitter = registerEmitter(member); | ||
sendToClientIfNewAlarmExists(member); | ||
return emitter; | ||
} | ||
|
||
public void notify(Long memberId, Object data, String comment) { | ||
sendToClient(memberId, data, comment); | ||
} | ||
|
||
private SseEmitter registerEmitter(Member member) { | ||
Long memberId = member.getMemberId(); | ||
SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); | ||
emitterRepository.save(memberId, emitter); | ||
|
||
emitter.onCompletion(() -> emitterRepository.delete(memberId)); | ||
emitter.onTimeout(() -> emitterRepository.delete(memberId)); | ||
|
||
return emitter; | ||
} | ||
|
||
private void sendToClientIfNewAlarmExists(Member member) { | ||
Boolean isMissedAlarms = alarmRepository.existsByReceiverAndIsReadFalse(member); | ||
if (isMissedAlarms.equals(false)) { | ||
sendToClient(member.getMemberId(), AlarmDefaultDataDto.from(member), "new sse alarm exists"); | ||
} | ||
} | ||
|
||
private <T> void sendToClient(Long memberId, Object data, String comment) { | ||
SseEmitter emitter = emitterRepository.get(memberId); | ||
if (emitter != null) { | ||
try { | ||
emitter.send(SseEmitter.event() | ||
.id(String.valueOf(memberId)) | ||
.name("sse-alarm") | ||
.data(data) | ||
.comment(comment)); | ||
} catch (IOException e) { | ||
emitterRepository.delete(memberId); | ||
emitter.completeWithError(e); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.