Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7065d7c
CLAP-149 HotFix: 작업 생성 및 수정 시 secured 오탈자 수정
parkjaehak Jan 26, 2025
d6d66b7
CLAP-149 HotFix: 작업생성에 대한 알림 생성 시 null 설정
parkjaehak Jan 26, 2025
2a96a08
CLAP-149 HotFix: 작업 생성 및 수정 시 mediatype json 추가
parkjaehak Jan 26, 2025
4ee201e
CLAP-150 Feature: 회원 최초 접속 시 Sse 등록 logic 구현
starboxxxx Jan 26, 2025
774442a
CLAP-150 Feature: 알림 생성 시 푸시 알림으로 receiver에게 실시간 전송하는 logic 구현
starboxxxx Jan 26, 2025
30916e2
CLAP-150 Fix: 알림 ErrorCode 추가
starboxxxx Jan 26, 2025
e19b5cc
CLAP-150 Feature: 알림 생성 시 sseRepository에서 실시간 알림을 전송하는 회원 조회하는 method
starboxxxx Jan 26, 2025
80607c6
CLAP-150 Feature: 이메일 webhook 푸시 알림 자동 전송 기능 구현
starboxxxx Jan 26, 2025
e47531f
CLAP-150 Feature: 이메일 webhook 푸시 알림 작성자 설정을 위한 초기 세팅(논의 필요)
starboxxxx Jan 26, 2025
84279a2
CLAP-150 Feature: KakaoWork 푸시 알림 자동 전송 기능 구현
starboxxxx Jan 26, 2025
3c0dbc7
CLAP-150 Feature: KakaoWork 푸시 알림 자동 전송 기능 구현에 포함
starboxxxx Jan 26, 2025
8137098
CLAP-150 Feature: KakaoWork 푸시 알림 자동 전송 기능 구현에 포함
starboxxxx Jan 26, 2025
a519d06
CLAP-150 Feature: Agit 실시간 Webhook 알림 전송 기능 구현
starboxxxx Jan 26, 2025
47d4584
CLAP-150 Feature: 이메일 전송에 필요한 html 파일
starboxxxx Jan 26, 2025
342c9d8
CLAP-150 Fix: Error Code 추가 설정
starboxxxx Jan 26, 2025
71af703
CLAP-150 Fix: Git Conflict
starboxxxx Jan 26, 2025
7821ec8
Merge remote-tracking branch 'origin/CLAP-150' into CLAP-150
starboxxxx Jan 26, 2025
d0a9ce9
CLAP-150 Fix: 이메일 알림 전송 기능에 필요한 thymeleaf 의존성 추가
starboxxxx Jan 26, 2025
00dcadd
CLAP-150 Fix: test yml에 spring mailsender 초기 설정 추가
starboxxxx Jan 27, 2025
261a21c
CLAP-150 Fix: 파일 구조 정리 및 불필요한 usecase 제거
starboxxxx Jan 27, 2025
6fb5e95
CLAP-150 Fix: Agit, KakaoWork 환경 변수 설정
starboxxxx Jan 27, 2025
eb4ec38
CLAP-150 Fix: SSE files infrastructure로 이동
starboxxxx Jan 27, 2025
202c6bb
CLAP-150 Fix: build 과정 중 test 실패 오류 관련 처리
starboxxxx Jan 27, 2025
7cee8a6
CLAP-150 Fix: build 과정 중 test 실패 오류 관련 처리
starboxxxx Jan 27, 2025
db001f4
CLAP-150 Fix: merge confliction
starboxxxx Jan 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ dependencies {
// Email Sender
implementation 'org.springframework.boot:spring-boot-starter-mail'

// Thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// Spring aop
implementation 'org.springframework.boot:spring-boot-starter-aop'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package clap.server.adapter.inbound.web.dto.common;


import java.util.List;

public record SliceResponse<T> (
List<T> content,
boolean hasNext,
boolean isFirst,
boolean isLast
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package clap.server.adapter.inbound.web.dto.notification;

import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;

public record SseRequest(
String taskTitle,
NotificationType notificationType,
Long receiverId,
String message
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package clap.server.adapter.inbound.web.notification;

import clap.server.adapter.inbound.security.SecurityUserDetails;
import clap.server.adapter.inbound.web.dto.common.SliceResponse;
import clap.server.adapter.inbound.web.dto.notification.FindNotificationListResponse;
import clap.server.application.port.inbound.notification.FindNotificationListUsecase;
import clap.server.common.annotation.architecture.WebAdapter;
Expand All @@ -9,7 +10,6 @@
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
Expand All @@ -34,7 +34,7 @@ public class FindNotificationController {
@Parameter(name = "size", description = "조회할 목록 페이지 당 개수", example = "5", required = false)
})
@GetMapping
public ResponseEntity<Page<FindNotificationListResponse>> findNotificationList(
public ResponseEntity<SliceResponse<FindNotificationListResponse>> findNotificationList(
@AuthenticationPrincipal SecurityUserDetails securityUserDetails,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "5") int size) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package clap.server.adapter.inbound.web.notification;

import clap.server.adapter.inbound.security.SecurityUserDetails;
import clap.server.application.port.inbound.notification.UpdateNotificationUsecase;
import clap.server.common.annotation.architecture.WebAdapter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -27,4 +29,10 @@ public class ManagementNotificationController {
public void updateNotificationIsRead(@PathVariable Long notificationId) {
updateNotificationUsecase.updateNotification(notificationId);
}

@Operation(summary = "알림 목록에서 전체 읽음 버튼을 눌렀을 때 전체 읽음 처리")
@PatchMapping
public void updateAllNotificationIsRead(@AuthenticationPrincipal SecurityUserDetails userInfo) {
updateNotificationUsecase.updateAllNotification(userInfo.getUserId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package clap.server.adapter.inbound.web.notification;


import clap.server.adapter.inbound.security.SecurityUserDetails;
import clap.server.application.port.inbound.notification.SubscribeSseUsecase;
import clap.server.common.annotation.architecture.WebAdapter;
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.security.core.annotation.AuthenticationPrincipal;
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;

@Tag(name = "SSE 관리 - 회원 등록(최초 접속시)")
@WebAdapter
@RestController
@RequestMapping("/api/sse")
@RequiredArgsConstructor
public class SubscribeEmitterController {

private final SubscribeSseUsecase subscribeSseUsecase;

@Operation(summary = "회원이 최초 접속 시 SSE(실시간 알림)에 연결하는 API")
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe(@AuthenticationPrincipal SecurityUserDetails userInfo) {
return subscribeSseUsecase.subscribe(userInfo.getUserId());
}
}
53 changes: 53 additions & 0 deletions src/main/java/clap/server/adapter/outbound/api/AgitClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package clap.server.adapter.outbound.api;

import clap.server.adapter.outbound.api.dto.SendAgitRequest;
import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;
import clap.server.application.port.outbound.webhook.SendAgitPort;
import clap.server.common.annotation.architecture.PersistenceAdapter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;


@PersistenceAdapter
@RequiredArgsConstructor
public class AgitClient implements SendAgitPort {

@Value("${agit.url}")
private String AGITWEBHOOK_URL;

@Override
public void sendAgit(SendAgitRequest request) {
RestTemplate restTemplate = new RestTemplate();

String message = null;
if (request.notificationType() == NotificationType.TASK_REQUESTED) {
message = request.taskName() + " 작업이 요청되었습니다.";
}
else if (request.notificationType() == NotificationType.COMMENT) {
message = request.taskName() + " 작업에 " + request.commenterName() + "님이 댓글을 남기셨습니다.";
}
else if (request.notificationType() == NotificationType.PROCESSOR_ASSIGNED) {
message = request.taskName() + " 작업에 담당자(" + request.message() + ")가 배정되었습니다.";
}
else if (request.notificationType() == NotificationType.PROCESSOR_CHANGED) {
message = request.taskName() + " 작업의 담당자가 " + request.message() + "로 변경되었습니다.";
}
else {
message = request.taskName() + " 작업의 상태가 " + request.message() + "로 변경되었습니다";
}

String payload = "{\"text\":\"" + message + "\"}";

HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");

HttpEntity<String> entity = new HttpEntity<>(payload, headers);

// Post 요청
restTemplate.exchange(AGITWEBHOOK_URL, HttpMethod.POST, entity, String.class);
}
}
86 changes: 86 additions & 0 deletions src/main/java/clap/server/adapter/outbound/api/EmailClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package clap.server.adapter.outbound.api;

import clap.server.adapter.outbound.api.dto.SendWebhookRequest;
import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;
import clap.server.application.port.outbound.webhook.SendEmailPort;
import clap.server.common.annotation.architecture.PersistenceAdapter;
import clap.server.exception.ApplicationException;
import clap.server.exception.code.NotificationErrorCode;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;

@PersistenceAdapter
@RequiredArgsConstructor
public class EmailClient implements SendEmailPort {

private final SpringTemplateEngine templateEngine;
private final JavaMailSender mailSender;

@Override
public void sendEmail(SendWebhookRequest request) {
try {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
String body;
Context context = new Context();

if (request.notificationType() == NotificationType.TASK_REQUESTED) {
helper.setTo(request.email());
helper.setSubject("[TaskFlow 알림] 신규 작업이 요청되었습니다.");

context.setVariable("receiverName", request.senderName());
context.setVariable("title", request.taskName());

body = templateEngine.process("task-request", context);
}
else if (request.notificationType() == NotificationType.STATUS_SWITCHED) {
helper.setTo(request.email());
helper.setSubject("[TaskFlow 알림] 작업 상태가 변경되었습니다.");

context.setVariable("status", request.message());
context.setVariable("title", request.taskName());

body = templateEngine.process("status-switch", context);
}

else if (request.notificationType() == NotificationType.PROCESSOR_CHANGED) {
helper.setTo(request.email());
helper.setSubject("[TaskFlow 알림] 작업 담당자가 변경되었습니다.");

context.setVariable("processorName", request.message());
context.setVariable("title", request.taskName());

body = templateEngine.process("processor-change", context);
}

else if (request.notificationType() == NotificationType.PROCESSOR_ASSIGNED) {
helper.setTo(request.email());
helper.setSubject("[TaskFlow 알림] 작업 담당자가 지정되었습니다.");

context.setVariable("processorName", request.message());
context.setVariable("title", request.taskName());

body = templateEngine.process("processor-assign", context);
}

else {
helper.setTo(request.email());
helper.setSubject("[TaskFlow 알림] 댓글이 작성되었습니다.");

context.setVariable("comment", request.message());
context.setVariable("title", request.taskName());

body = templateEngine.process("comment", context);
}

helper.setText(body, true);
mailSender.send(mimeMessage);
} catch (Exception e) {
throw new ApplicationException(NotificationErrorCode.EMAIL_SEND_FAILED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package clap.server.adapter.outbound.api;

import clap.server.adapter.outbound.api.dto.SendKakaoWorkRequest;
import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;
import clap.server.application.port.outbound.webhook.SendKaKaoWorkPort;
import clap.server.common.annotation.architecture.PersistenceAdapter;
import clap.server.exception.ApplicationException;
import clap.server.exception.code.NotificationErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;

@PersistenceAdapter
@RequiredArgsConstructor
public class KakaoWorkClient implements SendKaKaoWorkPort {

@Value("${kakaowork.url}")
private String kakaworkUrl;

@Value("${kakaowork.auth}")
private String kakaworkAuth;

private final ObjectBlockService makeObjectBlock;

@Override
public void sendKakaoWord(SendKakaoWorkRequest request) {
RestTemplate restTemplate = new RestTemplate();

// Payload 생성
String payload = null;
if (request.notificationType() == NotificationType.TASK_REQUESTED) {
payload = makeObjectBlock.makeTaskRequestBlock(request);
}
else if (request.notificationType() == NotificationType.PROCESSOR_ASSIGNED) {
payload = makeObjectBlock.makeNewProcessorBlock(request);
}
else if (request.notificationType() == NotificationType.PROCESSOR_CHANGED) {
payload = makeObjectBlock.makeProcessorChangeBlock(request);
}
else if (request.notificationType() == NotificationType.STATUS_SWITCHED) {
payload = makeObjectBlock.makeTaskStatusBlock(request);
}
else {
payload = makeObjectBlock.makeCommentBlock(request);
}

// HTTP 요청 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
headers.add("Authorization", kakaworkAuth);

// HTTP 요청 엔터티 생성
HttpEntity<String> entity = new HttpEntity<>(payload, headers);

try {
// Post 요청 전송
restTemplate.exchange(
kakaworkUrl, HttpMethod.POST, entity, String.class
);

} catch (Exception e) {
throw new ApplicationException(NotificationErrorCode.KAKAO_SEND_FAILED);
}
}
}
Loading
Loading