Skip to content
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

알림 추가 #150 #154

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 2 additions & 5 deletions src/main/java/io/oduck/api/domain/contact/dto/ContactReq.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.oduck.api.domain.contact.dto;

import io.oduck.api.domain.contact.entity.InquiryType;
import io.oduck.api.domain.contact.entity.ContactType;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -10,10 +10,7 @@ public class ContactReq {
@Getter
@AllArgsConstructor
public static class PostReq {
@NotBlank
@Length(min = 1, max = 50,
message = "글자 수는 1~50을 허용합니다.")
private InquiryType type;
private ContactType type;

@NotBlank
@Length(min = 1, max = 50,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class Contact extends BaseEntity {
private String content;

@Enumerated(value = EnumType.STRING)
private InquiryType type;
private ContactType type;

private boolean answered = false;

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/io/oduck/api/domain/contact/entity/ContactType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.oduck.api.domain.contact.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum ContactType {
ADD_REQUEST("기능 추가 건의"),
BUG_REPORT("버그 신고"),
ETC_REQUEST("기타 문의"),
;

String description;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.oduck.api.domain.contact.infra.event;

import io.oduck.api.domain.contact.dto.ContactReq.PostReq;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(access = AccessLevel.PRIVATE)
public class ContactEvent {
private String title;
private String content;
private String type;
private String name;

public static ContactEvent from(PostReq request, String nickname) {
return ContactEvent.builder()
.type(request.getType().getDescription())
.title(request.getTitle())
.content(request.getContent())
.name(nickname)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.oduck.api.domain.contact.infra.event;

import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class ContactEventPublisher {

private final ApplicationEventPublisher eventPublisher;

public void contact(ContactEvent contactEvent) {
eventPublisher.publishEvent(contactEvent);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.oduck.api.domain.contact.infra.notifier;

import io.oduck.api.global.notification.Notifier;
import io.oduck.api.global.notification.dto.Message;
import io.oduck.api.global.webHook.DiscordWebhook;
import io.oduck.api.global.webHook.DiscordWebhook.EmbedObject;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Primary
@RequiredArgsConstructor
@Component
@Slf4j
public class ContactNotifier implements Notifier {

@Value("${config.webhook.qna}")
private String url;

@Override
public void sendNotification(Message message) {
DiscordWebhook webhook = new DiscordWebhook(url);

EmbedObject content = message.getContent();
webhook.addEmbed(content);

try{
webhook.execute();
} catch (IOException exception) {
log.error("Discord WebHook Error");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import io.oduck.api.global.common.PageResponse;

public interface ContactService {
void inquiry(Long memberId, PostReq request);
void contact(Long memberId, PostReq request);

PageResponse<MyInquiry> getAllByMemberId(Long memberId, int page, int size);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import io.oduck.api.domain.admin.repository.AdminRepository;
import io.oduck.api.domain.contact.dto.AnswerFeedback;
import io.oduck.api.domain.contact.dto.ContactId;
import io.oduck.api.domain.contact.infra.event.ContactEvent;
import io.oduck.api.domain.contact.dto.ContactReq.AnswerReq;
import io.oduck.api.domain.contact.dto.ContactReq.AnswerUpdateReq;
import io.oduck.api.domain.contact.dto.ContactRequestHolder;
import io.oduck.api.domain.contact.dto.ContactRes.DetailRes;
import io.oduck.api.domain.contact.entity.Contact;
import io.oduck.api.domain.contact.entity.FeedbackType;
import io.oduck.api.domain.contact.infra.event.ContactEventPublisher;
import io.oduck.api.domain.contact.repository.ContactRepository;
import io.oduck.api.domain.member.entity.Member;
import io.oduck.api.domain.member.repository.MemberRepository;
Expand All @@ -31,17 +33,22 @@ public class ContactServiceImpl implements ContactService {

private final MemberRepository memberRepository;
private final ContactRepository contactRepository;
private final AdminRepository adminRepository;

private final ContactPolicy contactPolicy;
private final AdminRepository adminRepository;
private final ContactEventPublisher eventPublisher;


@Override
@Transactional
public void inquiry(Long memberId, PostReq request) {
Member member = memberRepository.findById(memberId)
public void contact(Long memberId, PostReq request) {
Member member = memberRepository.findWithProfileById(memberId)
.orElseThrow(() -> new NotFoundException("member"));

member.inquiry(ContactRequestHolder.from(request, member));
member.contact(ContactRequestHolder.from(request, member));

String nickname = member.getMemberProfile().getName();
eventPublisher.contact(ContactEvent.from(request, nickname));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public void delete() {
this.role = Role.WITHDRAWAL;
}

public void inquiry(ContactRequestHolder holder) {
public void contact(ContactRequestHolder holder) {
Contact contact = holder.getContact();

this.contacts.add(contact);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
import io.oduck.api.domain.member.entity.Member;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberRepository extends JpaRepository<Member,Long>, MemberRepositoryCustom{
Optional<Member> findByIdAndDeletedAtIsNull(Long id);

@Query("select m from Member m join fetch m.memberProfile p where m.id = :id")
Optional<Member> findWithProfileById(@Param("id") Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.oduck.api.domain.notifier.service;

import io.oduck.api.domain.contact.infra.event.ContactEvent;
import io.oduck.api.global.notification.Notifier;
import io.oduck.api.global.notification.dto.Message;
import io.oduck.api.global.webHook.DiscordWebhook.EmbedObject;
import java.awt.Color;
import java.util.Date;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@RequiredArgsConstructor
public class ContactEventHandler {
private final Notifier notifier;

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleContactEvent(ContactEvent contactEvent) {
String title = contactEvent.getTitle();
String content = contactEvent.getContent();
String category = contactEvent.getType();
String name = contactEvent.getName();

String requestTime = new Date().toString();

EmbedObject embed = new EmbedObject()
.setTitle(title)
.setDescription(String.format("[문의사항] %s 님의 문의사항", name))
.setColor(new Color(000, 100, 255))
.addField("CONTENT", content, false)
.addField("CATEGORY", category, false)
.addField("TIME", requestTime, false);

notifier.sendNotification(Message.from(embed));
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package io.oduck.api.global.advice;

import static io.oduck.api.global.utils.HttpHeaderUtils.getClientIP;

import io.oduck.api.global.common.ErrorResponse;
import io.oduck.api.global.exception.BadRequestException;
import io.oduck.api.global.exception.ConflictException;
import io.oduck.api.global.exception.CustomException;
import io.oduck.api.global.exception.ForbiddenException;
import io.oduck.api.global.exception.NotFoundException;
import io.oduck.api.global.exception.UnauthorizedException;
import io.oduck.api.global.webHook.WebHookService;
import io.oduck.api.global.notification.Notifier;
import io.oduck.api.global.notification.dto.Message;
import io.oduck.api.global.webHook.DiscordWebhook.EmbedObject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import lombok.RequiredArgsConstructor;
import java.awt.Color;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
Expand All @@ -22,14 +28,16 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.client.HttpClientErrorException.Forbidden;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

@RequiredArgsConstructor
@Slf4j
@RestControllerAdvice
public class ExceptionHandlerAdvice {
private final WebHookService webHookService;
private final Notifier notifier;

public ExceptionHandlerAdvice(@Qualifier("exceptionNotifier") Notifier notifier) {
this.notifier = notifier;
}

// 요청 바디 필드 유효성 검증 예외 처리
@ExceptionHandler
Expand Down Expand Up @@ -113,9 +121,15 @@ public ResponseEntity<?> handleCustomException(CustomException e) {
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
protected ErrorResponse handleNullPointerException(HttpServletRequest req, NullPointerException e) {
log.error("handleNullPointerException", e);

// Discord WebHook에 보낼 때 "가 있으면 json 파싱 에러가 발생함.
// NPE 메시지에서 "를 사용함. -> Discord WebHook에 보낼 때 " -> \" 로 치환
webHookService.sendMsg(new NullPointerException(e.getMessage().replace("\"", "\\\"")), req);
String message = e.getMessage().replace("\"", "\\\"");
NullPointerException npe = new NullPointerException(message);

EmbedObject content = getMessageContent(npe.getMessage(), getStackTraceInfo(npe), req);

notifier.sendNotification(Message.from(content));
return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
}

Expand All @@ -127,7 +141,44 @@ protected ErrorResponse handleNullPointerException(HttpServletRequest req, NullP
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleException(HttpServletRequest req, Exception e) {
log.error("# Uncaught exceptions, which can be fatal to the server", e);
webHookService.sendMsg(e, req);

EmbedObject content = getMessageContent(e.getMessage(), getStackTraceInfo(e), req);

notifier.sendNotification(Message.from(content));
return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
}


private EmbedObject getMessageContent(String message, String stackTraceInfo, HttpServletRequest req) {
String description = message;
String stackTrace = stackTraceInfo;
String clientIP = getClientIP(req);
String requestURL = req.getRequestURL().toString();
String requestMethod = req.getMethod();
String requestTime = new Date().toString();
String requestUserAgent = req.getHeader("User-Agent");

EmbedObject content = new EmbedObject()
.setTitle("** Error Stack **")
.setDescription(description)
.setColor(new Color(16711680))
.addField("HTTP_METHOD", requestMethod, false)
.addField("REQUEST_ENDPOINT", requestURL, false)
.addField("CLIENT_IP", clientIP, false)
.addField("ERROR_STACK", stackTrace, false)
.addField("TIME", requestTime, false)
.addField("USER_AGENT", requestUserAgent, false);
return content;
}

private String getStackTraceInfo(Exception e) {
StackTraceElement[] stackTrace = e.getStackTrace();

String stackTraceInfo = null;
if (stackTrace.length > 0) {
StackTraceElement firstElement = stackTrace[0];
stackTraceInfo = firstElement.getClassName() + "." + firstElement.getMethodName() + "(" + firstElement.getFileName() + ":" + firstElement.getLineNumber() + ")";
}
return stackTraceInfo;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.oduck.api.global.advice.infra;

import io.oduck.api.global.notification.Notifier;
import io.oduck.api.global.notification.dto.Message;
import io.oduck.api.global.webHook.DiscordWebhook;
import io.oduck.api.global.webHook.DiscordWebhook.EmbedObject;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@Qualifier("exceptionNotifier")
@RequiredArgsConstructor
@Slf4j
public class ExceptionNotifier implements Notifier {

@Value("${config.webhook.url}")
private String url;

@Override
public void sendNotification(Message message) {
DiscordWebhook webhook = new DiscordWebhook(url);

EmbedObject content = message.getContent();
webhook.addEmbed(content);

try{
webhook.execute();
} catch (IOException exception) {
log.error("Discord WebHook Error");
}
}
}
Loading