Skip to content

Commit

Permalink
Merge pull request #77 from EFUB4-Jukebox/develop
Browse files Browse the repository at this point in the history
[Deploy] 배포 v0.3.0
  • Loading branch information
seohyun-lee authored Jul 23, 2024
2 parents 25ad2c3 + eaeedf1 commit aa4edd3
Show file tree
Hide file tree
Showing 62 changed files with 1,223 additions and 261 deletions.
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());
}
}
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);
}
}
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()
);
}
}
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()
);
}
}
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()
);
}
}
52 changes: 33 additions & 19 deletions src/main/java/sws/songpin/domain/alarm/entity/Alarm.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,55 @@
import jakarta.validation.constraints.NotNull;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import sws.songpin.domain.member.entity.Member;
import sws.songpin.global.BaseTimeEntity;

import java.time.LocalDateTime;

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
@Entity
public class Alarm extends BaseTimeEntity {
public class Alarm {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "alarm_id", updatable = false)
private Long alarmId;

@Column(name = "message", length = 100)
@Enumerated(EnumType.STRING)
@Column(name = "alarm_type")
@NotNull
private String message;
private AlarmType alarmType;

@Column(name = "target_mem_id")
private Long targetMemId;
@Column(name = "message", length = 100)
private String message;

@Column(name = "is_checked")
@NotNull
@Column(name = "is_read")
@ColumnDefault("false")
private Boolean isChecked;
@Builder.Default
private Boolean isRead = false;

@CreatedDate
@Column(updatable = false)
private LocalDateTime createdTime;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", updatable = false)
@JoinColumn(name = "sender_id", updatable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Member sender;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "receiver_id", updatable = false)
@NotNull
private Member member;

@Builder
public Alarm(Long alarmId, String message, Long targetMemId, Boolean isChecked, Member member) {
this.alarmId = alarmId;
this.message = message;
this.targetMemId = targetMemId;
this.isChecked = isChecked;
this.member = member;
@OnDelete(action = OnDeleteAction.CASCADE)
private Member receiver;

public void readAlarm() {
this.isRead = true;
}
}
14 changes: 14 additions & 0 deletions src/main/java/sws/songpin/domain/alarm/entity/AlarmType.java
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;
}
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);
}
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 src/main/java/sws/songpin/domain/alarm/service/AlarmService.java
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 src/main/java/sws/songpin/domain/alarm/service/EmitterService.java
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);
}
}
}
}
Loading

0 comments on commit aa4edd3

Please sign in to comment.