From 9ee3af9498e5482de332ec7dae2e9c21c132c798 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Sun, 11 Feb 2024 18:43:33 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat:=20=ED=98=84=EC=9E=AC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=ED=95=9C=20=EB=A9=A4=EB=B2=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=ED=95=98=EB=8A=94=20=EC=9C=A0=ED=8B=B8=EB=A6=AC?= =?UTF-8?q?=ED=8B=B0=20=EA=B5=AC=ED=98=84=20(#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 멤버 유틸리티 구현 * refactor: 예외 케이스 분리 및 불필요한 메서드 제거 --- .../gdsc/global/exception/ErrorCode.java | 7 +++- .../gdsc/global/util/MemberUtil.java | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/global/util/MemberUtil.java diff --git a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java index 4bf8434d3..582bdf4af 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -9,16 +9,19 @@ public enum ErrorCode { INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러입니다."), - // Jwt + // Auth INVALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 JWT 토큰입니다."), EXPIRED_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 JWT 토큰입니다."), + AUTH_NOT_EXIST(HttpStatus.INTERNAL_SERVER_ERROR, "시큐리티 인증 정보가 존재하지 않습니다."), + AUTH_NOT_PARSABLE(HttpStatus.INTERNAL_SERVER_ERROR, "시큐리티 인증 정보 파싱에 실패했습니다."), // Parameter INVALID_QUERY_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 쿼리 파라미터입니다."), // Member MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."), - MEMBER_DELETED(HttpStatus.CONFLICT, "탈퇴한 회원입니다."); + MEMBER_DELETED(HttpStatus.CONFLICT, "탈퇴한 회원입니다."), + ; private final HttpStatus status; private final String message; diff --git a/src/main/java/com/gdschongik/gdsc/global/util/MemberUtil.java b/src/main/java/com/gdschongik/gdsc/global/util/MemberUtil.java new file mode 100644 index 000000000..63315d461 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/util/MemberUtil.java @@ -0,0 +1,41 @@ +package com.gdschongik.gdsc.global.util; + +import com.gdschongik.gdsc.domain.member.dao.MemberRepository; +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MemberUtil { + + private final MemberRepository memberRepository; + + public Long getCurrentMemberId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + validateAuthenticationNotNull(authentication); + + try { + return Long.parseLong(authentication.getName()); + } catch (NumberFormatException e) { + throw new CustomException(ErrorCode.AUTH_NOT_PARSABLE); + } + } + + private void validateAuthenticationNotNull(Authentication authentication) { + if (authentication == null) { + throw new CustomException(ErrorCode.AUTH_NOT_EXIST); + } + } + + public Member getCurrentMember() { + return memberRepository + .findById(getCurrentMemberId()) + .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); + } +} From b6197e186d95c72be39f0228a5190cdd746b6c05 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Sun, 11 Feb 2024 19:52:48 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20requirement=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: requirement 도메인 구현 * refactor: requirement를 embeddable로 변경 --- .../gdsc/domain/member/domain/Member.java | 11 ++++- .../requirement/domain/Requirement.java | 42 +++++++++++++++++++ .../requirement/domain/RequirementStatus.java | 13 ++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/requirement/domain/Requirement.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/requirement/domain/RequirementStatus.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java index 5c41d7947..ff7bd86ab 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java @@ -3,8 +3,10 @@ import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.common.model.BaseTimeEntity; +import com.gdschongik.gdsc.domain.requirement.domain.Requirement; import com.gdschongik.gdsc.global.exception.CustomException; import jakarta.persistence.Column; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -54,6 +56,9 @@ public class Member extends BaseTimeEntity { private String univEmail; + @Embedded + private Requirement requirement; + @Builder(access = AccessLevel.PRIVATE) private Member( MemberRole role, @@ -67,7 +72,8 @@ private Member( String nickname, String oauthId, LocalDateTime lastLoginAt, - String univEmail) { + String univEmail, + Requirement requirement) { this.role = role; this.status = status; this.name = name; @@ -80,13 +86,16 @@ private Member( this.oauthId = oauthId; this.lastLoginAt = lastLoginAt; this.univEmail = univEmail; + this.requirement = requirement; } public static Member createGuestMember(String oauthId) { + Requirement requirement = Requirement.createRequirement(); return Member.builder() .oauthId(oauthId) .role(MemberRole.GUEST) .status(MemberStatus.NORMAL) + .requirement(requirement) .build(); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/requirement/domain/Requirement.java b/src/main/java/com/gdschongik/gdsc/domain/requirement/domain/Requirement.java new file mode 100644 index 000000000..865293a35 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/requirement/domain/Requirement.java @@ -0,0 +1,42 @@ +package com.gdschongik.gdsc.domain.requirement.domain; + +import static com.gdschongik.gdsc.domain.requirement.domain.RequirementStatus.*; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Requirement { + + @Enumerated(EnumType.STRING) + private RequirementStatus univStatus; + + @Enumerated(EnumType.STRING) + private RequirementStatus discordStatus; + + @Enumerated(EnumType.STRING) + private RequirementStatus paymentStatus; + + @Builder(access = AccessLevel.PRIVATE) + private Requirement( + RequirementStatus univStatus, RequirementStatus discordStatus, RequirementStatus paymentStatus) { + this.univStatus = univStatus; + this.discordStatus = discordStatus; + this.paymentStatus = paymentStatus; + } + + public static Requirement createRequirement() { + return Requirement.builder() + .univStatus(PENDING) + .discordStatus(PENDING) + .paymentStatus(PENDING) + .build(); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/requirement/domain/RequirementStatus.java b/src/main/java/com/gdschongik/gdsc/domain/requirement/domain/RequirementStatus.java new file mode 100644 index 000000000..902298e4f --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/requirement/domain/RequirementStatus.java @@ -0,0 +1,13 @@ +package com.gdschongik.gdsc.domain.requirement.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum RequirementStatus { + PENDING("PENDING"), + VERIFIED("VERIFIED"); + + private final String value; +} From f6865b9908c5d335a58c7f9dabf0a58856edccab Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Sun, 11 Feb 2024 21:53:03 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?api=20=EA=B5=AC=ED=98=84=20(#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 어드민 멤버 수정 api 추가 * feat: 어드민 멤버 수정 request dto 추가 * feat: 정규표현식 상수 클래스 추가 * feat: 어드민 멤버 수정 서비스 추가 * feat: 멤버 수정 메서드 추가 * feat: 존재하지 않는 멤버 예외 추가 * fix: 닉네임 최소 1자 이상이도록 수정 * fix: RegexConstant 수정 * style: 개행 제거 * feat: 검증 메서드 추가 * refactor: update 메서드 수정 * feat: ErrorCode 추가 * feat: 삭제된 멤버 수정 못하도록 검증 추가 * fix: 필드명 변경 * refactor: if문 제거 * refactor: NotBlank로 변경 * feat: dto 검증 예외 처리 메서드 생성 * test: 탈퇴한 회원 정보 수정 시 예외 처리 테스트 추가 * refactor: 탈퇴 여부 확인을 MemberStatus이 처리 * fix: 메서드명 수정 * remove: 중복 null-check 제거 * fix: 메서드명 수정 * refactor: 멤버 상태 검증 시 차단 여부도 확인 * refactor: db에 하이픈 없이 저장하도록 수정 * fix: 메서드명 수정 * refactor: 검증 메서드를 수정 메서드 내부로 이동 * refactor: ErrorCode의 이름을 Response 내에서 처리하도록 수정 * refactor: 수정 로직 수정 * remove: 사용하지 않는 import 제거 * style: spotless apply --- .../member/api/AdminMemberController.java | 11 ++++++ .../member/application/MemberService.java | 9 +++++ .../gdsc/domain/member/domain/Member.java | 28 ++++++++++++++- .../domain/member/domain/MemberStatus.java | 4 +++ .../dto/request/MemberUpdateRequest.java | 16 +++++++++ .../global/common/constant/RegexConstant.java | 10 ++++++ .../gdsc/global/exception/ErrorCode.java | 11 +++--- .../gdsc/global/exception/ErrorResponse.java | 4 +++ .../exception/GlobalExceptionHandler.java | 13 +++++++ .../member/application/MemberServiceTest.java | 36 +++++++++++++++++++ 10 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberUpdateRequest.java create mode 100644 src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java create mode 100644 src/test/java/com/gdschongik/gdsc/domain/member/application/MemberServiceTest.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java index dcedf66c3..7f7a77de1 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java @@ -2,7 +2,9 @@ import com.gdschongik.gdsc.domain.member.application.MemberService; import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; +import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.domain.member.dto.response.MemberFindAllResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -10,6 +12,8 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -31,4 +35,11 @@ public ResponseEntity withdrawMember(@PathVariable Long memberId) { memberService.withdrawMember(memberId); return ResponseEntity.ok().build(); } + + @PutMapping("/{memberId}") + public ResponseEntity updateMember( + @PathVariable Long memberId, @Valid @RequestBody MemberUpdateRequest request) { + memberService.updateMember(memberId, request); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java index 2933d7d2a..69d7dbd7c 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java @@ -5,8 +5,10 @@ import com.gdschongik.gdsc.domain.member.dao.MemberRepository; import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; +import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.domain.member.dto.response.MemberFindAllResponse; import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -30,4 +32,11 @@ public void withdrawMember(Long memberId) { Member member = memberRepository.findById(memberId).orElseThrow(() -> new CustomException(MEMBER_NOT_FOUND)); member.withdraw(); } + + @Transactional + public void updateMember(Long memberId, MemberUpdateRequest request) { + Member member = + memberRepository.findById(memberId).orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); + member.updateMemberInfo(request); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java index ff7bd86ab..fb285f02c 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java @@ -3,6 +3,7 @@ import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.common.model.BaseTimeEntity; +import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.domain.requirement.domain.Requirement; import com.gdschongik.gdsc.global.exception.CustomException; import jakarta.persistence.Column; @@ -106,7 +107,32 @@ public void withdraw() { this.status = MemberStatus.DELETED; } - public boolean isDeleted() { + private boolean isDeleted() { return this.status.isDeleted(); } + + private boolean isForbidden() { + return this.status.isForbidden(); + } + + public void updateMemberInfo(MemberUpdateRequest request) { + validateStatusUpdatable(); + + this.studentId = request.studentId(); + this.name = request.name(); + this.phone = request.phone(); + this.department = request.department(); + this.email = request.email(); + this.discordUsername = request.discordUsername(); + this.nickname = request.nickname(); + } + + private void validateStatusUpdatable() { + if (isDeleted()) { + throw new CustomException(MEMBER_DELETED); + } + if (isForbidden()) { + throw new CustomException(MEMBER_FORBIDDEN); + } + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberStatus.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberStatus.java index ac8cb7265..a37e655eb 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberStatus.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberStatus.java @@ -15,4 +15,8 @@ public enum MemberStatus { public boolean isDeleted() { return this.equals(DELETED); } + + public boolean isForbidden() { + return this.equals(FORBIDDEN); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberUpdateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberUpdateRequest.java new file mode 100644 index 000000000..d834c0772 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberUpdateRequest.java @@ -0,0 +1,16 @@ +package com.gdschongik.gdsc.domain.member.dto.request; + +import static com.gdschongik.gdsc.global.common.constant.RegexConstant.*; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public record MemberUpdateRequest( + @NotBlank @Pattern(regexp = STUDENT_ID, message = "학번은 " + STUDENT_ID + " 형식이어야 합니다.") String studentId, + @NotBlank String name, + @NotBlank @Pattern(regexp = PHONE, message = "전화번호는 " + PHONE + " 형식이어야 합니다.") String phone, + @NotBlank String department, + @NotBlank @Email String email, + @NotBlank String discordUsername, + @NotBlank @Pattern(regexp = NICKNAME, message = "닉네임은 " + NICKNAME + " 형식이어야 합니다.") String nickname) {} diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java new file mode 100644 index 000000000..c8a62c2f9 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java @@ -0,0 +1,10 @@ +package com.gdschongik.gdsc.global.common.constant; + +public class RegexConstant { + public static final String STUDENT_ID = "^[A-C]{1}[0-9]{6}$"; + public static final String PHONE = "^010-[0-9]{4}-[0-9]{4}$"; + public static final String PHONE_WITHOUT_HYPHEN = "^010[0-9]{8}$"; + public static final String NICKNAME = "[ㄱ-ㅣ가-힣]{1,6}$"; + + private RegexConstant() {} +} diff --git a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java index 582bdf4af..05bc663b2 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -8,6 +8,9 @@ @AllArgsConstructor public enum ErrorCode { INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러입니다."), + METHOD_ARGUMENT_NULL(HttpStatus.BAD_REQUEST, "인자는 null이 될 수 없습니다."), + METHOD_ARGUMENT_NOT_VALID(HttpStatus.BAD_REQUEST, "인자가 유효하지 않습니다."), + REGEX_VIOLATION(HttpStatus.BAD_REQUEST, "정규표현식을 위반했습니다."), // Auth INVALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 JWT 토큰입니다."), @@ -15,13 +18,13 @@ public enum ErrorCode { AUTH_NOT_EXIST(HttpStatus.INTERNAL_SERVER_ERROR, "시큐리티 인증 정보가 존재하지 않습니다."), AUTH_NOT_PARSABLE(HttpStatus.INTERNAL_SERVER_ERROR, "시큐리티 인증 정보 파싱에 실패했습니다."), - // Parameter - INVALID_QUERY_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 쿼리 파라미터입니다."), - // Member MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."), MEMBER_DELETED(HttpStatus.CONFLICT, "탈퇴한 회원입니다."), - ; + MEMBER_FORBIDDEN(HttpStatus.CONFLICT, "차단된 회원입니다."), + + // Parameter + INVALID_QUERY_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 쿼리 파라미터입니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorResponse.java b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorResponse.java index e818b1075..03acec347 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorResponse.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorResponse.java @@ -4,4 +4,8 @@ public record ErrorResponse(String errorCodeName, String errorMessage) { public static ErrorResponse of(ErrorCode errorCode) { return new ErrorResponse(errorCode.name(), errorCode.getMessage()); } + + public static ErrorResponse of(ErrorCode errorCode, String errorMessage) { + return new ErrorResponse(errorCode.name(), errorMessage); + } } diff --git a/src/main/java/com/gdschongik/gdsc/global/exception/GlobalExceptionHandler.java b/src/main/java/com/gdschongik/gdsc/global/exception/GlobalExceptionHandler.java index 42cd4a1ed..86168133e 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/GlobalExceptionHandler.java @@ -1,9 +1,13 @@ package com.gdschongik.gdsc.global.exception; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @Slf4j @@ -22,4 +26,13 @@ public ResponseEntity handleException(Exception e) { return ResponseEntity.status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus()) .body(ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR)); } + + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + log.error("METHOD_ARGUMENT_NOT_VALID : {}", e.getMessage(), e); + String errorMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + return ResponseEntity.status(status.value()) + .body(ErrorResponse.of(ErrorCode.METHOD_ARGUMENT_NOT_VALID, errorMessage)); + } } diff --git a/src/test/java/com/gdschongik/gdsc/domain/member/application/MemberServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/member/application/MemberServiceTest.java new file mode 100644 index 000000000..b138910cd --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/domain/member/application/MemberServiceTest.java @@ -0,0 +1,36 @@ +package com.gdschongik.gdsc.domain.member.application; + +import static org.assertj.core.api.Assertions.*; + +import com.gdschongik.gdsc.domain.member.dao.MemberRepository; +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.exception.ErrorCode; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class MemberServiceTest { + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MemberService memberService; + + @Test + void status가_DELETED라면_예외_발생() { + // given + Member member = Member.createGuestMember("oAuthId"); + member.withdraw(); + memberRepository.save(member); + + // when & then + MemberUpdateRequest requestBody = new MemberUpdateRequest( + "A111111", "name", "010-1234-5678", "department", "email@email.com", "discordUsername", "한글"); + assertThatThrownBy(() -> memberService.updateMember(member.getId(), requestBody)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.MEMBER_DELETED.getMessage()); + } +} From cc99a4cda5f155cfab864e97c26b312ab109798f Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Sun, 11 Feb 2024 23:53:48 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20=20=ED=97=AC=EC=8A=A4=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20API=EB=A5=BC=20=EC=9C=84=ED=95=9C=20actuator=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20(#54)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 존재하지 않는 설정파일 제거 * feat: url에 따른 인증 설정 추가 * feat: 액추에이터 설정 추가 --- build.gradle | 3 +++ .../gdsc/global/config/WebSecurityConfig.java | 12 ++++++++++++ src/main/resources/application-actuator.yml | 13 +++++++++++++ src/main/resources/application.yml | 2 +- 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/application-actuator.yml diff --git a/build.gradle b/build.gradle index 924f790ab..a99ad76e8 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,9 @@ dependencies { // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' + + // Actuator + implementation 'org.springframework.boot:spring-boot-starter-actuator' } tasks.named('test') { diff --git a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java index 18f9fb40b..e8442305d 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java @@ -82,6 +82,18 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.addFilterAfter(jwtExceptionFilter(objectMapper), LogoutFilter.class); http.addFilterAfter(jwtFilter(jwtService, cookieUtil), LogoutFilter.class); + http.authorizeHttpRequests(authorize -> authorize + .requestMatchers("/oauth2/**") + .permitAll() + .requestMatchers("/gdsc-actuator/**") + .permitAll() + .requestMatchers("/onboarding/**") + .authenticated() + .requestMatchers("/admin/**") + .hasRole("ADMIN") + .anyRequest() + .authenticated()); + return http.build(); } diff --git a/src/main/resources/application-actuator.yml b/src/main/resources/application-actuator.yml new file mode 100644 index 000000000..9290215f9 --- /dev/null +++ b/src/main/resources/application-actuator.yml @@ -0,0 +1,13 @@ +management: + endpoints: + web: + exposure: + include: health + base-path: /gdsc-actuator + jmx: + exposure: + exclude: "*" + enabled-by-default: false + endpoint: + health: + enabled: true diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 41ff71fc6..37cd16cd1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,9 +6,9 @@ spring: dev: "dev, datasource" include: - redis - - storage - security - swagger + - actuator logging: level: From 49a9575861e11a81db6ed9ab2c5ff001cd28ee78 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Mon, 12 Feb 2024 01:14:39 +0900 Subject: [PATCH 05/15] =?UTF-8?q?docs:=20admin=20member=20api=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=ED=99=94=20(#46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 어드민 controller 문서 작업 * docs: request dto 문서화 --- .../member/api/AdminMemberController.java | 6 +++++ .../dto/request/MemberQueryRequest.java | 18 ++++++++------ .../dto/request/MemberUpdateRequest.java | 24 +++++++++++++------ 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java index 7f7a77de1..a0c77173f 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java @@ -4,6 +4,8 @@ import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.domain.member.dto.response.MemberFindAllResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Admin Member", description = "어드민 회원 관리 API입니다.") @RestController @RequestMapping("/admin/members") @RequiredArgsConstructor @@ -24,18 +27,21 @@ public class AdminMemberController { private final MemberService memberService; + @Operation(summary = "전체 회원 목록 조회", description = "전체 회원 목록을 조회합니다.") @GetMapping public ResponseEntity> getMembers(MemberQueryRequest queryRequest, Pageable pageable) { Page response = memberService.findAll(queryRequest, pageable); return ResponseEntity.ok().body(response); } + @Operation(summary = "회원 탈퇴", description = "회원을 탈퇴시킵니다.") @DeleteMapping("/{memberId}") public ResponseEntity withdrawMember(@PathVariable Long memberId) { memberService.withdrawMember(memberId); return ResponseEntity.ok().build(); } + @Operation(summary = "회원 정보 수정", description = "회원 정보를 수정합니다.") @PutMapping("/{memberId}") public ResponseEntity updateMember( @PathVariable Long memberId, @Valid @RequestBody MemberUpdateRequest request) { diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java index c88c77670..03206a992 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java @@ -1,10 +1,14 @@ package com.gdschongik.gdsc.domain.member.dto.request; +import static com.gdschongik.gdsc.global.common.constant.RegexConstant.*; + +import io.swagger.v3.oas.annotations.media.Schema; + public record MemberQueryRequest( - String studentId, - String name, - String phone, - String department, - String email, - String discordUsername, - String discordNickname) {} + @Schema(description = "학번", pattern = STUDENT_ID) String studentId, + @Schema(description = "이름") String name, + @Schema(description = "전화번호", pattern = PHONE) String phone, + @Schema(description = "학과") String department, + @Schema(description = "이메일") String email, + @Schema(description = "discord username") String discordUsername, + @Schema(description = "커뮤니티 닉네임", pattern = NICKNAME) String discordNickname) {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberUpdateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberUpdateRequest.java index d834c0772..6b0031927 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberUpdateRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberUpdateRequest.java @@ -2,15 +2,25 @@ import static com.gdschongik.gdsc.global.common.constant.RegexConstant.*; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; public record MemberUpdateRequest( - @NotBlank @Pattern(regexp = STUDENT_ID, message = "학번은 " + STUDENT_ID + " 형식이어야 합니다.") String studentId, - @NotBlank String name, - @NotBlank @Pattern(regexp = PHONE, message = "전화번호는 " + PHONE + " 형식이어야 합니다.") String phone, - @NotBlank String department, - @NotBlank @Email String email, - @NotBlank String discordUsername, - @NotBlank @Pattern(regexp = NICKNAME, message = "닉네임은 " + NICKNAME + " 형식이어야 합니다.") String nickname) {} + @NotBlank + @Pattern(regexp = STUDENT_ID, message = "학번은 " + STUDENT_ID + " 형식이어야 합니다.") + @Schema(description = "학번", pattern = STUDENT_ID) + String studentId, + @NotBlank @Schema(description = "이름") String name, + @NotBlank + @Pattern(regexp = PHONE, message = "전화번호는 " + PHONE + " 형식이어야 합니다.") + @Schema(description = "전화번호", pattern = PHONE) + String phone, + @NotBlank @Schema(description = "학과") String department, + @NotBlank @Email @Schema(description = "이메일") String email, + @NotBlank @Schema(description = "discord username") String discordUsername, + @NotBlank + @Pattern(regexp = NICKNAME, message = "닉네임은 " + NICKNAME + " 형식이어야 합니다.") + @Schema(description = "커뮤니티 닉네임", pattern = NICKNAME) + String nickname) {} From 8796e8f40ffe2244af310435cf45e99e08620d6f Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Mon, 12 Feb 2024 01:17:56 +0900 Subject: [PATCH 06/15] =?UTF-8?q?chore:=20=EB=8F=84=EC=BB=A4=ED=97=88?= =?UTF-8?q?=EB=B8=8C=EC=97=90=20=EC=98=AC=EB=9D=BC=EA=B0=84=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EC=9E=AC=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C?= =?UTF-8?q?=EC=9A=B0=20=EC=B6=94=EA=B0=80=20(#56)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: deploy 전용 워크플로우 작성 * fix: 오타 수정 --- .github/workflows/develop_deploy.yml | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/develop_deploy.yml diff --git a/.github/workflows/develop_deploy.yml b/.github/workflows/develop_deploy.yml new file mode 100644 index 000000000..395e9ecc0 --- /dev/null +++ b/.github/workflows/develop_deploy.yml @@ -0,0 +1,29 @@ +name: Deploy to Develop + +on: + workflow_dispatch: + inputs: + commit_hash: + description: 'commit_hash' + required: true + +jobs: + deploy: + runs-on: ubuntu-latest + environment: develop + steps: + - name: Deploy to EC2 Server + uses: appleboy/ssh-action@master + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_IMAGE_TAG: ${{ github.event.inputs.commit_hash }} + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + envs: DOCKERHUB_USERNAME,DOCKERHUB_IMAGE_TAG # docker-compose.yml 에서 사용할 환경 변수 + script: | + echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin + docker pull ${{ env.DOCKERHUB_USERNAME }}/gdsc-server:${{ env.DOCKERHUB_IMAGE_TAG }} + docker compose -f /home/ubuntu/docker-compose.yml up -d + docker image prune -a -f From e27bc8298daa30e6a9a8a3dc56b3acb3a8d9a0b4 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Mon, 12 Feb 2024 01:21:01 +0900 Subject: [PATCH 07/15] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EB=8C=80=EA=B8=B0=EC=A4=91=EC=9D=B8=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84=20=20(#44)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 파일 구조 수정 * chore: 파일 구조 수정 * feat: 대기중인 멤버 조회 기능 추가 * feat: 대기중인 멤버 조회 기능 추가 * docs: 어드민 controller 문서 작업 * style: spotless apply * style: spotless apply --- .../member/api/AdminMemberController.java | 8 +++++ .../member/application/MemberService.java | 7 +++++ .../domain/member/dao/MemberRepository.java | 8 ++++- .../gdsc/domain/member/domain/Member.java | 1 - .../domain/Requirement.java | 4 +-- .../domain/RequirementStatus.java | 2 +- .../MemberPendingFindAllResponse.java | 29 +++++++++++++++++++ 7 files changed, 54 insertions(+), 5 deletions(-) rename src/main/java/com/gdschongik/gdsc/domain/{requirement => member}/domain/Requirement.java (89%) rename src/main/java/com/gdschongik/gdsc/domain/{requirement => member}/domain/RequirementStatus.java (78%) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberPendingFindAllResponse.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java index a0c77173f..a0d42b09e 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java @@ -4,6 +4,7 @@ import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.domain.member.dto.response.MemberFindAllResponse; +import com.gdschongik.gdsc.domain.member.dto.response.MemberPendingFindAllResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -41,6 +42,13 @@ public ResponseEntity withdrawMember(@PathVariable Long memberId) { return ResponseEntity.ok().build(); } + @Operation(summary = "대기중인 회원 목록 조회", description = "대기중인 회원 목록을 조회합니다.") + @GetMapping("/pending") + public ResponseEntity> getPendingMembers(Pageable pageable) { + Page response = memberService.findAllPendingMembers(pageable); + return ResponseEntity.ok().body(response); + } + @Operation(summary = "회원 정보 수정", description = "회원 정보를 수정합니다.") @PutMapping("/{memberId}") public ResponseEntity updateMember( diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java index 69d7dbd7c..7fe0b5e93 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java @@ -4,9 +4,11 @@ import com.gdschongik.gdsc.domain.member.dao.MemberRepository; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.domain.MemberRole; import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.domain.member.dto.response.MemberFindAllResponse; +import com.gdschongik.gdsc.domain.member.dto.response.MemberPendingFindAllResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; @@ -39,4 +41,9 @@ public void updateMember(Long memberId, MemberUpdateRequest request) { memberRepository.findById(memberId).orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); member.updateMemberInfo(request); } + + public Page findAllPendingMembers(Pageable pageable) { + Page members = memberRepository.findAllByRole(MemberRole.GUEST, pageable); + return members.map(MemberPendingFindAllResponse::of); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberRepository.java b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberRepository.java index 5fc3e0c88..c3196f3a6 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberRepository.java @@ -1,6 +1,12 @@ package com.gdschongik.gdsc.domain.member.dao; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -public interface MemberRepository extends JpaRepository, MemberCustomRepository {} +public interface MemberRepository extends JpaRepository, MemberCustomRepository { + + Page findAllByRole(MemberRole role, Pageable pageable); +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java index fb285f02c..d9aa3a30a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java @@ -4,7 +4,6 @@ import com.gdschongik.gdsc.domain.common.model.BaseTimeEntity; import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; -import com.gdschongik.gdsc.domain.requirement.domain.Requirement; import com.gdschongik.gdsc.global.exception.CustomException; import jakarta.persistence.Column; import jakarta.persistence.Embedded; diff --git a/src/main/java/com/gdschongik/gdsc/domain/requirement/domain/Requirement.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Requirement.java similarity index 89% rename from src/main/java/com/gdschongik/gdsc/domain/requirement/domain/Requirement.java rename to src/main/java/com/gdschongik/gdsc/domain/member/domain/Requirement.java index 865293a35..b6edbac89 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/requirement/domain/Requirement.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Requirement.java @@ -1,6 +1,6 @@ -package com.gdschongik.gdsc.domain.requirement.domain; +package com.gdschongik.gdsc.domain.member.domain; -import static com.gdschongik.gdsc.domain.requirement.domain.RequirementStatus.*; +import static com.gdschongik.gdsc.domain.member.domain.RequirementStatus.*; import jakarta.persistence.Embeddable; import jakarta.persistence.EnumType; diff --git a/src/main/java/com/gdschongik/gdsc/domain/requirement/domain/RequirementStatus.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/RequirementStatus.java similarity index 78% rename from src/main/java/com/gdschongik/gdsc/domain/requirement/domain/RequirementStatus.java rename to src/main/java/com/gdschongik/gdsc/domain/member/domain/RequirementStatus.java index 902298e4f..9a174d4d4 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/requirement/domain/RequirementStatus.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/RequirementStatus.java @@ -1,4 +1,4 @@ -package com.gdschongik.gdsc.domain.requirement.domain; +package com.gdschongik.gdsc.domain.member.domain; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberPendingFindAllResponse.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberPendingFindAllResponse.java new file mode 100644 index 000000000..a86b61163 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberPendingFindAllResponse.java @@ -0,0 +1,29 @@ +package com.gdschongik.gdsc.domain.member.dto.response; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.domain.Requirement; + +public record MemberPendingFindAllResponse( + Long memberId, + String studentId, + String name, + String phone, + String department, + String email, + String discordUsername, + String nickname, + Requirement requirement) { + + public static MemberPendingFindAllResponse of(Member member) { + return new MemberPendingFindAllResponse( + member.getId(), + member.getStudentId(), + member.getName(), + member.getPhone(), + member.getDepartment(), + member.getEmail(), + member.getDiscordUsername(), + member.getNickname(), + member.getRequirement()); + } +} From dea68da8d375fb21bde0ce80d366c574e891e317 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:39:48 +0900 Subject: [PATCH 08/15] =?UTF-8?q?fix:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20Basi?= =?UTF-8?q?c=20Auth=EA=B0=80=20=EC=9E=91=EB=8F=99=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 스웨거 필터의 oauth 디폴트 구성 무효화 * refactor: 메서드 이름 수정 * feat: 미인증 요청 시 리다이렉트 대신 항상 401 리턴하도록 변경 --- .../gdschongik/gdsc/global/config/WebSecurityConfig.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java index e8442305d..e6645fbc7 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java @@ -60,10 +60,11 @@ private static void defaultFilterChain(HttpSecurity http) throws Exception { @Bean @Order(1) @ConditionalOnProperty(name = "spring.profiles.active", havingValue = "dev") - public SecurityFilterChain swaggerSecurityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain swaggerFilterChain(HttpSecurity http) throws Exception { defaultFilterChain(http); http.securityMatcher(getSwaggerUrls()) + .oauth2Login(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()) .httpBasic(withDefaults()); @@ -79,6 +80,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .successHandler(customSuccessHandler(jwtService, cookieUtil)) .failureHandler((request, response, exception) -> response.setStatus(401))); + http.exceptionHandling(exception -> + exception.authenticationEntryPoint((request, response, authException) -> response.setStatus(401))); + http.addFilterAfter(jwtExceptionFilter(objectMapper), LogoutFilter.class); http.addFilterAfter(jwtFilter(jwtService, cookieUtil), LogoutFilter.class); From 1dbdbef2e09db837804d5df280ff65b772b9ca8d Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Mon, 12 Feb 2024 18:16:38 +0900 Subject: [PATCH 09/15] =?UTF-8?q?fix:=20=EC=A0=84=EC=B2=B4=20=EB=A9=A4?= =?UTF-8?q?=EB=B2=84=20=EC=A1=B0=ED=9A=8C=20request=20dto=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: 변수명 통일 --- .../gdsc/domain/member/dao/MemberCustomRepositoryImpl.java | 6 +++--- .../gdsc/domain/member/dto/request/MemberQueryRequest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java index e2e0f1844..ca5cfa2a2 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java @@ -62,7 +62,7 @@ private BooleanBuilder queryOption(MemberQueryRequest queryRequest) { .and(eqDepartment(queryRequest.department())) .and(eqEmail(queryRequest.email())) .and(eqDiscordUsername(queryRequest.discordUsername())) - .and(eqDiscordNickname(queryRequest.discordNickname())); + .and(eqNickname(queryRequest.nickname())); } private BooleanExpression eqStudentId(String studentId) { @@ -89,7 +89,7 @@ private BooleanExpression eqDiscordUsername(String discordUsername) { return discordUsername != null ? member.discordUsername.containsIgnoreCase(discordUsername) : null; } - private BooleanExpression eqDiscordNickname(String discordNickname) { - return discordNickname != null ? member.nickname.containsIgnoreCase(discordNickname) : null; + private BooleanExpression eqNickname(String nickname) { + return nickname != null ? member.nickname.containsIgnoreCase(nickname) : null; } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java index 03206a992..e5a439adb 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java @@ -11,4 +11,4 @@ public record MemberQueryRequest( @Schema(description = "학과") String department, @Schema(description = "이메일") String email, @Schema(description = "discord username") String discordUsername, - @Schema(description = "커뮤니티 닉네임", pattern = NICKNAME) String discordNickname) {} + @Schema(description = "커뮤니티 닉네임", pattern = NICKNAME) String nickname) {} From 39ebc9ac4bdabb7752b850a1f88aca374093416c Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Mon, 12 Feb 2024 19:04:54 +0900 Subject: [PATCH 10/15] =?UTF-8?q?refactor:=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9D=B4=20dto=EC=97=90=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/application/MemberService.java | 9 ++++++- .../gdsc/domain/member/domain/Member.java | 24 ++++++++++++------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java index 7fe0b5e93..5d257ce44 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java @@ -39,7 +39,14 @@ public void withdrawMember(Long memberId) { public void updateMember(Long memberId, MemberUpdateRequest request) { Member member = memberRepository.findById(memberId).orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); - member.updateMemberInfo(request); + member.updateMemberInfo( + request.studentId(), + request.name(), + request.phone(), + request.department(), + request.email(), + request.discordUsername(), + request.nickname()); } public Page findAllPendingMembers(Pageable pageable) { diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java index d9aa3a30a..4243f5adf 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java @@ -3,7 +3,6 @@ import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.common.model.BaseTimeEntity; -import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.global.exception.CustomException; import jakarta.persistence.Column; import jakarta.persistence.Embedded; @@ -114,16 +113,23 @@ private boolean isForbidden() { return this.status.isForbidden(); } - public void updateMemberInfo(MemberUpdateRequest request) { + public void updateMemberInfo( + String studentId, + String name, + String phone, + String department, + String email, + String discordUsername, + String nickname) { validateStatusUpdatable(); - this.studentId = request.studentId(); - this.name = request.name(); - this.phone = request.phone(); - this.department = request.department(); - this.email = request.email(); - this.discordUsername = request.discordUsername(); - this.nickname = request.nickname(); + this.studentId = studentId; + this.name = name; + this.phone = phone; + this.department = department; + this.email = email; + this.discordUsername = discordUsername; + this.nickname = nickname; } private void validateStatusUpdatable() { From a13d3f42463dcdd9b50d5b272e3df60cbcbf4017 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Mon, 12 Feb 2024 19:33:40 +0900 Subject: [PATCH 11/15] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EB=9E=9C=EB=94=A9=ED=95=A0=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B2=B0=EC=A0=95=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=A0=20=ED=97=A4=EB=8D=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#64)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 랜딩 상태 추가 * feat: oauth2 인증주체에 랜딩상태 필드 추가 * refactor: 미사용 메서드 제거 * feat: 소셜 로그인 응답 헤더에 랜딩상태 추가 --- .../common/constant/SecurityConstant.java | 2 +- .../global/security/CustomOAuth2User.java | 6 ++--- .../global/security/CustomSuccessHandler.java | 4 +-- .../gdsc/global/security/LandingStatus.java | 27 +++++++++++++++++++ 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java index 5ff2d1820..52da9c9be 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java @@ -2,7 +2,7 @@ public class SecurityConstant { - public static final String REGISTRATION_REQUIRED_HEADER = "Registration-Required"; + public static final String LANDING_STATUS_HEADER = "Landing-Status"; public static final String TOKEN_ROLE_NAME = "role"; public static final String GITHUB_NAME_ATTR_KEY = "id"; public static final String ACCESS_TOKEN_HEADER_PREFIX = "Bearer "; diff --git a/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java b/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java index e0b689684..69e2c0781 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java @@ -13,14 +13,12 @@ public class CustomOAuth2User extends DefaultOAuth2User { private final Long memberId; private final MemberRole memberRole; + private final LandingStatus landingStatus; public CustomOAuth2User(OAuth2User oAuth2User, Member member) { super(oAuth2User.getAuthorities(), oAuth2User.getAttributes(), GITHUB_NAME_ATTR_KEY); this.memberId = member.getId(); this.memberRole = member.getRole(); - } - - public boolean isGuest() { - return memberRole == MemberRole.GUEST; + this.landingStatus = LandingStatus.of(member); } } diff --git a/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java b/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java index 9a4f4e7b6..4533e0e1e 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java @@ -28,8 +28,8 @@ public void onAuthenticationSuccess( CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); - // 게스트 유저이면 회원가입 필요하므로 헤더 설정 - response.setHeader(REGISTRATION_REQUIRED_HEADER, oAuth2User.isGuest() ? "true" : "false"); + // 랜딩 페이지 결정에 필요한 정보를 헤더에 추가 + response.setHeader(LANDING_STATUS_HEADER, oAuth2User.getLandingStatus().name()); // 토큰 생성 후 쿠키에 저장 AccessTokenDto accessTokenDto = diff --git a/src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java b/src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java new file mode 100644 index 000000000..bf91a388b --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java @@ -0,0 +1,27 @@ +package com.gdschongik.gdsc.global.security; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.domain.RequirementStatus; + +public enum LandingStatus { + TO_STUDENT_AUTHENTICATION, // 재학생 인증 페이지로 랜딩 + TO_REGISTRATION, // 가입신청 페이지로 랜딩 + TO_DASHBOARD, // 대시보드로 랜딩 + ; + + public static LandingStatus of(Member member) { + // 아직 재학생 인증을 하지 않았다면 재학생 인증 페이지로 랜딩 + if (member.getRequirement().getUnivStatus() == RequirementStatus.PENDING) { + return TO_STUDENT_AUTHENTICATION; + } + + // 재학생 인증은 했지만 가입신청을 하지 않았다면 가입신청 페이지로 랜딩 + // 가입신청 여부는 학번 존재여부로 판단 + if (member.getStudentId() == null) { + return TO_REGISTRATION; + } + + // 재학생 인증과 가입신청을 모두 완료했다면 대시보드로 랜딩 + return TO_DASHBOARD; + } +} From 03751fa3cf9a2951be636da6c82b88153289bea5 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Mon, 12 Feb 2024 20:19:50 +0900 Subject: [PATCH 12/15] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=EA=B0=80=EC=9E=85=EC=8B=A0=EC=B2=AD=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=ED=95=98=EA=B8=B0=20api=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: requirement 불충족 멤버 승인요청 시 예외 발생 * remove: 테스트 제거 * feat: 멤버 승인 요청 dto 추가 * feat: 멤버 승인 요청 api 추가 * feat: 멤버 승인 요청 service 추가 * feat: 멤버 승인 요청 repository 추가 * feat: 멤버 승인 메서드 추가 * refactor: requirementVerified와 eqId를 분리 --- .../member/api/AdminMemberController.java | 9 ++++++ .../member/application/MemberService.java | 20 +++++++++++++ .../member/dao/MemberCustomRepository.java | 2 ++ .../dao/MemberCustomRepositoryImpl.java | 29 +++++++++++++++++++ .../gdsc/domain/member/domain/Member.java | 4 +++ .../dto/request/MemberGrantRequest.java | 6 ++++ .../dto/response/MemberGrantResponse.java | 13 +++++++++ 7 files changed, 83 insertions(+) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberGrantRequest.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberGrantResponse.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java index a0d42b09e..809f5edee 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java @@ -1,9 +1,11 @@ package com.gdschongik.gdsc.domain.member.api; import com.gdschongik.gdsc.domain.member.application.MemberService; +import com.gdschongik.gdsc.domain.member.dto.request.MemberGrantRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.domain.member.dto.response.MemberFindAllResponse; +import com.gdschongik.gdsc.domain.member.dto.response.MemberGrantResponse; import com.gdschongik.gdsc.domain.member.dto.response.MemberPendingFindAllResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -56,4 +58,11 @@ public ResponseEntity updateMember( memberService.updateMember(memberId, request); return ResponseEntity.ok().build(); } + + @Operation(summary = "회원 승인", description = "회원의 가입을 승인합니다.") + @PutMapping("/grant") + public ResponseEntity grantMember(@Valid @RequestBody MemberGrantRequest request) { + MemberGrantResponse response = memberService.grantMember(request); + return ResponseEntity.ok().body(response); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java index 5d257ce44..7eb813bc4 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java @@ -5,12 +5,16 @@ import com.gdschongik.gdsc.domain.member.dao.MemberRepository; import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.domain.member.dto.request.MemberGrantRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; import com.gdschongik.gdsc.domain.member.dto.response.MemberFindAllResponse; +import com.gdschongik.gdsc.domain.member.dto.response.MemberGrantResponse; import com.gdschongik.gdsc.domain.member.dto.response.MemberPendingFindAllResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.exception.ErrorCode; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -53,4 +57,20 @@ public Page findAllPendingMembers(Pageable pageabl Page members = memberRepository.findAllByRole(MemberRole.GUEST, pageable); return members.map(MemberPendingFindAllResponse::of); } + + @Transactional + public MemberGrantResponse grantMember(MemberGrantRequest request) { + List verifiedMembers = getVerifiedMembers(request); + verifiedMembers.forEach(Member::grant); + return MemberGrantResponse.of(verifiedMembers); + } + + private List getVerifiedMembers(MemberGrantRequest request) { + List memberIdList = request.memberIdList(); + return memberIdList.stream() + .map(memberRepository::findVerifiedById) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepository.java b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepository.java index 85a6a9436..6bab58d45 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepository.java @@ -10,4 +10,6 @@ public interface MemberCustomRepository { Page findAll(MemberQueryRequest queryRequest, Pageable pageable); Optional findNormalByOauthId(String oauthId); + + Optional findVerifiedById(Long id); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java index ca5cfa2a2..c9a65d259 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java @@ -4,6 +4,7 @@ import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.member.domain.MemberStatus; +import com.gdschongik.gdsc.domain.member.domain.RequirementStatus; import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.dsl.BooleanExpression; @@ -44,6 +45,34 @@ public Optional findNormalByOauthId(String oauthId) { .fetchOne()); } + @Override + public Optional findVerifiedById(Long id) { + return Optional.ofNullable(queryFactory + .selectFrom(member) + .where(eqId(id), requirementVerified()) + .fetchOne()); + } + + private BooleanBuilder requirementVerified() { + return new BooleanBuilder().and(discordVerified()).and(univVerified()).and(paymentVerified()); + } + + private BooleanExpression discordVerified() { + return member.requirement.discordStatus.eq(RequirementStatus.VERIFIED); + } + + private BooleanExpression univVerified() { + return member.requirement.univStatus.eq(RequirementStatus.VERIFIED); + } + + private BooleanExpression paymentVerified() { + return member.requirement.paymentStatus.eq(RequirementStatus.VERIFIED); + } + + private BooleanExpression eqId(Long id) { + return member.id.eq(id); + } + private BooleanExpression eqOauthId(String oauthId) { return member.oauthId.eq(oauthId); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java index 4243f5adf..21a309caf 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java @@ -140,4 +140,8 @@ private void validateStatusUpdatable() { throw new CustomException(MEMBER_FORBIDDEN); } } + + public void grant() { + this.role = MemberRole.USER; + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberGrantRequest.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberGrantRequest.java new file mode 100644 index 000000000..8c10ac820 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberGrantRequest.java @@ -0,0 +1,6 @@ +package com.gdschongik.gdsc.domain.member.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record MemberGrantRequest(@Schema(description = "승인할 멤버 ID 리스트") List memberIdList) {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberGrantResponse.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberGrantResponse.java new file mode 100644 index 000000000..929428260 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberGrantResponse.java @@ -0,0 +1,13 @@ +package com.gdschongik.gdsc.domain.member.dto.response; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record MemberGrantResponse(@Schema(description = "승인에 성공한 멤버 ID 리스트") List grantedMembers) { + public static MemberGrantResponse of(List grantedMembers) { + List grantedMemberIdList = + grantedMembers.stream().map(Member::getId).toList(); + return new MemberGrantResponse(grantedMemberIdList); + } +} From fb4659d713909518f9567fae3a4066a87ff03477 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Mon, 12 Feb 2024 23:12:19 +0900 Subject: [PATCH 13/15] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=EA=B0=80=EC=9E=85=20=EC=8B=A0=EC=B2=AD=20?= =?UTF-8?q?(#66)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * rename: MemberService를 AdminMemberService로 변경 * feat: 회원 가입 신청 구현 * rename: signupMember로 변경 * refactor: 재학생 인증 상태 검증 추가 --- .../member/api/AdminMemberController.java | 14 ++++----- .../api/OnboardingMemberController.java | 29 +++++++++++++++++++ ...erService.java => AdminMemberService.java} | 2 +- .../application/OnboardingMemberService.java | 29 +++++++++++++++++++ .../gdsc/domain/member/domain/Member.java | 26 +++++++++++++++++ .../domain/member/domain/Requirement.java | 4 +++ .../dto/request/MemberSignupRequest.java | 26 +++++++++++++++++ .../gdsc/global/exception/ErrorCode.java | 5 +++- ...eTest.java => AdminMemberServiceTest.java} | 6 ++-- 9 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/api/OnboardingMemberController.java rename src/main/java/com/gdschongik/gdsc/domain/member/application/{MemberService.java => AdminMemberService.java} (98%) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberSignupRequest.java rename src/test/java/com/gdschongik/gdsc/domain/member/application/{MemberServiceTest.java => AdminMemberServiceTest.java} (87%) diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java index 809f5edee..5a7d6c5d0 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java @@ -1,6 +1,6 @@ package com.gdschongik.gdsc.domain.member.api; -import com.gdschongik.gdsc.domain.member.application.MemberService; +import com.gdschongik.gdsc.domain.member.application.AdminMemberService; import com.gdschongik.gdsc.domain.member.dto.request.MemberGrantRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; @@ -28,26 +28,26 @@ @RequiredArgsConstructor public class AdminMemberController { - private final MemberService memberService; + private final AdminMemberService adminMemberService; @Operation(summary = "전체 회원 목록 조회", description = "전체 회원 목록을 조회합니다.") @GetMapping public ResponseEntity> getMembers(MemberQueryRequest queryRequest, Pageable pageable) { - Page response = memberService.findAll(queryRequest, pageable); + Page response = adminMemberService.findAll(queryRequest, pageable); return ResponseEntity.ok().body(response); } @Operation(summary = "회원 탈퇴", description = "회원을 탈퇴시킵니다.") @DeleteMapping("/{memberId}") public ResponseEntity withdrawMember(@PathVariable Long memberId) { - memberService.withdrawMember(memberId); + adminMemberService.withdrawMember(memberId); return ResponseEntity.ok().build(); } @Operation(summary = "대기중인 회원 목록 조회", description = "대기중인 회원 목록을 조회합니다.") @GetMapping("/pending") public ResponseEntity> getPendingMembers(Pageable pageable) { - Page response = memberService.findAllPendingMembers(pageable); + Page response = adminMemberService.findAllPendingMembers(pageable); return ResponseEntity.ok().body(response); } @@ -55,14 +55,14 @@ public ResponseEntity> getPendingMembers(Page @PutMapping("/{memberId}") public ResponseEntity updateMember( @PathVariable Long memberId, @Valid @RequestBody MemberUpdateRequest request) { - memberService.updateMember(memberId, request); + adminMemberService.updateMember(memberId, request); return ResponseEntity.ok().build(); } @Operation(summary = "회원 승인", description = "회원의 가입을 승인합니다.") @PutMapping("/grant") public ResponseEntity grantMember(@Valid @RequestBody MemberGrantRequest request) { - MemberGrantResponse response = memberService.grantMember(request); + MemberGrantResponse response = adminMemberService.grantMember(request); return ResponseEntity.ok().body(response); } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/OnboardingMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/OnboardingMemberController.java new file mode 100644 index 000000000..98288479c --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/OnboardingMemberController.java @@ -0,0 +1,29 @@ +package com.gdschongik.gdsc.domain.member.api; + +import com.gdschongik.gdsc.domain.member.application.OnboardingMemberService; +import com.gdschongik.gdsc.domain.member.dto.request.MemberSignupRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Onboarding Member", description = "회원 온보딩 API입니다.") +@RestController +@RequestMapping("/onboarding/members") +@RequiredArgsConstructor +public class OnboardingMemberController { + + private final OnboardingMemberService onboardingMemberService; + + @Operation(summary = "회원 가입 신청", description = "회원 가입을 신청합니다.") + @PostMapping + public ResponseEntity signupMember(@Valid @RequestBody MemberSignupRequest request) { + onboardingMemberService.signupMember(request); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java similarity index 98% rename from src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java rename to src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java index 7eb813bc4..c20b9544a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/MemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java @@ -24,7 +24,7 @@ @Service @RequiredArgsConstructor @Transactional(readOnly = true) -public class MemberService { +public class AdminMemberService { private final MemberRepository memberRepository; diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java new file mode 100644 index 000000000..279b0bcbc --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java @@ -0,0 +1,29 @@ +package com.gdschongik.gdsc.domain.member.application; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.dto.request.MemberSignupRequest; +import com.gdschongik.gdsc.global.util.MemberUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class OnboardingMemberService { + + private final MemberUtil memberUtil; + + @Transactional + public void signupMember(MemberSignupRequest request) { + Member currentMember = memberUtil.getCurrentMember(); + currentMember.signup( + request.studentId(), + request.name(), + request.phone(), + request.department(), + request.email(), + request.discordUsername(), + request.nickname()); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java index 21a309caf..3e62a495e 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java @@ -98,6 +98,26 @@ public static Member createGuestMember(String oauthId) { .build(); } + public void signup( + String studentId, + String name, + String phone, + String department, + String email, + String discordUsername, + String nickname) { + validateStatusUpdatable(); + validateUnivStatus(); + + this.studentId = studentId; + this.name = name; + this.phone = phone; + this.department = department; + this.email = email; + this.discordUsername = discordUsername; + this.nickname = nickname; + } + public void withdraw() { if (isDeleted()) { throw new CustomException(MEMBER_DELETED); @@ -141,6 +161,12 @@ private void validateStatusUpdatable() { } } + private void validateUnivStatus() { + if (this.requirement.isUnivPending()) { + throw new CustomException(UNIV_NOT_VERIFIED); + } + } + public void grant() { this.role = MemberRole.USER; } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Requirement.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Requirement.java index b6edbac89..f4f29c21f 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Requirement.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Requirement.java @@ -39,4 +39,8 @@ public static Requirement createRequirement() { .paymentStatus(PENDING) .build(); } + + public boolean isUnivPending() { + return this.univStatus == PENDING; + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberSignupRequest.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberSignupRequest.java new file mode 100644 index 000000000..86ade754a --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberSignupRequest.java @@ -0,0 +1,26 @@ +package com.gdschongik.gdsc.domain.member.dto.request; + +import static com.gdschongik.gdsc.global.common.constant.RegexConstant.*; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public record MemberSignupRequest( + @NotBlank + @Pattern(regexp = STUDENT_ID, message = "학번은 " + STUDENT_ID + " 형식이어야 합니다.") + @Schema(description = "학번", pattern = STUDENT_ID) + String studentId, + @NotBlank @Schema(description = "이름") String name, + @NotBlank + @Pattern(regexp = PHONE, message = "전화번호는 " + PHONE + " 형식이어야 합니다.") + @Schema(description = "전화번호", pattern = PHONE) + String phone, + @NotBlank @Schema(description = "학과") String department, + @NotBlank @Email @Schema(description = "이메일") String email, + @NotBlank @Schema(description = "discord username") String discordUsername, + @NotBlank + @Pattern(regexp = NICKNAME, message = "닉네임은 " + NICKNAME + " 형식이어야 합니다.") + @Schema(description = "커뮤니티 닉네임", pattern = NICKNAME) + String nickname) {} diff --git a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java index 05bc663b2..0e39ed850 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -24,7 +24,10 @@ public enum ErrorCode { MEMBER_FORBIDDEN(HttpStatus.CONFLICT, "차단된 회원입니다."), // Parameter - INVALID_QUERY_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 쿼리 파라미터입니다."); + INVALID_QUERY_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 쿼리 파라미터입니다."), + + // Requirement + UNIV_NOT_VERIFIED(HttpStatus.CONFLICT, "재학생 인증이 되지 않았습니다."); private final HttpStatus status; private final String message; diff --git a/src/test/java/com/gdschongik/gdsc/domain/member/application/MemberServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/member/application/AdminMemberServiceTest.java similarity index 87% rename from src/test/java/com/gdschongik/gdsc/domain/member/application/MemberServiceTest.java rename to src/test/java/com/gdschongik/gdsc/domain/member/application/AdminMemberServiceTest.java index b138910cd..00ecd1aa8 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/member/application/MemberServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/member/application/AdminMemberServiceTest.java @@ -12,12 +12,12 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class MemberServiceTest { +class AdminMemberServiceTest { @Autowired private MemberRepository memberRepository; @Autowired - private MemberService memberService; + private AdminMemberService adminMemberService; @Test void status가_DELETED라면_예외_발생() { @@ -29,7 +29,7 @@ class MemberServiceTest { // when & then MemberUpdateRequest requestBody = new MemberUpdateRequest( "A111111", "name", "010-1234-5678", "department", "email@email.com", "discordUsername", "한글"); - assertThatThrownBy(() -> memberService.updateMember(member.getId(), requestBody)) + assertThatThrownBy(() -> adminMemberService.updateMember(member.getId(), requestBody)) .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.MEMBER_DELETED.getMessage()); } From 8ab82b8a62ec900bf3d9b1d8a570628f89bb1ff3 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Tue, 13 Feb 2024 01:06:16 +0900 Subject: [PATCH 14/15] =?UTF-8?q?refactor:=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=8B=A0=EC=B2=AD=20request=20dto=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 멤버 가입 신청 request dto 수정 * style: spotless apply --- .../member/application/OnboardingMemberService.java | 8 +------- .../gdschongik/gdsc/domain/member/domain/Member.java | 11 +---------- .../member/dto/request/MemberSignupRequest.java | 7 +------ 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java index 279b0bcbc..70cf07efc 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java @@ -18,12 +18,6 @@ public class OnboardingMemberService { public void signupMember(MemberSignupRequest request) { Member currentMember = memberUtil.getCurrentMember(); currentMember.signup( - request.studentId(), - request.name(), - request.phone(), - request.department(), - request.email(), - request.discordUsername(), - request.nickname()); + request.studentId(), request.name(), request.phone(), request.department(), request.email()); } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java index 3e62a495e..eff242fde 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java @@ -98,14 +98,7 @@ public static Member createGuestMember(String oauthId) { .build(); } - public void signup( - String studentId, - String name, - String phone, - String department, - String email, - String discordUsername, - String nickname) { + public void signup(String studentId, String name, String phone, String department, String email) { validateStatusUpdatable(); validateUnivStatus(); @@ -114,8 +107,6 @@ public void signup( this.phone = phone; this.department = department; this.email = email; - this.discordUsername = discordUsername; - this.nickname = nickname; } public void withdraw() { diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberSignupRequest.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberSignupRequest.java index 86ade754a..11404907d 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberSignupRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberSignupRequest.java @@ -18,9 +18,4 @@ public record MemberSignupRequest( @Schema(description = "전화번호", pattern = PHONE) String phone, @NotBlank @Schema(description = "학과") String department, - @NotBlank @Email @Schema(description = "이메일") String email, - @NotBlank @Schema(description = "discord username") String discordUsername, - @NotBlank - @Pattern(regexp = NICKNAME, message = "닉네임은 " + NICKNAME + " 형식이어야 합니다.") - @Schema(description = "커뮤니티 닉네임", pattern = NICKNAME) - String nickname) {} + @NotBlank @Email @Schema(description = "이메일") String email) {} From 09a2837d18cc06b5298112b528cec3aca46cd6eb Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Tue, 13 Feb 2024 02:02:38 +0900 Subject: [PATCH 15/15] =?UTF-8?q?chore:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=97=B0=EB=8F=99=20=EC=84=B8=ED=8C=85=20(#69)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 디스코드 구성 추가 * chore: discord yml 설정 추가 * chore: JDA 설정 변경 * feat: 디스코드 리스너를 컴포넌트로 등록하도록 설정 * feat: 테스트 핑퐁 리스너 추가 * fix: 테스트 시 jda 로드하지 않도록 수정 * fix: JDA 의존성을 설정 클래스에서 직접 주입하도록 수정 * test: 테스트 프로파일 활성화 * chore: 디스코드가 활성화된 상태에서만 빈을 초기화하도록 설정 * fix: JDA가 초기화된 경우에만 빈 후처리기에 주입하도록 수정 * chore: 디스코드 yml을 항상 로드하도록 수정 * test: 테스트 시에는 jda 비활성화 --- build.gradle | 3 ++ .../discord/listener/PingpongListener.java | 30 +++++++++++++++ .../gdsc/global/config/DiscordConfig.java | 37 +++++++++++++++++++ .../gdsc/global/config/PropertyConfig.java | 3 +- .../gdsc/global/discord/Listener.java | 12 ++++++ .../discord/ListenerBeanPostProcessor.java | 19 ++++++++++ .../gdsc/global/property/DiscordProperty.java | 13 +++++++ src/main/resources/application-discord.yml | 2 + src/main/resources/application.yml | 1 + .../gdschongik/gdsc/GdscApplicationTests.java | 2 + .../application/AdminMemberServiceTest.java | 2 + src/test/resources/application-test.yml | 3 ++ 12 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/discord/listener/PingpongListener.java create mode 100644 src/main/java/com/gdschongik/gdsc/global/config/DiscordConfig.java create mode 100644 src/main/java/com/gdschongik/gdsc/global/discord/Listener.java create mode 100644 src/main/java/com/gdschongik/gdsc/global/discord/ListenerBeanPostProcessor.java create mode 100644 src/main/java/com/gdschongik/gdsc/global/property/DiscordProperty.java create mode 100644 src/main/resources/application-discord.yml diff --git a/build.gradle b/build.gradle index a99ad76e8..d1102d043 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,9 @@ dependencies { // Actuator implementation 'org.springframework.boot:spring-boot-starter-actuator' + + // Discord + implementation 'net.dv8tion:JDA:5.0.0-beta.20' } tasks.named('test') { diff --git a/src/main/java/com/gdschongik/gdsc/domain/discord/listener/PingpongListener.java b/src/main/java/com/gdschongik/gdsc/domain/discord/listener/PingpongListener.java new file mode 100644 index 000000000..fb77487d7 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/discord/listener/PingpongListener.java @@ -0,0 +1,30 @@ +package com.gdschongik.gdsc.domain.discord.listener; + +import com.gdschongik.gdsc.global.discord.Listener; +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; + +@Slf4j +@Listener +public class PingpongListener extends ListenerAdapter { + + @Override + public void onMessageReceived(MessageReceivedEvent event) { + User author = event.getAuthor(); + TextChannel channel = event.getChannel().asTextChannel(); + Message message = event.getMessage(); + String content = message.getContentRaw(); // get only textual content of message + + log.info("Message from {} in {}: {}", author.getName(), channel.getName(), message.getContentDisplay()); + + if (author.isBot()) return; + + if (content.equals("!ping")) { + channel.sendMessage("Pong!").queue(); + } + } +} diff --git a/src/main/java/com/gdschongik/gdsc/global/config/DiscordConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/DiscordConfig.java new file mode 100644 index 000000000..27e3ae463 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/config/DiscordConfig.java @@ -0,0 +1,37 @@ +package com.gdschongik.gdsc.global.config; + +import com.gdschongik.gdsc.global.discord.ListenerBeanPostProcessor; +import com.gdschongik.gdsc.global.property.DiscordProperty; +import lombok.RequiredArgsConstructor; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.MemberCachePolicy; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class DiscordConfig { + + private final DiscordProperty discordProperty; + + @Bean + @ConditionalOnProperty(value = "discord.enabled", havingValue = "true", matchIfMissing = true) + public JDA jda() { + return JDABuilder.createDefault(discordProperty.getToken()) + .setActivity(Activity.playing("테스트")) + .enableIntents(GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_MEMBERS) + .setMemberCachePolicy(MemberCachePolicy.ALL) + .build(); + } + + @Bean + @ConditionalOnBean(JDA.class) + public ListenerBeanPostProcessor listenerBeanPostProcessor(JDA jda) { + return new ListenerBeanPostProcessor(jda); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java index 0a29d335b..7f6d903ca 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java @@ -1,11 +1,12 @@ package com.gdschongik.gdsc.global.config; +import com.gdschongik.gdsc.global.property.DiscordProperty; import com.gdschongik.gdsc.global.property.JwtProperty; import com.gdschongik.gdsc.global.property.RedisProperty; import com.gdschongik.gdsc.global.property.SwaggerProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; -@EnableConfigurationProperties({JwtProperty.class, RedisProperty.class, SwaggerProperty.class}) +@EnableConfigurationProperties({JwtProperty.class, RedisProperty.class, SwaggerProperty.class, DiscordProperty.class}) @Configuration public class PropertyConfig {} diff --git a/src/main/java/com/gdschongik/gdsc/global/discord/Listener.java b/src/main/java/com/gdschongik/gdsc/global/discord/Listener.java new file mode 100644 index 000000000..6cd401f08 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/discord/Listener.java @@ -0,0 +1,12 @@ +package com.gdschongik.gdsc.global.discord; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.stereotype.Component; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface Listener {} diff --git a/src/main/java/com/gdschongik/gdsc/global/discord/ListenerBeanPostProcessor.java b/src/main/java/com/gdschongik/gdsc/global/discord/ListenerBeanPostProcessor.java new file mode 100644 index 000000000..d260af0dd --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/discord/ListenerBeanPostProcessor.java @@ -0,0 +1,19 @@ +package com.gdschongik.gdsc.global.discord; + +import lombok.RequiredArgsConstructor; +import net.dv8tion.jda.api.JDA; +import org.springframework.beans.factory.config.BeanPostProcessor; + +@RequiredArgsConstructor +public class ListenerBeanPostProcessor implements BeanPostProcessor { + + private final JDA jda; + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean.getClass().isAnnotationPresent(Listener.class)) { + jda.addEventListener(bean); + } + return bean; + } +} diff --git a/src/main/java/com/gdschongik/gdsc/global/property/DiscordProperty.java b/src/main/java/com/gdschongik/gdsc/global/property/DiscordProperty.java new file mode 100644 index 000000000..5199d0a40 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/property/DiscordProperty.java @@ -0,0 +1,13 @@ +package com.gdschongik.gdsc.global.property; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@AllArgsConstructor +@ConfigurationProperties(prefix = "discord") +public class DiscordProperty { + + private final String token; +} diff --git a/src/main/resources/application-discord.yml b/src/main/resources/application-discord.yml new file mode 100644 index 000000000..70ca71861 --- /dev/null +++ b/src/main/resources/application-discord.yml @@ -0,0 +1,2 @@ +discord: + token: ${DISCORD_BOT_TOKEN:} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 37cd16cd1..e8fd8f2f8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,6 +9,7 @@ spring: - security - swagger - actuator + - discord logging: level: diff --git a/src/test/java/com/gdschongik/gdsc/GdscApplicationTests.java b/src/test/java/com/gdschongik/gdsc/GdscApplicationTests.java index 1e73666e7..856159dce 100644 --- a/src/test/java/com/gdschongik/gdsc/GdscApplicationTests.java +++ b/src/test/java/com/gdschongik/gdsc/GdscApplicationTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class GdscApplicationTests { @Test diff --git a/src/test/java/com/gdschongik/gdsc/domain/member/application/AdminMemberServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/member/application/AdminMemberServiceTest.java index 00ecd1aa8..dffa75673 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/member/application/AdminMemberServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/member/application/AdminMemberServiceTest.java @@ -10,8 +10,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class AdminMemberServiceTest { @Autowired private MemberRepository memberRepository; diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 915f6cf78..4cd183245 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -5,3 +5,6 @@ spring: datasource: url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;MODE=MYSQL + +discord: + enabled: false