diff --git a/build.gradle b/build.gradle index c17d55e7..962217f7 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/common/SliceResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/common/SliceResponse.java index 3cffcec0..d8c08509 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/common/SliceResponse.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/common/SliceResponse.java @@ -5,8 +5,7 @@ public record SliceResponse ( List content, - int currentPage, - int size, + boolean hasNext, boolean isFirst, boolean isLast ) { diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/notification/SseRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/notification/SseRequest.java new file mode 100644 index 00000000..4dd9224c --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/notification/SseRequest.java @@ -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 +) { +} diff --git a/src/main/java/clap/server/adapter/inbound/web/notification/SubscribeEmitterController.java b/src/main/java/clap/server/adapter/inbound/web/notification/SubscribeEmitterController.java new file mode 100644 index 00000000..d2503ed7 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/notification/SubscribeEmitterController.java @@ -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()); + } +} diff --git a/src/main/java/clap/server/adapter/inbound/web/task/ManagementTaskController.java b/src/main/java/clap/server/adapter/inbound/web/task/ManagementTaskController.java index 3b20d9ee..7a4c0f19 100644 --- a/src/main/java/clap/server/adapter/inbound/web/task/ManagementTaskController.java +++ b/src/main/java/clap/server/adapter/inbound/web/task/ManagementTaskController.java @@ -33,8 +33,8 @@ public class ManagementTaskController { private final ApprovalTaskUsecase approvalTaskUsecase; @Operation(summary = "작업 요청 생성") - @PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) - @Secured({"ROLE_MANAGER, ROLE_USER"}) + @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + @Secured({"ROLE_MANAGER", "ROLE_USER"}) public ResponseEntity createTask( @RequestPart(name = "taskInfo") @Valid CreateTaskRequest createTaskRequest, @RequestPart(name = "attachment") @NotNull List attachments, @@ -44,8 +44,8 @@ public ResponseEntity createTask( } @Operation(summary = "작업 수정") - @PatchMapping(value = "/{taskId}", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) - @Secured({"ROLE_MANAGER, ROLE_USER"}) + @PatchMapping(value = "/{taskId}", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + @Secured({"ROLE_MANAGER", "ROLE_USER"}) public ResponseEntity updateTask( @PathVariable @NotNull Long taskId, @RequestPart(name = "taskInfo") @Valid UpdateTaskRequest updateTaskRequest, diff --git a/src/main/java/clap/server/adapter/outbound/api/AgitClient.java b/src/main/java/clap/server/adapter/outbound/api/AgitClient.java new file mode 100644 index 00000000..026018cd --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/api/AgitClient.java @@ -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 entity = new HttpEntity<>(payload, headers); + + // Post 요청 + restTemplate.exchange(AGITWEBHOOK_URL, HttpMethod.POST, entity, String.class); + } +} diff --git a/src/main/java/clap/server/adapter/outbound/api/EmailClient.java b/src/main/java/clap/server/adapter/outbound/api/EmailClient.java new file mode 100644 index 00000000..ee5fb2f9 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/api/EmailClient.java @@ -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); + } + } +} diff --git a/src/main/java/clap/server/adapter/outbound/api/KakaoWorkClient.java b/src/main/java/clap/server/adapter/outbound/api/KakaoWorkClient.java new file mode 100644 index 00000000..9557f973 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/api/KakaoWorkClient.java @@ -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 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); + } + } +} diff --git a/src/main/java/clap/server/adapter/outbound/api/ObjectBlockService.java b/src/main/java/clap/server/adapter/outbound/api/ObjectBlockService.java new file mode 100644 index 00000000..77c67eef --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/api/ObjectBlockService.java @@ -0,0 +1,373 @@ +package clap.server.adapter.outbound.api; + + +import clap.server.adapter.outbound.api.dto.SendKakaoWorkRequest; +import clap.server.adapter.outbound.persistense.repository.notification.NotificationRepository; +import clap.server.application.port.outbound.webhook.MakeObjectBlockPort; +import clap.server.common.annotation.architecture.PersistenceAdapter; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@PersistenceAdapter +@RequiredArgsConstructor +public class ObjectBlockService implements MakeObjectBlockPort { + + private final ObjectMapper objectMapper; + private final NotificationRepository notificationRepository; + + @Override + public String makeTaskRequestBlock(SendKakaoWorkRequest request) { + // Blocks 데이터 생성 + Object[] blocks = new Object[]{ + // Header 블록 + Map.of( + "type", "header", + "text", "TaskFlow 작업 요청 알림", + "style", "blue" + ), + // Text 블록 1 + Map.of( + "type", "text", + "text", "TaskFlow 작업 요청 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", "새로운 작업이 요청되었습니다.", + "bold", true + ) + } + ), + // Text 블록 2: 제목 변수 사용 + Map.of( + "type", "text", + "text", "TaskFlow 작업 요청 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - Task 제목 : " + request.taskName(), + "bold", false + ) + } + ), + // Text 블록 3: 요청자 변수 사용 + Map.of( + "type", "text", + "text", "TaskFlow 작업 요청 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - 요청자 : " + request.senderName(), + "bold", false + ) + } + ), + // Button 블록 + Map.of( + "type", "button", + "text", "확인하기", + "style", "default", + "action", Map.of( + "type", "open_system_browser", + "name", "button1", + "value", "http://example.com/details/999" + ) + ) + }; + + String payload; + try { + payload = "{" + + "\"email\":\"" + request.email() + "\"," + + "\"text\": \"신규 작업 요청 알림\"," + // fallback 메시지 + "\"blocks\":" + objectMapper.writeValueAsString(blocks) + + "}"; + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + return payload; + } + + @Override + public String makeNewProcessorBlock(SendKakaoWorkRequest request) { + Object[] blocks = new Object[]{ + Map.of( + "type", "header", + "text", "TaskFlow 알림 서비스", + "style", "blue" + ), + Map.of( + "type", "text", + "text", "TaskFlow 담당자 선정 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", "TaskFlow 담당자 선정 알림", + "bold", true + ) + } + ), + Map.of( + "type", "text", + "text", "TaskFlow 담당자 선정 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - Task 제목 : " + request.taskName(), + "bold", false + ) + } + ), + Map.of( + "type", "text", + "text", "TaskFlow 담당자 선정 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - 담당자 : " + request.message(), + "bold", false + ) + } + ), + Map.of( + "type", "button", + "text", "확인하기", + "style", "default", + "action", Map.of( + "type", "open_system_browser", + "name", "button1", + "value", "http://example.com/details/999" + ) + ) + }; + + String payload; + try { + payload = "{" + + "\"email\":\"" + request.email() + "\"," + + "\"text\":\"작업 담당자 할당 알림\"," + // fallback 메시지 + "\"blocks\":" + objectMapper.writeValueAsString(blocks) + + "}"; + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + return payload; + } + + @Override + public String makeProcessorChangeBlock(SendKakaoWorkRequest request) { + Object[] blocks = new Object[]{ + Map.of( + "type", "header", + "text", "TaskFlow 알림 서비스", + "style", "blue" + ), + Map.of( + "type", "text", + "text", "TaskFlow 담당자 변경 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", "TaskFlow 담당자 변경 알림", + "bold", true + ) + } + ), + Map.of( + "type", "text", + "text", "TaskFlow 담당자 변경 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - Task 제목 : " + request.taskName(), + "bold", false + ) + } + ), + Map.of( + "type", "text", + "text", "TaskFlow 담당자 변경 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - 담당자 : " + request.message(), + "bold", false + ) + } + ), + Map.of( + "type", "button", + "text", "확인하기", + "style", "default", + "action", Map.of( + "type", "open_system_browser", + "name", "button1", + "value", "http://example.com/details/999" + ) + ) + }; + + String payload; + try { + payload = "{" + + "\"email\":\"" + request.email() + "\"," + + "\"text\":\"작업 담당자 변경 알림\"," + // fallback 메시지 + "\"blocks\":" + objectMapper.writeValueAsString(blocks) + + "}"; + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + + return payload; + } + + @Override + public String makeCommentBlock(SendKakaoWorkRequest request) { + Object[] blocks = new Object[]{ + Map.of( + "type", "header", + "text", "TaskFlow 알림 서비스", + "style", "blue" + ), + Map.of( + "type", "text", + "text", "TaskFlow 댓글 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", "TaskFlow 댓글 알림", + "bold", true + ) + } + ), + Map.of( + "type", "text", + "text", "TaskFlow 댓글 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - Task Title : " + request.taskName(), + "bold", false + ) + } + ), + Map.of( + "type", "text", + "text", "TaskFlow 댓글 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - 작성자 : " + request.commenterName(), + "bold", false + ) + } + ), + Map.of( + "type", "text", + "text", "TaskFlow 댓글 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - 내용 : " + request.message(), + "bold", false + ) + } + ), + Map.of( + "type", "button", + "text", "확인하기", + "style", "default", + "action", Map.of( + "type", "open_system_browser", + "name", "button1", + "value", "http://example.com/details/999" + ) + ) + }; + + String payload; + try { + payload = "{" + + "\"email\":\"" + request.email() + "\"," + + "\"text\":\"댓글 알림\"," + // fallback 메시지 + "\"blocks\":" + objectMapper.writeValueAsString(blocks) + + "}"; + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + return payload; + } + + @Override + public String makeTaskStatusBlock(SendKakaoWorkRequest request) { + Object[] blocks = new Object[]{ + Map.of( + "type", "header", + "text", "TaskFlow 알림 서비스", + "style", "blue" + ), + Map.of( + "type", "text", + "text", "TaskFlow 작업 상태 변경 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", "TaskFlow 작업 상태 변경 알림", + "bold", true + ) + } + ), + Map.of( + "type", "text", + "text", "TaskFlow 상태 변경 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - Task Title : " + request.taskName(), + "bold", false + ) + } + ), + Map.of( + "type", "text", + "text", "TaskFlow 작업 상태 변경 알림", + "inlines", new Object[]{ + Map.of( + "type", "styled", + "text", " - 상태 : " + request.message(), + "bold", false + ) + } + ), + Map.of( + "type", "button", + "text", "확인하기", + "style", "default", + "action", Map.of( + "type", "open_system_browser", + "name", "button1", + "value", "http://example.com/details/999" + ) + ) + }; + + String payload; + try { + payload = "{" + + "\"email\":\"" + request.email() + "\"," + + "\"text\":\"작업 상태 변경 알림\"," + // fallback 메시지 + "\"blocks\":" + objectMapper.writeValueAsString(blocks) + + "}"; + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + return payload; + } +} diff --git a/src/main/java/clap/server/adapter/outbound/api/dto/SendAgitRequest.java b/src/main/java/clap/server/adapter/outbound/api/dto/SendAgitRequest.java new file mode 100644 index 00000000..65add4ab --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/api/dto/SendAgitRequest.java @@ -0,0 +1,13 @@ +package clap.server.adapter.outbound.api.dto; + +import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; + +public record SendAgitRequest( + String email, + NotificationType notificationType, + String taskName, + String senderName, + String message, + String commenterName +) { +} diff --git a/src/main/java/clap/server/adapter/outbound/api/dto/SendKakaoWorkRequest.java b/src/main/java/clap/server/adapter/outbound/api/dto/SendKakaoWorkRequest.java new file mode 100644 index 00000000..2bf6252d --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/api/dto/SendKakaoWorkRequest.java @@ -0,0 +1,14 @@ +package clap.server.adapter.outbound.api.dto; + +import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; + +public record SendKakaoWorkRequest( + + String email, + NotificationType notificationType, + String taskName, + String senderName, + String message, + String commenterName +) { +} diff --git a/src/main/java/clap/server/adapter/outbound/api/dto/SendWebhookRequest.java b/src/main/java/clap/server/adapter/outbound/api/dto/SendWebhookRequest.java new file mode 100644 index 00000000..81bfe62c --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/api/dto/SendWebhookRequest.java @@ -0,0 +1,14 @@ +package clap.server.adapter.outbound.api.dto; + +import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; + +public record SendWebhookRequest( + + String email, + NotificationType notificationType, + String taskName, + String senderName, + String message, + String commenterName +) { +} diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/sse/SsePersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/infrastructure/sse/SsePersistenceAdapter.java new file mode 100644 index 00000000..18e04210 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/sse/SsePersistenceAdapter.java @@ -0,0 +1,30 @@ +package clap.server.adapter.outbound.infrastructure.sse; + +import clap.server.adapter.outbound.infrastructure.sse.repository.EmitterRepository; +import clap.server.application.port.outbound.notification.CommandSsePort; +import clap.server.application.port.outbound.notification.LoadSsePort; +import clap.server.common.annotation.architecture.PersistenceAdapter; +import lombok.RequiredArgsConstructor; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@PersistenceAdapter +@RequiredArgsConstructor +public class SsePersistenceAdapter implements LoadSsePort, CommandSsePort { + + private final EmitterRepository emitterRepository; + + @Override + public void save(Long receiverId, SseEmitter emitter) { + emitterRepository.save(receiverId, emitter); + } + + @Override + public void delete(Long receiverId) { + emitterRepository.delete(receiverId); + } + + @Override + public SseEmitter get(Long receiverId) { + return emitterRepository.get(receiverId); + } +} diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/sse/repository/EmitterRepository.java b/src/main/java/clap/server/adapter/outbound/infrastructure/sse/repository/EmitterRepository.java new file mode 100644 index 00000000..6d201b25 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/sse/repository/EmitterRepository.java @@ -0,0 +1,12 @@ +package clap.server.adapter.outbound.infrastructure.sse.repository; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +public interface EmitterRepository { + void save(Long id, SseEmitter emitter); + + void delete(Long id); + + SseEmitter get(Long id); + +} diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/sse/repository/EmitterRepositoryImpl.java b/src/main/java/clap/server/adapter/outbound/infrastructure/sse/repository/EmitterRepositoryImpl.java new file mode 100644 index 00000000..2651022f --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/sse/repository/EmitterRepositoryImpl.java @@ -0,0 +1,30 @@ +package clap.server.adapter.outbound.infrastructure.sse.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 EmitterRepositoryImpl implements EmitterRepository{ + + private final Map emitters = new ConcurrentHashMap<>(); + + @Override + public void save(Long id, SseEmitter emitter) { + emitters.put(id, emitter); + } + + @Override + public void delete(Long id) { + emitters.remove(id); + } + + @Override + public SseEmitter get(Long id) { + return emitters.get(id); + } +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/evenlistener/CustomEventListener.java b/src/main/java/clap/server/adapter/outbound/persistense/evenlistener/CustomEventListener.java index 3a7ba0a0..5e88dcad 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/evenlistener/CustomEventListener.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/evenlistener/CustomEventListener.java @@ -1,11 +1,17 @@ package clap.server.adapter.outbound.persistense.evenlistener; +import clap.server.adapter.inbound.web.dto.notification.SseRequest; +import clap.server.adapter.outbound.api.dto.SendAgitRequest; +import clap.server.adapter.outbound.api.dto.SendKakaoWorkRequest; +import clap.server.adapter.outbound.api.dto.SendWebhookRequest; import clap.server.application.service.notification.CreateNotificationService; +import clap.server.application.service.notification.SendSseService; +import clap.server.application.service.webhook.SendAgitService; +import clap.server.application.service.webhook.SendEmailService; +import clap.server.application.service.webhook.SendKaKaoWorkService; import clap.server.domain.model.notification.Notification; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; @@ -14,10 +20,33 @@ @Component public class CustomEventListener { private final CreateNotificationService createNotificationService; + private final SendSseService sendSseService; + private final SendEmailService sendEmailService; + private final SendKaKaoWorkService sendKaKaoWorkService; + private final SendAgitService agitService; @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - @Transactional(propagation = Propagation.REQUIRES_NEW) public void handler(Notification request) { createNotificationService.createNotification(request); } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleSseNotification(SseRequest sseRequest) { + sendSseService.send(sseRequest); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleEmailSend(SendWebhookRequest email) { + sendEmailService.sendEmail(email); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleKakaoWorkSend(SendKakaoWorkRequest kakaoWork) { + sendKaKaoWorkService.sendKaKaoWork(kakaoWork); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleAgitSend(SendAgitRequest agit) { + agitService.sendAgit(agit); + } } diff --git a/src/main/java/clap/server/application/Task/CreateTaskService.java b/src/main/java/clap/server/application/Task/CreateTaskService.java index e529fc4e..8425d1c9 100644 --- a/src/main/java/clap/server/application/Task/CreateTaskService.java +++ b/src/main/java/clap/server/application/Task/CreateTaskService.java @@ -1,8 +1,12 @@ package clap.server.application.Task; +import clap.server.adapter.inbound.web.dto.notification.SseRequest; import clap.server.adapter.inbound.web.dto.task.CreateTaskRequest; import clap.server.adapter.inbound.web.dto.task.CreateTaskResponse; +import clap.server.adapter.outbound.api.dto.SendAgitRequest; +import clap.server.adapter.outbound.api.dto.SendKakaoWorkRequest; +import clap.server.adapter.outbound.api.dto.SendWebhookRequest; import clap.server.adapter.outbound.infrastructure.s3.S3UploadAdapter; import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; import clap.server.application.mapper.AttachmentMapper; @@ -13,6 +17,7 @@ import clap.server.application.port.outbound.task.CommandAttachmentPort; import clap.server.application.port.outbound.task.CommandTaskPort; +import clap.server.application.service.webhook.SendKaKaoWorkService; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.member.Member; import clap.server.domain.model.notification.Notification; @@ -40,6 +45,7 @@ public class CreateTaskService implements CreateTaskUsecase { private final CommandTaskPort commandTaskPort; private final CommandAttachmentPort commandAttachmentPort; private final S3UploadAdapter s3UploadAdapter; + private final SendKaKaoWorkService sendKaKaoWorkService; private final ApplicationEventPublisher applicationEventPublisher; @Override @@ -66,8 +72,51 @@ private void publishNotification(Task task){ // 검토자들 각각에 대한 알림 생성 후 event 발행 for (Member reviewer : reviewers) { + // 알림 저장 Notification notification = createTaskNotification(task, reviewer, NotificationType.TASK_REQUESTED); applicationEventPublisher.publishEvent(notification); + + // SSE 실시간 알림 전송 + SseRequest sseRequest = new SseRequest( + notification.getTask().getTitle(), + notification.getType(), + reviewer.getMemberId(), + null + ); + applicationEventPublisher.publishEvent(sseRequest); + + //Email Webhook 실시간 전송 + SendWebhookRequest sendWebhookRequest = new SendWebhookRequest( + reviewer.getMemberInfo().getEmail(), + NotificationType.TASK_REQUESTED, + task.getTitle(), + task.getRequester().getNickname(), + null, + null + ); + applicationEventPublisher.publishEvent(sendWebhookRequest); + + //Kakao Webhook 실시간 전송 + SendKakaoWorkRequest sendKakaoWorkRequest = new SendKakaoWorkRequest( + reviewer.getMemberInfo().getEmail(), + NotificationType.TASK_REQUESTED, + task.getTitle(), + task.getRequester().getNickname(), + null, + null + ); + applicationEventPublisher.publishEvent(sendKakaoWorkRequest); + + //아지트 Webhook 실시간 전송 + SendAgitRequest sendAgitRequest = new SendAgitRequest( + reviewer.getMemberInfo().getEmail(), + NotificationType.TASK_REQUESTED, + task.getTitle(), + task.getRequester().getNickname(), + null, + null + ); + applicationEventPublisher.publishEvent(sendAgitRequest); } } diff --git a/src/main/java/clap/server/application/mapper/NotificationMapper.java b/src/main/java/clap/server/application/mapper/NotificationMapper.java index d53d71b4..e2c83e52 100644 --- a/src/main/java/clap/server/application/mapper/NotificationMapper.java +++ b/src/main/java/clap/server/application/mapper/NotificationMapper.java @@ -23,8 +23,7 @@ public static FindNotificationListResponse toFindNoticeListResponse(Notification public static SliceResponse toSliceOfFindNoticeListResponse(Slice slice) { return new SliceResponse<>( slice.getContent(), - slice.getNumber(), - slice.getSize(), + slice.hasNext(), slice.isFirst(), slice.isLast() ); diff --git a/src/main/java/clap/server/application/port/inbound/notification/CreateNotificationUsecase.java b/src/main/java/clap/server/application/port/inbound/notification/CreateNotificationUsecase.java deleted file mode 100644 index 147ced9e..00000000 --- a/src/main/java/clap/server/application/port/inbound/notification/CreateNotificationUsecase.java +++ /dev/null @@ -1,7 +0,0 @@ -package clap.server.application.port.inbound.notification; - -import clap.server.domain.model.notification.Notification; - -public interface CreateNotificationUsecase { - void createNotification(Notification notification); -} diff --git a/src/main/java/clap/server/application/port/inbound/notification/SendSseUsecase.java b/src/main/java/clap/server/application/port/inbound/notification/SendSseUsecase.java new file mode 100644 index 00000000..c7b4e5a3 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/notification/SendSseUsecase.java @@ -0,0 +1,8 @@ +package clap.server.application.port.inbound.notification; + +import clap.server.adapter.inbound.web.dto.notification.SseRequest; + +public interface SendSseUsecase { + + void send(SseRequest request); +} diff --git a/src/main/java/clap/server/application/port/inbound/notification/SubscribeSseUsecase.java b/src/main/java/clap/server/application/port/inbound/notification/SubscribeSseUsecase.java new file mode 100644 index 00000000..20a270c4 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/notification/SubscribeSseUsecase.java @@ -0,0 +1,8 @@ +package clap.server.application.port.inbound.notification; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +public interface SubscribeSseUsecase { + + SseEmitter subscribe(Long memberId); +} diff --git a/src/main/java/clap/server/application/port/outbound/notification/CommandSsePort.java b/src/main/java/clap/server/application/port/outbound/notification/CommandSsePort.java new file mode 100644 index 00000000..465095f7 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/notification/CommandSsePort.java @@ -0,0 +1,12 @@ +package clap.server.application.port.outbound.notification; + + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +public interface CommandSsePort { + + void save(Long receiverId, SseEmitter emitter); + + void delete(Long receiverId); + +} diff --git a/src/main/java/clap/server/application/port/outbound/notification/LoadSsePort.java b/src/main/java/clap/server/application/port/outbound/notification/LoadSsePort.java new file mode 100644 index 00000000..88a085c4 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/notification/LoadSsePort.java @@ -0,0 +1,8 @@ +package clap.server.application.port.outbound.notification; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +public interface LoadSsePort { + + SseEmitter get(Long receiverId); +} diff --git a/src/main/java/clap/server/application/port/outbound/webhook/MakeObjectBlockPort.java b/src/main/java/clap/server/application/port/outbound/webhook/MakeObjectBlockPort.java new file mode 100644 index 00000000..9ca0ac86 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/webhook/MakeObjectBlockPort.java @@ -0,0 +1,16 @@ +package clap.server.application.port.outbound.webhook; + +import clap.server.adapter.outbound.api.dto.SendKakaoWorkRequest; + +public interface MakeObjectBlockPort { + + String makeTaskRequestBlock(SendKakaoWorkRequest request); + + String makeNewProcessorBlock(SendKakaoWorkRequest request); + + String makeProcessorChangeBlock(SendKakaoWorkRequest request); + + String makeCommentBlock(SendKakaoWorkRequest request); + + String makeTaskStatusBlock(SendKakaoWorkRequest request); +} diff --git a/src/main/java/clap/server/application/port/outbound/webhook/SendAgitPort.java b/src/main/java/clap/server/application/port/outbound/webhook/SendAgitPort.java new file mode 100644 index 00000000..f6e7cfd3 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/webhook/SendAgitPort.java @@ -0,0 +1,7 @@ +package clap.server.application.port.outbound.webhook; + +import clap.server.adapter.outbound.api.dto.SendAgitRequest; + +public interface SendAgitPort { + void sendAgit(SendAgitRequest request); +} diff --git a/src/main/java/clap/server/application/port/outbound/webhook/SendEmailPort.java b/src/main/java/clap/server/application/port/outbound/webhook/SendEmailPort.java new file mode 100644 index 00000000..e69f7094 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/webhook/SendEmailPort.java @@ -0,0 +1,8 @@ +package clap.server.application.port.outbound.webhook; + +import clap.server.adapter.outbound.api.dto.SendWebhookRequest; + +public interface SendEmailPort { + + void sendEmail(SendWebhookRequest request); +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/port/outbound/webhook/SendKaKaoWorkPort.java b/src/main/java/clap/server/application/port/outbound/webhook/SendKaKaoWorkPort.java new file mode 100644 index 00000000..03550a57 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/webhook/SendKaKaoWorkPort.java @@ -0,0 +1,8 @@ +package clap.server.application.port.outbound.webhook; + +import clap.server.adapter.outbound.api.dto.SendKakaoWorkRequest; + +public interface SendKaKaoWorkPort { + + void sendKakaoWord(SendKakaoWorkRequest request); +} diff --git a/src/main/java/clap/server/application/service/notification/CreateNotificationService.java b/src/main/java/clap/server/application/service/notification/CreateNotificationService.java index 840acb69..8071eba1 100644 --- a/src/main/java/clap/server/application/service/notification/CreateNotificationService.java +++ b/src/main/java/clap/server/application/service/notification/CreateNotificationService.java @@ -1,6 +1,5 @@ package clap.server.application.service.notification; -import clap.server.application.port.inbound.notification.CreateNotificationUsecase; import clap.server.application.port.outbound.notification.CommandNotificationPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.notification.Notification; @@ -8,11 +7,10 @@ @ApplicationService @RequiredArgsConstructor -public class CreateNotificationService implements CreateNotificationUsecase { +public class CreateNotificationService{ private final CommandNotificationPort commandNotificationPort; - @Override public void createNotification(Notification request) { commandNotificationPort.save(request); diff --git a/src/main/java/clap/server/application/service/notification/SendSseService.java b/src/main/java/clap/server/application/service/notification/SendSseService.java new file mode 100644 index 00000000..4139b53d --- /dev/null +++ b/src/main/java/clap/server/application/service/notification/SendSseService.java @@ -0,0 +1,31 @@ +package clap.server.application.service.notification; + +import clap.server.adapter.inbound.web.dto.notification.SseRequest; +import clap.server.application.port.inbound.notification.SendSseUsecase; +import clap.server.application.port.outbound.notification.LoadSsePort; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.NotificationErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; + +@ApplicationService +@RequiredArgsConstructor +public class SendSseService implements SendSseUsecase { + + private final LoadSsePort loadSsePort; + + @Override + public void send(SseRequest request) { + SseEmitter sseEmitter = loadSsePort.get(request.receiverId()); + try { + sseEmitter.send(SseEmitter.event() + .id(String.valueOf(request.receiverId())) + .data(request)); + } catch (Exception e) { + throw new ApplicationException(NotificationErrorCode.SSE_SEND_FAILED); + } + } +} diff --git a/src/main/java/clap/server/application/service/notification/SubscribeSseService.java b/src/main/java/clap/server/application/service/notification/SubscribeSseService.java new file mode 100644 index 00000000..558e4aa5 --- /dev/null +++ b/src/main/java/clap/server/application/service/notification/SubscribeSseService.java @@ -0,0 +1,42 @@ +package clap.server.application.service.notification; + +import clap.server.application.port.inbound.notification.SubscribeSseUsecase; +import clap.server.application.port.outbound.notification.CommandSsePort; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.NotificationErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import java.io.IOException; + +@ApplicationService +@RequiredArgsConstructor +public class SubscribeSseService implements SubscribeSseUsecase { + + private final CommandSsePort commandSsePort; + + // SSE 연결 지속 시간 + private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; + + @Override + public SseEmitter subscribe(Long memberId) { + SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); + + commandSsePort.save(memberId, emitter); + + // SSE가 작업을 완료하거나 종료되었을때 emitterRepository에서 해당 연결 값 삭제 + emitter.onCompletion(() -> commandSsePort.delete(memberId)); + emitter.onTimeout(() -> commandSsePort.delete(memberId)); + + try { + emitter.send(SseEmitter.event() + .id(String.valueOf(memberId)) + .data("Sse 연결 성공. [memberId = " + memberId + "]")); + } catch (Exception e) { + throw new ApplicationException(NotificationErrorCode.SSE_SEND_FAILED); + } + return emitter; + } + + +} diff --git a/src/main/java/clap/server/application/service/webhook/SendAgitService.java b/src/main/java/clap/server/application/service/webhook/SendAgitService.java new file mode 100644 index 00000000..958ce52c --- /dev/null +++ b/src/main/java/clap/server/application/service/webhook/SendAgitService.java @@ -0,0 +1,17 @@ +package clap.server.application.service.webhook; + +import clap.server.adapter.outbound.api.dto.SendAgitRequest; +import clap.server.application.port.outbound.webhook.SendAgitPort; +import clap.server.common.annotation.architecture.ApplicationService; +import lombok.RequiredArgsConstructor; + +@ApplicationService +@RequiredArgsConstructor +public class SendAgitService { + + private final SendAgitPort agitPort; + + public void sendAgit(SendAgitRequest request) { + agitPort.sendAgit(request); + } +} diff --git a/src/main/java/clap/server/application/service/webhook/SendEmailService.java b/src/main/java/clap/server/application/service/webhook/SendEmailService.java new file mode 100644 index 00000000..72f93712 --- /dev/null +++ b/src/main/java/clap/server/application/service/webhook/SendEmailService.java @@ -0,0 +1,17 @@ +package clap.server.application.service.webhook; + +import clap.server.adapter.outbound.api.dto.SendWebhookRequest; +import clap.server.application.port.outbound.webhook.SendEmailPort; +import clap.server.common.annotation.architecture.ApplicationService; +import lombok.RequiredArgsConstructor; + +@ApplicationService +@RequiredArgsConstructor +public class SendEmailService { + + private final SendEmailPort port; + + public void sendEmail(SendWebhookRequest request) { + port.sendEmail(request); + } +} diff --git a/src/main/java/clap/server/application/service/webhook/SendKaKaoWorkService.java b/src/main/java/clap/server/application/service/webhook/SendKaKaoWorkService.java new file mode 100644 index 00000000..35ea3f22 --- /dev/null +++ b/src/main/java/clap/server/application/service/webhook/SendKaKaoWorkService.java @@ -0,0 +1,17 @@ +package clap.server.application.service.webhook; + +import clap.server.adapter.outbound.api.dto.SendKakaoWorkRequest; +import clap.server.application.port.outbound.webhook.SendKaKaoWorkPort; +import clap.server.common.annotation.architecture.ApplicationService; +import lombok.RequiredArgsConstructor; + +@ApplicationService +@RequiredArgsConstructor +public class SendKaKaoWorkService { + + private final SendKaKaoWorkPort sendKaKaoWorkPort; + + public void sendKaKaoWork(SendKakaoWorkRequest request) { + sendKaKaoWorkPort.sendKakaoWord(request); + } +} diff --git a/src/main/java/clap/server/exception/code/NotificationErrorCode.java b/src/main/java/clap/server/exception/code/NotificationErrorCode.java index f6805e61..237b3c96 100644 --- a/src/main/java/clap/server/exception/code/NotificationErrorCode.java +++ b/src/main/java/clap/server/exception/code/NotificationErrorCode.java @@ -9,6 +9,10 @@ public enum NotificationErrorCode implements BaseErrorCode { NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "NOTIFICATION_001", "알림을 찾을 수 없습니다"), + SSE_SEND_FAILED(HttpStatus.BAD_REQUEST, "NOTIFICATION_002", "SSE 초기 연결에 실패하였습니다"), + EMAIL_SEND_FAILED(HttpStatus.BAD_REQUEST, "NOTIFICATION_003", "이메일 알림 전송에 실패하였습니다"), + KAKAO_SEND_FAILED(HttpStatus.BAD_REQUEST, "NOTIFICATION_004", "카카오워크 알림 전송에 실패하였습니다"), + AGIT_SEND_FAILED(HttpStatus.BAD_REQUEST, "NOTIFICATION_005", "아지트 알림 전송에 실패하였습니다"), ; private final HttpStatus httpStatus; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f4527cd4..a689f8f0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,6 +13,20 @@ spring: name: taskflow web.resources.add-mappings: false + # 구글 알림 이메일 발신자 정보 설정(논의 필요) + mail: + host: smtp.gmail.com + port: 587 + username: leegd120@gmail.com + password: znlictzarqurxlla + properties: + mail: + smtp: + auth: true + starttls: + enable: true + + server: port: ${APPLICATION_PORT:8080} @@ -27,8 +41,21 @@ server: web: domain: - local: ${TASKFLOW_LOCAL_WEB:127.0.0.1:3O00} - service: ${TASKFLOW_SERVICE_WEB:127.0.0.1:3000} + local: ${TASKFLOW_LOCAL_WEB:127.0.0.1:5173} + service: ${TASKFLOW_SERVICE_WEB:127.0.0.1:5173} + + +# 카카오워크 및 agit url, accessKey값 환경 변수로 설정 +kakaowork: + url: https://api.kakaowork.com/v1/messages.send_by_email + auth: Bearer 1b01becc.a7f10da76d2e4038948771107cfe5c1d + +agit: + url: https://agit.io/webhook/a342181d-fb18-4eb0-a99a-30f4fb5b14b1 + +local: ${TASKFLOW_LOCAL_WEB:127.0.0.1:5173} +service: ${TASKFLOW_SERVICE_WEB:127.0.0.1:5173} + #logging: # level: # root: INFO diff --git a/src/main/resources/templates/comment.html b/src/main/resources/templates/comment.html new file mode 100644 index 00000000..08f63efc --- /dev/null +++ b/src/main/resources/templates/comment.html @@ -0,0 +1,87 @@ + + + + + Notion Notification + + + + + + diff --git a/src/main/resources/templates/processor-assign.html b/src/main/resources/templates/processor-assign.html new file mode 100644 index 00000000..0de7fd85 --- /dev/null +++ b/src/main/resources/templates/processor-assign.html @@ -0,0 +1,87 @@ + + + + + Notion Notification + + + + + + diff --git a/src/main/resources/templates/processor-change.html b/src/main/resources/templates/processor-change.html new file mode 100644 index 00000000..7806a74b --- /dev/null +++ b/src/main/resources/templates/processor-change.html @@ -0,0 +1,87 @@ + + + + + Notion Notification + + + + + + diff --git a/src/main/resources/templates/status-switch.html b/src/main/resources/templates/status-switch.html new file mode 100644 index 00000000..374233d2 --- /dev/null +++ b/src/main/resources/templates/status-switch.html @@ -0,0 +1,87 @@ + + + + + Notion Notification + + + + + + diff --git a/src/main/resources/templates/task-request.html b/src/main/resources/templates/task-request.html new file mode 100644 index 00000000..5c349ca5 --- /dev/null +++ b/src/main/resources/templates/task-request.html @@ -0,0 +1,87 @@ + + + + + Notion Notification + + + + + + diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 683f2af6..b10d6c61 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -14,6 +14,18 @@ spring: hibernate: ddl-auto: create + mail: + host: smtp.gmail.com + port: 587 + username: leegd120@gmail.com + password: znlictzarqurxlla + properties: + mail: + smtp: + auth: true + starttls: + enable: true + testcontainers: beans: startup: parallel @@ -46,4 +58,11 @@ password: length: 12 characters: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+ +kakaowork: + url: https://api.kakaowork.com/v1/messages.send_by_email + auth: Bearer 1b01becc.a7f10da76d2e4038948771107cfe5c1d + +agit: + url: https://agit.io/webhook/a342181d-fb18-4eb0-a99a-30f4fb5b14b1 +