diff --git a/src/main/java/com/sopterm/makeawish/common/AbuseException.java b/src/main/java/com/sopterm/makeawish/common/AbuseException.java new file mode 100644 index 00000000..699e68ee --- /dev/null +++ b/src/main/java/com/sopterm/makeawish/common/AbuseException.java @@ -0,0 +1,10 @@ +package com.sopterm.makeawish.common; + +import lombok.Getter; + +@Getter +public class AbuseException extends RuntimeException { + public AbuseException(String message) { + super(message); + } +} diff --git a/src/main/java/com/sopterm/makeawish/common/ApiResponse.java b/src/main/java/com/sopterm/makeawish/common/ApiResponse.java index d7d5619a..ae3b2411 100644 --- a/src/main/java/com/sopterm/makeawish/common/ApiResponse.java +++ b/src/main/java/com/sopterm/makeawish/common/ApiResponse.java @@ -27,4 +27,12 @@ public static ApiResponse fail(String message) { .message(message) .build(); } + + public static ApiResponse fail(String message, Object data) { + return ApiResponse.builder() + .success(false) + .message(message) + .data(data) + .build(); + } } diff --git a/src/main/java/com/sopterm/makeawish/common/ErrorHandler.java b/src/main/java/com/sopterm/makeawish/common/ErrorHandler.java index 5c06db63..c48d874b 100644 --- a/src/main/java/com/sopterm/makeawish/common/ErrorHandler.java +++ b/src/main/java/com/sopterm/makeawish/common/ErrorHandler.java @@ -82,4 +82,10 @@ public ResponseEntity popbillException(PopbillException exception){ val response = ApiResponse.fail(exception.getMessage()); return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); } + + @ExceptionHandler(AbuseException.class) + public ResponseEntity abuseException(AbuseException exception){ + val response = ApiResponse.fail(exception.getMessage()); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } } diff --git a/src/main/java/com/sopterm/makeawish/common/message/ErrorMessage.java b/src/main/java/com/sopterm/makeawish/common/message/ErrorMessage.java index f9d4c3a1..ab4f565b 100644 --- a/src/main/java/com/sopterm/makeawish/common/message/ErrorMessage.java +++ b/src/main/java/com/sopterm/makeawish/common/message/ErrorMessage.java @@ -25,6 +25,7 @@ public enum ErrorMessage { NULL_PRINCIPAL("principal 이 null 일 수 없습니다."), NO_EXIST_USER_ACCOUNT("유저의 계좌정보가 없습니다."), NOT_VALID_USER_ACCOUNT("유저의 계좌번호가 아닙니다."), + IS_ABUSE_USER("어뷰징 유저로 이용 불가합니다."), /** cake **/ INVALID_CAKE("존재하지 않는 케이크입니다."), diff --git a/src/main/java/com/sopterm/makeawish/common/message/SuccessMessage.java b/src/main/java/com/sopterm/makeawish/common/message/SuccessMessage.java index 244ebf3d..a039ae23 100644 --- a/src/main/java/com/sopterm/makeawish/common/message/SuccessMessage.java +++ b/src/main/java/com/sopterm/makeawish/common/message/SuccessMessage.java @@ -18,6 +18,7 @@ public enum SuccessMessage { SUCCESS_UPDATE_USER_ACCOUNT_INFO("유저 계좌정보 수정 성공"), SUCCESS_DELETE_USER("회원 탈퇴 성공"), SUCCESS_VERIFY_USER_ACCOUNT("유저 계좌정보 인증 성공"), + IS_NOT_ABUSE_USER("어뷰징 유저가 아닙니다."), /** wish **/ SUCCESS_CREATE_WISH("소원 링크 생성 성공"), diff --git a/src/main/java/com/sopterm/makeawish/controller/UserController.java b/src/main/java/com/sopterm/makeawish/controller/UserController.java index 1a83e932..89d6bbfd 100644 --- a/src/main/java/com/sopterm/makeawish/controller/UserController.java +++ b/src/main/java/com/sopterm/makeawish/controller/UserController.java @@ -1,9 +1,10 @@ package com.sopterm.makeawish.controller; -import com.popbill.api.PopbillException; import com.sopterm.makeawish.common.ApiResponse; import com.sopterm.makeawish.domain.user.InternalMemberDetails; import com.sopterm.makeawish.dto.user.UserAccountRequestDTO; +import com.sopterm.makeawish.dto.user.UserAccountVerifyRequestDTO; +import com.sopterm.makeawish.service.AbuseService; import com.sopterm.makeawish.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -15,6 +16,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import static com.sopterm.makeawish.common.message.ErrorMessage.NOT_VALID_USER_ACCOUNT; import static com.sopterm.makeawish.common.message.ErrorMessage.NO_EXIST_USER_ACCOUNT; import static com.sopterm.makeawish.common.message.SuccessMessage.*; import static java.util.Objects.nonNull; @@ -27,6 +29,8 @@ public class UserController { private final UserService userService; + private final AbuseService abuseService; + private static final int VERIFY_ACCOUNT_SUCCESS = 0; @Operation(summary = "유저 계좌 정보 가져오기") @GetMapping("/account") @@ -60,10 +64,23 @@ public ResponseEntity deleteUser( } @Operation(summary = "계좌 실명 조회") - @GetMapping("/verify-account") + @PostMapping("/verify-account") public ResponseEntity checkAccountInformation( - @RequestParam String name, @RequestParam String BankCode, @RequestParam String AccountNumber) throws Exception { - userService.verifyUserAccount(name, BankCode, AccountNumber); - return ResponseEntity.ok(ApiResponse.success(SUCCESS_VERIFY_USER_ACCOUNT.getMessage())); + @Parameter(hidden = true) @AuthenticationPrincipal InternalMemberDetails memberDetails, + @RequestBody UserAccountVerifyRequestDTO verifyRequestDTO) throws Exception { + val response = userService.verifyUserAccount(memberDetails.getId(), verifyRequestDTO); + return response == VERIFY_ACCOUNT_SUCCESS + ? ResponseEntity.ok(ApiResponse.success(SUCCESS_VERIFY_USER_ACCOUNT.getMessage())) + : ResponseEntity.ok(ApiResponse.fail(NOT_VALID_USER_ACCOUNT.getMessage(), response)); + } + + @Operation(summary = "어뷰징 유저 확인") + @GetMapping("/abuse") + public ResponseEntity checkAbuseUser( + @Parameter(hidden = true) @AuthenticationPrincipal InternalMemberDetails memberDetails + ) { + abuseService.checkAbuseUser(memberDetails.getId()); + val response = abuseService.countAbuseLogByUser(memberDetails.getId()); + return ResponseEntity.ok(ApiResponse.success(IS_NOT_ABUSE_USER.getMessage(), response)); } } diff --git a/src/main/java/com/sopterm/makeawish/controller/WishController.java b/src/main/java/com/sopterm/makeawish/controller/WishController.java index 862f41d7..9c672837 100644 --- a/src/main/java/com/sopterm/makeawish/controller/WishController.java +++ b/src/main/java/com/sopterm/makeawish/controller/WishController.java @@ -5,6 +5,8 @@ import com.sopterm.makeawish.dto.wish.UserWishUpdateRequestDTO; import com.sopterm.makeawish.dto.wish.WishIdRequestDTO; import com.sopterm.makeawish.dto.wish.WishRequestDTO; +import com.sopterm.makeawish.service.AbuseService; +import com.sopterm.makeawish.service.UserService; import com.sopterm.makeawish.service.WishService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -35,6 +37,7 @@ public class WishController { private final WishService wishService; + private final AbuseService abuseService; @Operation(summary = "소원 링크 생성") @PostMapping @@ -42,6 +45,7 @@ public ResponseEntity createWish( @Parameter(hidden = true) @AuthenticationPrincipal InternalMemberDetails memberDetails, @RequestBody WishRequestDTO requestDTO ) { + abuseService.checkAbuseUser(memberDetails.getId()); val wishId = wishService.createWish(memberDetails.getId(), requestDTO); return ResponseEntity .created(getURI(wishId)) diff --git a/src/main/java/com/sopterm/makeawish/domain/abuse/AbuseLog.java b/src/main/java/com/sopterm/makeawish/domain/abuse/AbuseLog.java new file mode 100644 index 00000000..a933579a --- /dev/null +++ b/src/main/java/com/sopterm/makeawish/domain/abuse/AbuseLog.java @@ -0,0 +1,37 @@ +package com.sopterm.makeawish.domain.abuse; + +import com.sopterm.makeawish.domain.user.User; +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; + +@Entity +@Getter +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class AbuseLog { + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "abuse_log_id") + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "user_id") + private User user; + + @CreatedDate + protected LocalDateTime createdAt; + + @Builder + public AbuseLog(User user){ + this.user = user; + } +} diff --git a/src/main/java/com/sopterm/makeawish/domain/abuse/AbuseUser.java b/src/main/java/com/sopterm/makeawish/domain/abuse/AbuseUser.java new file mode 100644 index 00000000..b7a0f3bb --- /dev/null +++ b/src/main/java/com/sopterm/makeawish/domain/abuse/AbuseUser.java @@ -0,0 +1,37 @@ +package com.sopterm.makeawish.domain.abuse; + +import com.sopterm.makeawish.domain.user.User; +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; + +@Entity +@Getter +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class AbuseUser { + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "abuse_user_id") + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "user_id") + private User user; + + @CreatedDate + protected LocalDateTime createdAt; + + @Builder + public AbuseUser(User user){ + this.user = user; + } +} diff --git a/src/main/java/com/sopterm/makeawish/dto/user/UserAccountVerifyRequestDTO.java b/src/main/java/com/sopterm/makeawish/dto/user/UserAccountVerifyRequestDTO.java new file mode 100644 index 00000000..47ccd6f7 --- /dev/null +++ b/src/main/java/com/sopterm/makeawish/dto/user/UserAccountVerifyRequestDTO.java @@ -0,0 +1,14 @@ +package com.sopterm.makeawish.dto.user; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record UserAccountVerifyRequestDTO( + String name, + @JsonProperty("bankCode") + String BankCode, + @JsonProperty("accountNumber") + String AccountNumber +) { +} diff --git a/src/main/java/com/sopterm/makeawish/repository/abuse/AbuseLogRepository.java b/src/main/java/com/sopterm/makeawish/repository/abuse/AbuseLogRepository.java new file mode 100644 index 00000000..458c73fa --- /dev/null +++ b/src/main/java/com/sopterm/makeawish/repository/abuse/AbuseLogRepository.java @@ -0,0 +1,11 @@ +package com.sopterm.makeawish.repository.abuse; + +import com.sopterm.makeawish.domain.abuse.AbuseLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface AbuseLogRepository extends JpaRepository { + @Query(value = "SELECT COUNT(AL) FROM ABUSE_LOG AL WHERE AL.user_id = :userId and AL.created_at >= (now() - interval '7 days')", nativeQuery = true) + Integer countAbuseLogByUserIdDuringWeekend(@Param("userId") Long userId); +} diff --git a/src/main/java/com/sopterm/makeawish/repository/abuse/AbuseUserRepository.java b/src/main/java/com/sopterm/makeawish/repository/abuse/AbuseUserRepository.java new file mode 100644 index 00000000..c64cafb1 --- /dev/null +++ b/src/main/java/com/sopterm/makeawish/repository/abuse/AbuseUserRepository.java @@ -0,0 +1,10 @@ +package com.sopterm.makeawish.repository.abuse; + +import com.sopterm.makeawish.domain.abuse.AbuseUser; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AbuseUserRepository extends JpaRepository { + Optional findAbuseUserByUserId(Long userId); +} diff --git a/src/main/java/com/sopterm/makeawish/service/AbuseService.java b/src/main/java/com/sopterm/makeawish/service/AbuseService.java new file mode 100644 index 00000000..10a0ef89 --- /dev/null +++ b/src/main/java/com/sopterm/makeawish/service/AbuseService.java @@ -0,0 +1,57 @@ +package com.sopterm.makeawish.service; + +import com.sopterm.makeawish.common.AbuseException; +import com.sopterm.makeawish.domain.abuse.AbuseLog; +import com.sopterm.makeawish.domain.abuse.AbuseUser; +import com.sopterm.makeawish.domain.user.User; +import com.sopterm.makeawish.repository.UserRepository; +import com.sopterm.makeawish.repository.abuse.AbuseLogRepository; +import com.sopterm.makeawish.repository.abuse.AbuseUserRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.sopterm.makeawish.common.message.ErrorMessage.INVALID_USER; +import static com.sopterm.makeawish.common.message.ErrorMessage.IS_ABUSE_USER; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AbuseService { + private final AbuseLogRepository abuseLogRepository; + private final AbuseUserRepository abuseUserRepository; + private final UserRepository userRepository; + private static final int ABUSE_CAUTION_COUNT = 4; + + @Transactional + public void createAbuseUser(Long userId) { + abuseUserRepository.save(new AbuseUser(getUser(userId))); + } + + public void checkAbuseUser(Long userId) { + abuseUserRepository.findAbuseUserByUserId(userId) + .ifPresent(abuseUser -> { + throw new AbuseException(IS_ABUSE_USER.getMessage()); + }); + } + + @Transactional + public Integer countAbuseLogByUser(Long userId) { + val abuseLogCount = abuseLogRepository.countAbuseLogByUserIdDuringWeekend(userId); + if (abuseLogCount >= ABUSE_CAUTION_COUNT) { + createAbuseUser(userId); + } + return abuseLogCount; + } + + public void createAbuseLog(AbuseLog abuseLog){ + abuseLogRepository.save(abuseLog); + } + + private User getUser(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException(INVALID_USER.getMessage())); + } +} diff --git a/src/main/java/com/sopterm/makeawish/service/UserService.java b/src/main/java/com/sopterm/makeawish/service/UserService.java index 2c1d8fb6..a17cc696 100644 --- a/src/main/java/com/sopterm/makeawish/service/UserService.java +++ b/src/main/java/com/sopterm/makeawish/service/UserService.java @@ -1,11 +1,12 @@ package com.sopterm.makeawish.service; -import com.popbill.api.AccountCheckInfo; import com.popbill.api.AccountCheckService; import com.popbill.api.PopbillException; +import com.sopterm.makeawish.domain.abuse.AbuseLog; import com.sopterm.makeawish.domain.user.User; import com.sopterm.makeawish.dto.user.UserAccountRequestDTO; import com.sopterm.makeawish.dto.user.UserAccountResponseDTO; +import com.sopterm.makeawish.dto.user.UserAccountVerifyRequestDTO; import com.sopterm.makeawish.repository.PresentRepository; import com.sopterm.makeawish.repository.UserRepository; import com.sopterm.makeawish.repository.wish.WishRepository; @@ -16,7 +17,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import static com.sopterm.makeawish.common.message.ErrorMessage.*; +import static com.sopterm.makeawish.common.message.ErrorMessage.INVALID_USER; +import static com.sopterm.makeawish.common.message.ErrorMessage.NO_EXIST_USER_ACCOUNT; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; @@ -25,56 +27,66 @@ @Transactional(readOnly = true) public class UserService { - private final UserRepository userRepository; - private final WishRepository wishRepository; - private final PresentRepository presentRepository; - private final AccountCheckService accountCheckService; + private final UserRepository userRepository; + private final WishRepository wishRepository; + private final PresentRepository presentRepository; + private final AccountCheckService accountCheckService; + private final AbuseService abuseService; - @Value("${popbill.businessNumber}") - private String corpNum; + @Value("${popbill.businessNumber}") + private String corpNum; + private static final int ABUSE_CAUTION_COUNT = 4; - public UserAccountResponseDTO getUserAccount(Long userId) { - val wisher = getUser(userId); - return nonNull(wisher.getAccount()) ? UserAccountResponseDTO.of(wisher) : null; - } + public UserAccountResponseDTO getUserAccount(Long userId) { + val wisher = getUser(userId); + return nonNull(wisher.getAccount()) ? UserAccountResponseDTO.of(wisher) : null; + } - @Transactional - public UserAccountResponseDTO updateUserAccount(Long userId, UserAccountRequestDTO requestDTO) { - val wisher = getUser(userId); - if (isNull(requestDTO.accountInfo())) { - throw new IllegalArgumentException(NO_EXIST_USER_ACCOUNT.getMessage()); - } - wisher.updateAccount( - requestDTO.accountInfo().getName(), - requestDTO.accountInfo().getBank(), - requestDTO.accountInfo().getAccount()); - wisher.updatePhoneNumber(requestDTO.phone()); - return UserAccountResponseDTO.of(wisher); - } + @Transactional + public UserAccountResponseDTO updateUserAccount(Long userId, UserAccountRequestDTO requestDTO) { + val wisher = getUser(userId); + if (isNull(requestDTO.accountInfo())) { + throw new IllegalArgumentException(NO_EXIST_USER_ACCOUNT.getMessage()); + } + wisher.updateAccount( + requestDTO.accountInfo().getName(), + requestDTO.accountInfo().getBank(), + requestDTO.accountInfo().getAccount()); + wisher.updatePhoneNumber(requestDTO.phone()); + return UserAccountResponseDTO.of(wisher); + } - @Transactional - public void deleteUser(Long userId) { - val user = getUser(userId); - user.getWishes().forEach(wish -> { - wish.getPresents().forEach(presentRepository::delete); - wishRepository.delete(wish); - }); - userRepository.delete(user); - } + @Transactional + public void deleteUser(Long userId) { + val user = getUser(userId); + user.getWishes().forEach(wish -> { + wish.getPresents().forEach(presentRepository::delete); + wishRepository.delete(wish); + }); + userRepository.delete(user); + } - private User getUser(Long userId) { - return userRepository.findById(userId) - .orElseThrow(() -> new EntityNotFoundException(INVALID_USER.getMessage())); - } + private User getUser(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException(INVALID_USER.getMessage())); + } - public void verifyUserAccount(String name, String BankCode, String AccountNumber) throws PopbillException { - try { - val accountInfo = accountCheckService.CheckAccountInfo(corpNum, BankCode, AccountNumber); - - if(!name.equals(accountInfo.getAccountName())) - throw new IllegalArgumentException(NOT_VALID_USER_ACCOUNT.getMessage()); - } catch (PopbillException e) { - throw new PopbillException(e.getCode(), e.getMessage()); - } - } + @Transactional + public Integer verifyUserAccount(Long userId, UserAccountVerifyRequestDTO verifyRequestDTO) throws PopbillException { + abuseService.checkAbuseUser(userId); + var response = 0; + try { + val accountInfo = accountCheckService.CheckAccountInfo(corpNum, verifyRequestDTO.BankCode(), verifyRequestDTO.AccountNumber()); + if (!verifyRequestDTO.name().equals(accountInfo.getAccountName())) { + val abuseLog = AbuseLog.builder() + .user(getUser(userId)) + .build(); + abuseService.createAbuseLog(abuseLog); + response = abuseService.countAbuseLogByUser(userId); + } + } catch (PopbillException e) { + throw new PopbillException(e.getCode(), e.getMessage()); + } + return response; + } } diff --git a/src/test/java/com/sopterm/makeawish/service/UserServiceTest.java b/src/test/java/com/sopterm/makeawish/service/UserServiceTest.java index 2d96a0af..8a97ff8e 100644 --- a/src/test/java/com/sopterm/makeawish/service/UserServiceTest.java +++ b/src/test/java/com/sopterm/makeawish/service/UserServiceTest.java @@ -1,9 +1,8 @@ package com.sopterm.makeawish.service; -import com.popbill.api.AccountCheckInfo; import com.popbill.api.AccountCheckService; import com.popbill.api.PopbillException; -import org.checkerframework.checker.units.qual.A; +import com.sopterm.makeawish.dto.user.UserAccountVerifyRequestDTO; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -11,7 +10,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertThrows; @SpringBootTest @@ -26,13 +25,16 @@ class UserServiceTest { @Test @DisplayName("유효하지 않은 기관코드인 경우 예외 발생") - void 계좌_연동_실패(){ + void failCheckAcountInfo() throws PopbillException { // given - String BankCode = "0000"; - String AccountNumber = "1234567890"; - - // when - then - assertThatThrownBy(() -> userService.verifyUserAccount("홍길동",BankCode, AccountNumber)) - .isInstanceOf(PopbillException.class); + String corpNum = "0123456789"; + UserAccountVerifyRequestDTO requestDTO = UserAccountVerifyRequestDTO.builder() + .BankCode("0000") + .name("홍길동") + .AccountNumber("1234567890") + .build(); + assertThrows(PopbillException.class, () -> { + accountCheckService.CheckAccountInfo(corpNum, requestDTO.BankCode(), requestDTO.AccountNumber()); + }); } } \ No newline at end of file