-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 알림 기능을 적용한다 #41
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
Changes from all commits
29740e9
128e3a6
734f7f9
40b31de
cec714f
47f863a
830072b
de9e5e5
d469b7b
4e64cc9
6b507f1
52911c4
ad4ac4c
7c37d58
c94d9b2
1aa3be9
b6a4bf2
0dce06c
d8c1329
53f104c
432ab79
5219631
2de8813
9a8cc59
dd82456
56b3ad5
3a21613
5fa61fd
02d6b83
7aa4f70
7afc271
88e5e52
550e70b
196049b
ac16c6d
5090251
796239e
0040234
f183748
f92ecbc
f103ac6
833df6b
3ed2562
5d1910f
fc577e8
4c8cca2
64040f3
22f27bf
976ea50
734c23c
4ea7046
24d5187
808f8c3
0f59e98
e045ed1
5dec65f
dab7f33
72806c7
6f1feb3
41335c0
69697dd
a6bcbcc
fd105a0
cdd5116
042b521
4f7a9ac
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,28 @@ | ||
:toc: left | ||
:source-highlighter: highlightjs | ||
:sectlinks: | ||
:toclevels: 2 | ||
:sectlinks: | ||
:sectnums: | ||
|
||
== Alert | ||
|
||
=== 알림 페이징 조회 (GET api/members/me/alerts) | ||
==== 요청 | ||
include::{snippets}/alert-controller-web-mvc-test/받은_알림_페이징조회/request-headers.adoc[] | ||
include::{snippets}/alert-controller-web-mvc-test/받은_알림_페이징조회/request-parts.adoc[] | ||
include::{snippets}/alert-controller-web-mvc-test/받은_알림_페이징조회/http-request.adoc[] | ||
==== 응답 | ||
include::{snippets}/alert-controller-web-mvc-test/받은_알림_페이징조회/response-fields.adoc[] | ||
include::{snippets}/alert-controller-web-mvc-test/받은_알림_페이징조회/http-response.adoc[] | ||
|
||
=== 알림 단일 조회 (GET api/members/me/alerts/{alertId}) | ||
읽지 않았던 알림일 경우 읽음 처리됨 | ||
|
||
==== 요청 | ||
include::{snippets}/alert-controller-web-mvc-test/단일_알림_조회/request-headers.adoc[] | ||
include::{snippets}/alert-controller-web-mvc-test/단일_알림_조회/path-parameters.adoc[] | ||
include::{snippets}/alert-controller-web-mvc-test/단일_알림_조회/http-request.adoc[] | ||
==== 응답 | ||
include::{snippets}/alert-controller-web-mvc-test/단일_알림_조회/response-fields.adoc[] | ||
include::{snippets}/alert-controller-web-mvc-test/단일_알림_조회/http-response.adoc[] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.atwoz.alert.application; | ||
|
||
import com.atwoz.alert.application.event.AlertCreatedEvent; | ||
import com.atwoz.alert.application.event.AlertTokenCreatedEvent; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.scheduling.annotation.Async; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.transaction.event.TransactionalEventListener; | ||
|
||
@RequiredArgsConstructor | ||
@Component | ||
public class AlertEventHandler { | ||
|
||
private static final String ASYNC_EXECUTOR = "asyncExecutor"; | ||
|
||
private final AlertService alertService; | ||
|
||
@Async(value = ASYNC_EXECUTOR) | ||
@TransactionalEventListener | ||
public void sendAlertCreatedEvent(final AlertCreatedEvent event) { | ||
alertService.sendAlert(event.group(), event.title(), event.body(), event.sender(), event.receiverId()); | ||
} | ||
|
||
@TransactionalEventListener | ||
public void sendAlertTokenCreatedEvent(final AlertTokenCreatedEvent event) { | ||
alertService.saveToken(event.id(), event.token()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.atwoz.alert.application; | ||
|
||
import com.atwoz.alert.domain.Alert; | ||
|
||
public interface AlertManager { | ||
|
||
void send(Alert alert, String sender, String token); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.atwoz.alert.application; | ||
|
||
import com.atwoz.alert.domain.AlertRepository; | ||
import com.atwoz.alert.infrastructure.dto.AlertPagingResponse; | ||
import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; | ||
import com.atwoz.global.application.BaseQueryService; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.data.domain.Page; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@RequiredArgsConstructor | ||
@Transactional(readOnly = true) | ||
@Service | ||
public class AlertQueryService extends BaseQueryService<AlertSearchResponse> { | ||
|
||
private final AlertRepository alertRepository; | ||
|
||
public AlertPagingResponse findMemberAlertsWithPaging(final Long memberId, final Pageable pageable) { | ||
Page<AlertSearchResponse> response = alertRepository.findMemberAlertsWithPaging(memberId, pageable); | ||
int nextPage = getNextPage(pageable.getPageNumber(), response); | ||
return AlertPagingResponse.of(response, nextPage); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.atwoz.alert.application; | ||
|
||
public interface AlertScheduler { | ||
|
||
void deleteExpiredAlerts(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package com.atwoz.alert.application; | ||
|
||
import com.atwoz.alert.domain.Alert; | ||
import com.atwoz.alert.domain.AlertRepository; | ||
import com.atwoz.alert.domain.AlertTokenRepository; | ||
import com.atwoz.alert.domain.vo.AlertGroup; | ||
import com.atwoz.alert.exception.exceptions.AlertNotFoundException; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@RequiredArgsConstructor | ||
@Transactional | ||
@Service | ||
public class AlertService { | ||
|
||
private final AlertRepository alertRepository; | ||
private final AlertTokenRepository tokenRepository; | ||
private final AlertManager alertManager; | ||
|
||
public void saveToken(final Long id, final String token) { | ||
tokenRepository.saveToken(id, token); | ||
} | ||
|
||
public void sendAlert(final AlertGroup group, final String title, final String body, final String sender, final Long receiverId) { | ||
String token = tokenRepository.getToken(receiverId); | ||
Alert alert = Alert.createWith(group, title, body, receiverId); | ||
Alert savedAlert = alertRepository.save(alert); | ||
alertManager.send(savedAlert, sender, token); | ||
} | ||
|
||
public Alert readAlert(final Long memberId, final Long id) { | ||
Alert alert = alertRepository.findByMemberIdAndId(memberId, id) | ||
.orElseThrow(AlertNotFoundException::new); | ||
alert.read(); | ||
return alert; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.atwoz.alert.application.event; | ||
|
||
import com.atwoz.alert.domain.vo.AlertGroup; | ||
|
||
public record AlertCreatedEvent( | ||
AlertGroup group, | ||
String title, | ||
String body, | ||
String sender, | ||
Long receiverId | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.atwoz.alert.application.event; | ||
|
||
public record AlertTokenCreatedEvent( | ||
Long id, | ||
String token | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package com.atwoz.alert.config; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.google.auth.oauth2.GoogleCredentials; | ||
import com.google.firebase.FirebaseApp; | ||
import com.google.firebase.FirebaseOptions; | ||
import com.google.firebase.ThreadManager; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
@Configuration | ||
public class FirebaseConfig { | ||
|
||
private static final String TYPE = "type"; | ||
private static final String PROJECT_ID = "project_id"; | ||
private static final String PRIVATE_KEY_ID = "private_key_id"; | ||
private static final String PRIVATE_KEY = "private_key"; | ||
private static final String CLIENT_EMAIL = "client_email"; | ||
private static final String CLIENT_ID = "client_id"; | ||
private static final String AUTH_URI = "auth_uri"; | ||
private static final String TOKEN_URI = "token_uri"; | ||
private static final String AUTH_CERT = "auth_provider_x509_cert_url"; | ||
private static final String CLIENT_CERT = "client_x509_cert_url"; | ||
private static final String UNIVERSE_DOMAIN = "universe_domain"; | ||
private static final String REPLACE_TARGET_MARK = "\\\\n"; | ||
private static final String REPLACE_VALUE_MARK = "\n"; | ||
|
||
@Value("${firebase.type}") | ||
private String type; | ||
|
||
@Value("${firebase.project_id}") | ||
private String projectId; | ||
|
||
@Value("${firebase.private_key_id}") | ||
private String privateKeyId; | ||
|
||
@Value("${firebase.private_key}") | ||
private String privateKey; | ||
|
||
@Value("${firebase.client_email}") | ||
private String clientEmail; | ||
|
||
@Value("${firebase.client_id}") | ||
private String clientId; | ||
|
||
@Value("${firebase.auth_uri}") | ||
private String authUri; | ||
|
||
@Value("${firebase.token_uri}") | ||
private String tokenUri; | ||
|
||
@Value("${firebase.auth_provider_x509_cert_url}") | ||
private String authCert; | ||
|
||
@Value("${firebase.client_x509_cert_url}") | ||
private String clientCert; | ||
|
||
@Value("${firebase.universe_domain}") | ||
private String universeDomain; | ||
|
||
@Bean | ||
public FirebaseApp firebaseApp() { | ||
if (!FirebaseApp.getApps().isEmpty()) { | ||
return FirebaseApp.getInstance(); | ||
} | ||
ThreadManager threadManager = new FirebaseThreadManager(); | ||
FirebaseOptions options = new FirebaseOptions.Builder() | ||
.setThreadManager(threadManager) | ||
.setCredentials(getCredentials()) | ||
.build(); | ||
return FirebaseApp.initializeApp(options); | ||
} | ||
|
||
private GoogleCredentials getCredentials() { | ||
try { | ||
String jsonContent = generateJsonContent(); | ||
InputStream inputStream = new ByteArrayInputStream(jsonContent.getBytes()); | ||
return GoogleCredentials.fromStream(inputStream); | ||
} catch (IOException e) { | ||
return GoogleCredentials.newBuilder() | ||
.build(); | ||
} | ||
} | ||
|
||
private String generateJsonContent() throws JsonProcessingException { | ||
ObjectMapper mapper = new ObjectMapper(); | ||
Map<String, String> firebaseConfig = generateFirebaseConfig(); | ||
firebaseConfig.put(PRIVATE_KEY, replaceNewLines(privateKey)); | ||
|
||
return mapper.writeValueAsString(firebaseConfig); | ||
} | ||
|
||
private Map<String, String> generateFirebaseConfig() { | ||
Map<String, String> firebaseConfig = new HashMap<>(); | ||
firebaseConfig.put(TYPE, type); | ||
firebaseConfig.put(PROJECT_ID, projectId); | ||
firebaseConfig.put(PRIVATE_KEY_ID, privateKeyId); | ||
firebaseConfig.put(CLIENT_EMAIL, clientEmail); | ||
firebaseConfig.put(CLIENT_ID, clientId); | ||
firebaseConfig.put(AUTH_URI, authUri); | ||
firebaseConfig.put(TOKEN_URI, tokenUri); | ||
firebaseConfig.put(AUTH_CERT, authCert); | ||
firebaseConfig.put(CLIENT_CERT, clientCert); | ||
firebaseConfig.put(UNIVERSE_DOMAIN, universeDomain); | ||
|
||
return firebaseConfig; | ||
} | ||
|
||
private String replaceNewLines(String value) { | ||
return value.replaceAll(REPLACE_TARGET_MARK, REPLACE_VALUE_MARK); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package com.atwoz.alert.config; | ||
|
||
import com.google.firebase.FirebaseApp; | ||
import com.google.firebase.ThreadManager; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.ThreadFactory; | ||
|
||
@Component | ||
public class FirebaseThreadManager extends ThreadManager { | ||
|
||
private static final int FIREBASE_THREADS_SIZE = 40; | ||
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. 쓰레드 40개로 선정하신 이유가 궁금합니다! 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. 사실 이 부분은 스레드를 얼마까지 정해뒀을 때 수요를 버틸 수 있을지 정확히 예측하기 힘들다고 판단하여 다른 글들을 참고해서 보수적으로 적용했었습니다. 실제 배포 후 모니터링한 뒤 더 늘릴 수 있다면 이후에 늘려보고 싶습니다. |
||
|
||
@Override | ||
protected ExecutorService getExecutor(final FirebaseApp firebaseApp) { | ||
return Executors.newFixedThreadPool(FIREBASE_THREADS_SIZE); | ||
} | ||
Comment on lines
+16
to
+19
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. newFixedThreadPool로 쓰레드를 설정해주셨는데요. 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.
|
||
|
||
@Override | ||
protected void releaseExecutor(final FirebaseApp firebaseApp, final ExecutorService executorService) { | ||
executorService.shutdownNow(); | ||
} | ||
|
||
@Override | ||
protected ThreadFactory getThreadFactory() { | ||
return Executors.defaultThreadFactory(); | ||
} | ||
} |
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.
오우 상수화랑 밸류 담느라 고생하셨네요 ㅠ