-
Notifications
You must be signed in to change notification settings - Fork 0
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
[#153] 실시간 알람 연결, (크루 알람,게임 알람) 생성, 수정, 삭제, 미확인 알람 확인 구현 #190
Changes from 77 commits
6e386db
473eb81
a1660fa
6ae6963
bc5c2e6
7de8236
226b231
dbbdb23
95cca67
706653a
408db12
9fa0f5f
ef138c8
34de4d1
04af8bd
91c3663
77e9a07
ad72030
b431b60
0c7e29f
5216572
f380b0f
67e96b9
88a8ff8
f264593
0bb2074
8c6c3a9
11fadde
165921c
ba28f92
2a542ee
fbc0948
cdbc934
c9238f5
56a2adf
a518f80
b994014
16efc0b
c085b41
3f4ce55
fa9d715
79473ac
6dabcc0
e95316c
576a5fe
543ff01
f5b2f45
c5609bd
1473d74
2c39923
704f6d6
f0e490a
1d26c29
b97e336
146eb30
ddf1089
a5ffa15
f75859a
2edb980
ef5fb41
9aa923a
d6ad7d5
a76d0bd
5a25655
0f301a3
167d198
22160dd
6970b6d
2e48511
cd53999
9fb98ab
ff6d07e
e9b7a2b
bf364ef
99c3c19
7b60bf2
877c80d
b0bdc91
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 |
---|---|---|
@@ -0,0 +1,14 @@ | ||
### 해당 사용자 알람 sse 연결 | ||
GET http://localhost:8080/alarms/subscribe | ||
Content-Type: text/event-stream | ||
Authorization: | ||
|
||
### 해당 사용자 읽지 않은 알림 확인 | ||
GET http://localhost:8080/alarms/unread | ||
Authorization: | ||
Content-Type: application/json | ||
|
||
### 해당 사용자의 모든 알람 삭제 | ||
DELETE http://localhost:8080/alarms | ||
Authorization: | ||
Content-Type: application/json |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
|
||
### 해당 사용자 크루 관련 알람 상태 변경 | ||
POST http://localhost:8080/crew-alarm/{crewAlarmId} | ||
Authorization: | ||
Content-Type: application/json | ||
|
||
{ | ||
"isRead": true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
|
||
### 게임 관련 알람 상태 수정 | ||
POST http://localhost:8080/game-alarm/{gameAlarmId} | ||
Authorization: | ||
Content-Type: application/json | ||
|
||
{ | ||
"isRead": true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package kr.pickple.back.alarm.config; | ||
|
||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.core.task.TaskExecutor; | ||
import org.springframework.scheduling.annotation.EnableAsync; | ||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | ||
|
||
@Configuration | ||
@EnableAsync | ||
public class AsynConfig { | ||
|
||
@Bean | ||
public TaskExecutor taskExecutor() { | ||
return CustomThreadPoolTaskExecutor.builder() | ||
.corePoolSize(30) | ||
.maxPoolSize(50) | ||
.queueCapacity(70) | ||
.build(); | ||
} | ||
|
||
static class CustomThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { | ||
private CustomThreadPoolTaskExecutor(final int corePoolSize, final int maxPoolSize, final int queueCapacity) { | ||
super(); | ||
this.setCorePoolSize(corePoolSize); | ||
this.setMaxPoolSize(maxPoolSize); | ||
this.setQueueCapacity(queueCapacity); | ||
this.initialize(); | ||
} | ||
|
||
public static Builder builder() { | ||
return new Builder(); | ||
} | ||
|
||
public static class Builder { | ||
private int corePoolSize; | ||
private int maxPoolSize; | ||
private int queueCapacity; | ||
|
||
public Builder corePoolSize(final int corePoolSize) { | ||
this.corePoolSize = corePoolSize; | ||
return this; | ||
} | ||
|
||
public Builder maxPoolSize(final int maxPoolSize) { | ||
this.maxPoolSize = maxPoolSize; | ||
return this; | ||
} | ||
|
||
public Builder queueCapacity(final int queueCapacity) { | ||
this.queueCapacity = queueCapacity; | ||
return this; | ||
} | ||
|
||
public CustomThreadPoolTaskExecutor build() { | ||
return new CustomThreadPoolTaskExecutor(corePoolSize, maxPoolSize, queueCapacity); | ||
} | ||
} | ||
} | ||
hanjo8813 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package kr.pickple.back.alarm.controller; | ||
|
||
import kr.pickple.back.alarm.dto.response.AlarmExistStatusResponse; | ||
import kr.pickple.back.alarm.service.AlarmService; | ||
import kr.pickple.back.alarm.service.SseEmitterService; | ||
import kr.pickple.back.auth.config.resolver.Login; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.DeleteMapping; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
|
||
import static org.springframework.http.HttpStatus.NO_CONTENT; | ||
import static org.springframework.http.HttpStatus.OK; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/alarms") | ||
public class AlarmController { | ||
|
||
private final AlarmService alarmService; | ||
private final SseEmitterService sseEmitterService; | ||
|
||
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) | ||
public ResponseEntity<SseEmitter> subscribeToSse( | ||
@Login final Long loggedInMemberId | ||
) { | ||
final SseEmitter emitter = alarmService.subscribeToSse(loggedInMemberId); | ||
sseEmitterService.sendCachedEventToUser(loggedInMemberId, emitter); | ||
|
||
return ResponseEntity.status(OK) | ||
.header("X-Accel-Buffering", "no") | ||
.body(emitter); | ||
Comment on lines
+34
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nginx 설정 중 프록시 버퍼링 옵션을 off로 바꾸면되지 않나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 부분을 정말 정말 많이 고민했었습니다.
해당 방식으로 인한 발생하는 또다른 문제: Nginx의 설정 파일에서 버퍼링을 비활성화하면 다른 모든 API 응답에 대해서도 버퍼링을 하지 않기 때문에 비효율적이기 때문이라는 문제점이 발생됩니다.
참고자료: 우테코 SSE 연결 (https://tecoble.techcourse.co.kr/post/2022-10-11-server-sent-events/) |
||
} | ||
|
||
@GetMapping("/unread") | ||
public ResponseEntity<AlarmExistStatusResponse> findUnreadAlarm( | ||
@Login final Long loggedInMemberId | ||
) { | ||
AlarmExistStatusResponse response = alarmService.checkUnReadAlarms(loggedInMemberId); | ||
|
||
return ResponseEntity | ||
.status(OK) | ||
.body(response); | ||
} | ||
|
||
@DeleteMapping | ||
public ResponseEntity<Void> deleteAllAlarms( | ||
@Login final Long loggedInMemberId | ||
) { | ||
alarmService.deleteAllAlarms(loggedInMemberId); | ||
|
||
return ResponseEntity.status(NO_CONTENT) | ||
.build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package kr.pickple.back.alarm.controller; | ||
|
||
import jakarta.validation.Valid; | ||
import kr.pickple.back.alarm.dto.request.CrewAlarmUpdateStatusRequest; | ||
import kr.pickple.back.alarm.service.CrewAlarmService; | ||
import kr.pickple.back.auth.config.resolver.Login; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.*; | ||
|
||
import static org.springframework.http.HttpStatus.NO_CONTENT; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/crew-alarms") | ||
public class CrewAlarmController { | ||
|
||
private final CrewAlarmService crewAlarmService; | ||
|
||
@PatchMapping("/{crewAlarmId}") | ||
public ResponseEntity<Void> updateCrewAlarmStatus( | ||
@Login final Long loggedInMemberId, | ||
@PathVariable final Long crewAlarmId, | ||
@Valid @RequestBody final CrewAlarmUpdateStatusRequest crewAlarmUpdateStatusRequest | ||
hanjo8813 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) { | ||
crewAlarmService.updateCrewAlarmById(loggedInMemberId, crewAlarmId, crewAlarmUpdateStatusRequest); | ||
|
||
return ResponseEntity.status(NO_CONTENT) | ||
.build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package kr.pickple.back.alarm.controller; | ||
|
||
import jakarta.validation.Valid; | ||
import kr.pickple.back.alarm.dto.request.GameAlarmUpdateStatusRequest; | ||
import kr.pickple.back.alarm.service.GameAlarmService; | ||
import kr.pickple.back.auth.config.resolver.Login; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.*; | ||
|
||
import static org.springframework.http.HttpStatus.NO_CONTENT; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/game-alarms") | ||
public class GameAlarmController { | ||
|
||
private final GameAlarmService gameAlarmService; | ||
|
||
@PatchMapping("/{gameAlarmId}") | ||
public ResponseEntity<Void> updateGameAlarmStatus( | ||
@Login final Long loggedInMemberId, | ||
@PathVariable final Long gameAlarmId, | ||
@Valid @RequestBody final GameAlarmUpdateStatusRequest gameAlarmUpdateStatusRequest | ||
) { | ||
gameAlarmService.updateGameAlarmById(loggedInMemberId, gameAlarmId, gameAlarmUpdateStatusRequest); | ||
|
||
return ResponseEntity.status(NO_CONTENT) | ||
.build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package kr.pickple.back.alarm.domain; | ||
|
||
import com.fasterxml.jackson.annotation.JsonCreator; | ||
import com.fasterxml.jackson.annotation.JsonValue; | ||
import kr.pickple.back.crew.exception.CrewException; | ||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
import java.util.Collections; | ||
import java.util.Map; | ||
import java.util.function.Function; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import static kr.pickple.back.alarm.exception.AlarmExceptionCode.ALARM_EXISTS_STATUS_NOT_FOUND; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public enum AlarmExistsStatus { | ||
|
||
EXISTS("읽지 않은 알람이 있음", true), | ||
NOT_EXISTS("읽지 않은 알람이 없음", false); | ||
|
||
private static final Map<String, AlarmExistsStatus> alarmExistsStatusMap = Collections.unmodifiableMap(Stream.of(values()) | ||
.collect(Collectors.toMap(AlarmExistsStatus::getDescription, Function.identity()))); | ||
|
||
private final String description; | ||
private final Boolean booleanValue; | ||
|
||
@JsonCreator | ||
public static AlarmExistsStatus from(final String description) { | ||
if (alarmExistsStatusMap.containsKey(description)) { | ||
return alarmExistsStatusMap.get(description); | ||
} | ||
throw new CrewException(ALARM_EXISTS_STATUS_NOT_FOUND, description); | ||
} | ||
|
||
@JsonValue | ||
public Boolean getBooleanValue() { | ||
return booleanValue; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package kr.pickple.back.alarm.domain; | ||
|
||
import com.fasterxml.jackson.annotation.JsonCreator; | ||
import com.fasterxml.jackson.annotation.JsonValue; | ||
import kr.pickple.back.crew.exception.CrewException; | ||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
import java.util.Collections; | ||
import java.util.Map; | ||
import java.util.function.Function; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import static kr.pickple.back.alarm.exception.AlarmExceptionCode.ALARM_STATUS_NOT_FOUND; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public enum AlarmStatus { | ||
|
||
TRUE("읽음", true), | ||
FALSE("읽지 않음", false); | ||
|
||
private static final Map<Boolean, AlarmStatus> alarmStatusMap = Collections.unmodifiableMap(Stream.of(values()) | ||
.collect(Collectors.toMap(AlarmStatus::getBooleanValue, Function.identity()))); | ||
|
||
private final String description; | ||
private final Boolean booleanValue; | ||
|
||
@JsonCreator | ||
public static AlarmStatus from(final Boolean booleanValue) { | ||
if (alarmStatusMap.containsKey(booleanValue)) { | ||
return alarmStatusMap.get(booleanValue); | ||
} | ||
throw new CrewException(ALARM_STATUS_NOT_FOUND, booleanValue); | ||
} | ||
|
||
@JsonValue | ||
public Boolean getBooleanValue() { | ||
return booleanValue; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package kr.pickple.back.alarm.domain; | ||
|
||
import jakarta.persistence.*; | ||
import jakarta.validation.constraints.NotNull; | ||
import kr.pickple.back.alarm.util.AlarmStatusConverter; | ||
import kr.pickple.back.alarm.util.CrewAlarmTypeConverter; | ||
import kr.pickple.back.common.domain.BaseEntity; | ||
import kr.pickple.back.crew.domain.Crew; | ||
import kr.pickple.back.member.domain.Member; | ||
import lombok.AccessLevel; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
import static kr.pickple.back.alarm.domain.AlarmStatus.FALSE; | ||
|
||
@Getter | ||
@Entity | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
public class CrewAlarm extends BaseEntity { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@NotNull | ||
@Column(length = 10) | ||
@Convert(converter = AlarmStatusConverter.class) | ||
private AlarmStatus isRead = FALSE; | ||
|
||
@NotNull | ||
@Column(length = 30) | ||
@Convert(converter = CrewAlarmTypeConverter.class) | ||
private CrewAlarmType crewAlarmType; | ||
|
||
@ManyToOne(fetch = FetchType.LAZY) | ||
@JoinColumn(name = "crew_id") | ||
private Crew crew; | ||
|
||
@ManyToOne(fetch = FetchType.LAZY) | ||
@JoinColumn(name = "member_id") | ||
private Member member; | ||
|
||
@Builder | ||
private CrewAlarm( | ||
final CrewAlarmType crewAlarmType, | ||
final Crew crew, | ||
final Member member | ||
) { | ||
this.crewAlarmType = crewAlarmType; | ||
this.crew = crew; | ||
this.member = member; | ||
} | ||
|
||
public void updateStatus(final AlarmStatus status) { | ||
this.isRead = status; | ||
} | ||
} |
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.
현재 매직넘버 사용 지양 원칙을 위반합니다.
이런 설정값은 보통 application.yml에 작성하고, property 객체로 받아오는게 일반적입니다.
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.
해당 문제에 착각을 했었습니다! 매직넘버가 있어, 해당 부분을 설정 파일로 빼었을때, 기존 크루 거절, 게임 거절의 경우 해당 알람이 발생되지 않았었기에 해당 PR에서는 수정하지 못했었습니다.
살펴보니 제 기존 로직에 대해서 다음과 같은 문제가 있었습니다.
현재 PR의 크루 거절, 게임 멤버 거절 알람
먼저 크루원 테이블, 게임 멤버 테이블에서 거절 시 하드 delete가 되어 알람 대상자가 미리 삭제되어 알람 발송이 불가하였습니다. 따라서 먼저 거절 시 알람을 보내고, 크루원 및 게임 멤버 테이블에 삭제되어야 했었네요.
이후의 PR에서 리팩토링한 부분
해당 부분을 놓치고 있었네요! 다음 PR에서 바로 수정을 했습니다!