Skip to content

Commit 80c51fa

Browse files
authored
Merge pull request #126 from TaskFlow-CLAP/CLAP-150
CLAP-150 시스템과 외부 푸시 알림 기능 구현
2 parents 2ef0a44 + db001f4 commit 80c51fa

38 files changed

+1506
-15
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ dependencies {
9090
// Email Sender
9191
implementation 'org.springframework.boot:spring-boot-starter-mail'
9292

93+
// Thymeleaf
94+
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
95+
9396
// Spring aop
9497
implementation 'org.springframework.boot:spring-boot-starter-aop'
9598

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package clap.server.adapter.inbound.web.dto.notification;
2+
3+
import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;
4+
5+
public record SseRequest(
6+
String taskTitle,
7+
NotificationType notificationType,
8+
Long receiverId,
9+
String message
10+
) {
11+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package clap.server.adapter.inbound.web.notification;
2+
3+
4+
import clap.server.adapter.inbound.security.SecurityUserDetails;
5+
import clap.server.application.port.inbound.notification.SubscribeSseUsecase;
6+
import clap.server.common.annotation.architecture.WebAdapter;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.tags.Tag;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.http.MediaType;
11+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
12+
import org.springframework.web.bind.annotation.GetMapping;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RestController;
15+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
16+
17+
@Tag(name = "SSE 관리 - 회원 등록(최초 접속시)")
18+
@WebAdapter
19+
@RestController
20+
@RequestMapping("/api/sse")
21+
@RequiredArgsConstructor
22+
public class SubscribeEmitterController {
23+
24+
private final SubscribeSseUsecase subscribeSseUsecase;
25+
26+
@Operation(summary = "회원이 최초 접속 시 SSE(실시간 알림)에 연결하는 API")
27+
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
28+
public SseEmitter subscribe(@AuthenticationPrincipal SecurityUserDetails userInfo) {
29+
return subscribeSseUsecase.subscribe(userInfo.getUserId());
30+
}
31+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package clap.server.adapter.outbound.api;
2+
3+
import clap.server.adapter.outbound.api.dto.SendAgitRequest;
4+
import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;
5+
import clap.server.application.port.outbound.webhook.SendAgitPort;
6+
import clap.server.common.annotation.architecture.PersistenceAdapter;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.http.HttpEntity;
10+
import org.springframework.http.HttpHeaders;
11+
import org.springframework.http.HttpMethod;
12+
import org.springframework.web.client.RestTemplate;
13+
14+
15+
@PersistenceAdapter
16+
@RequiredArgsConstructor
17+
public class AgitClient implements SendAgitPort {
18+
19+
@Value("${agit.url}")
20+
private String AGITWEBHOOK_URL;
21+
22+
@Override
23+
public void sendAgit(SendAgitRequest request) {
24+
RestTemplate restTemplate = new RestTemplate();
25+
26+
String message = null;
27+
if (request.notificationType() == NotificationType.TASK_REQUESTED) {
28+
message = request.taskName() + " 작업이 요청되었습니다.";
29+
}
30+
else if (request.notificationType() == NotificationType.COMMENT) {
31+
message = request.taskName() + " 작업에 " + request.commenterName() + "님이 댓글을 남기셨습니다.";
32+
}
33+
else if (request.notificationType() == NotificationType.PROCESSOR_ASSIGNED) {
34+
message = request.taskName() + " 작업에 담당자(" + request.message() + ")가 배정되었습니다.";
35+
}
36+
else if (request.notificationType() == NotificationType.PROCESSOR_CHANGED) {
37+
message = request.taskName() + " 작업의 담당자가 " + request.message() + "로 변경되었습니다.";
38+
}
39+
else {
40+
message = request.taskName() + " 작업의 상태가 " + request.message() + "로 변경되었습니다";
41+
}
42+
43+
String payload = "{\"text\":\"" + message + "\"}";
44+
45+
HttpHeaders headers = new HttpHeaders();
46+
headers.add("Content-Type", "application/json");
47+
48+
HttpEntity<String> entity = new HttpEntity<>(payload, headers);
49+
50+
// Post 요청
51+
restTemplate.exchange(AGITWEBHOOK_URL, HttpMethod.POST, entity, String.class);
52+
}
53+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package clap.server.adapter.outbound.api;
2+
3+
import clap.server.adapter.outbound.api.dto.SendWebhookRequest;
4+
import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;
5+
import clap.server.application.port.outbound.webhook.SendEmailPort;
6+
import clap.server.common.annotation.architecture.PersistenceAdapter;
7+
import clap.server.exception.ApplicationException;
8+
import clap.server.exception.code.NotificationErrorCode;
9+
import jakarta.mail.internet.MimeMessage;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.mail.javamail.JavaMailSender;
12+
import org.springframework.mail.javamail.MimeMessageHelper;
13+
import org.thymeleaf.context.Context;
14+
import org.thymeleaf.spring6.SpringTemplateEngine;
15+
16+
@PersistenceAdapter
17+
@RequiredArgsConstructor
18+
public class EmailClient implements SendEmailPort {
19+
20+
private final SpringTemplateEngine templateEngine;
21+
private final JavaMailSender mailSender;
22+
23+
@Override
24+
public void sendEmail(SendWebhookRequest request) {
25+
try {
26+
MimeMessage mimeMessage = mailSender.createMimeMessage();
27+
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
28+
String body;
29+
Context context = new Context();
30+
31+
if (request.notificationType() == NotificationType.TASK_REQUESTED) {
32+
helper.setTo(request.email());
33+
helper.setSubject("[TaskFlow 알림] 신규 작업이 요청되었습니다.");
34+
35+
context.setVariable("receiverName", request.senderName());
36+
context.setVariable("title", request.taskName());
37+
38+
body = templateEngine.process("task-request", context);
39+
}
40+
else if (request.notificationType() == NotificationType.STATUS_SWITCHED) {
41+
helper.setTo(request.email());
42+
helper.setSubject("[TaskFlow 알림] 작업 상태가 변경되었습니다.");
43+
44+
context.setVariable("status", request.message());
45+
context.setVariable("title", request.taskName());
46+
47+
body = templateEngine.process("status-switch", context);
48+
}
49+
50+
else if (request.notificationType() == NotificationType.PROCESSOR_CHANGED) {
51+
helper.setTo(request.email());
52+
helper.setSubject("[TaskFlow 알림] 작업 담당자가 변경되었습니다.");
53+
54+
context.setVariable("processorName", request.message());
55+
context.setVariable("title", request.taskName());
56+
57+
body = templateEngine.process("processor-change", context);
58+
}
59+
60+
else if (request.notificationType() == NotificationType.PROCESSOR_ASSIGNED) {
61+
helper.setTo(request.email());
62+
helper.setSubject("[TaskFlow 알림] 작업 담당자가 지정되었습니다.");
63+
64+
context.setVariable("processorName", request.message());
65+
context.setVariable("title", request.taskName());
66+
67+
body = templateEngine.process("processor-assign", context);
68+
}
69+
70+
else {
71+
helper.setTo(request.email());
72+
helper.setSubject("[TaskFlow 알림] 댓글이 작성되었습니다.");
73+
74+
context.setVariable("comment", request.message());
75+
context.setVariable("title", request.taskName());
76+
77+
body = templateEngine.process("comment", context);
78+
}
79+
80+
helper.setText(body, true);
81+
mailSender.send(mimeMessage);
82+
} catch (Exception e) {
83+
throw new ApplicationException(NotificationErrorCode.EMAIL_SEND_FAILED);
84+
}
85+
}
86+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package clap.server.adapter.outbound.api;
2+
3+
import clap.server.adapter.outbound.api.dto.SendKakaoWorkRequest;
4+
import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;
5+
import clap.server.application.port.outbound.webhook.SendKaKaoWorkPort;
6+
import clap.server.common.annotation.architecture.PersistenceAdapter;
7+
import clap.server.exception.ApplicationException;
8+
import clap.server.exception.code.NotificationErrorCode;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.http.HttpEntity;
12+
import org.springframework.http.HttpHeaders;
13+
import org.springframework.http.HttpMethod;
14+
import org.springframework.web.client.RestTemplate;
15+
16+
@PersistenceAdapter
17+
@RequiredArgsConstructor
18+
public class KakaoWorkClient implements SendKaKaoWorkPort {
19+
20+
@Value("${kakaowork.url}")
21+
private String kakaworkUrl;
22+
23+
@Value("${kakaowork.auth}")
24+
private String kakaworkAuth;
25+
26+
private final ObjectBlockService makeObjectBlock;
27+
28+
@Override
29+
public void sendKakaoWord(SendKakaoWorkRequest request) {
30+
RestTemplate restTemplate = new RestTemplate();
31+
32+
// Payload 생성
33+
String payload = null;
34+
if (request.notificationType() == NotificationType.TASK_REQUESTED) {
35+
payload = makeObjectBlock.makeTaskRequestBlock(request);
36+
}
37+
else if (request.notificationType() == NotificationType.PROCESSOR_ASSIGNED) {
38+
payload = makeObjectBlock.makeNewProcessorBlock(request);
39+
}
40+
else if (request.notificationType() == NotificationType.PROCESSOR_CHANGED) {
41+
payload = makeObjectBlock.makeProcessorChangeBlock(request);
42+
}
43+
else if (request.notificationType() == NotificationType.STATUS_SWITCHED) {
44+
payload = makeObjectBlock.makeTaskStatusBlock(request);
45+
}
46+
else {
47+
payload = makeObjectBlock.makeCommentBlock(request);
48+
}
49+
50+
// HTTP 요청 헤더 설정
51+
HttpHeaders headers = new HttpHeaders();
52+
headers.add("Content-Type", "application/json");
53+
headers.add("Authorization", kakaworkAuth);
54+
55+
// HTTP 요청 엔터티 생성
56+
HttpEntity<String> entity = new HttpEntity<>(payload, headers);
57+
58+
try {
59+
// Post 요청 전송
60+
restTemplate.exchange(
61+
kakaworkUrl, HttpMethod.POST, entity, String.class
62+
);
63+
64+
} catch (Exception e) {
65+
throw new ApplicationException(NotificationErrorCode.KAKAO_SEND_FAILED);
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)