diff --git a/build.gradle b/build.gradle index 395e1a6..b6da6e8 100644 --- a/build.gradle +++ b/build.gradle @@ -68,6 +68,8 @@ dependencies { //mail implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.0.5' + // s3 bucket + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } diff --git a/src/main/java/store/itpick/backend/common/exception/AuthException.java b/src/main/java/store/itpick/backend/common/exception/AuthException.java new file mode 100644 index 0000000..fa7a1bf --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception/AuthException.java @@ -0,0 +1,22 @@ +package store.itpick.backend.common.exception; + +import lombok.Getter; +import store.itpick.backend.common.response.status.ResponseStatus; + +@Getter //사용자 정의 예외 +public class AuthException extends RuntimeException{ + private final ResponseStatus exceptionStatus; //예외 상태 저장 + + //객체를 매개변수로 예외 객체 생성 + public AuthException(ResponseStatus exceptionStatus) { + super(exceptionStatus.getMessage()); + this.exceptionStatus = exceptionStatus; + } + + //객체, 메세지를 매개변수로 예외 객체 생성 + public AuthException(ResponseStatus exceptionStatus, String message) { + super(message); + this.exceptionStatus = exceptionStatus; + } + +} diff --git a/src/main/java/store/itpick/backend/common/exception_handler/AuthExceptionControllerAdvice.java b/src/main/java/store/itpick/backend/common/exception_handler/AuthExceptionControllerAdvice.java new file mode 100644 index 0000000..258f077 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception_handler/AuthExceptionControllerAdvice.java @@ -0,0 +1,23 @@ +package store.itpick.backend.common.exception_handler; + +import jakarta.annotation.Priority; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import store.itpick.backend.common.exception.AuthException; +import store.itpick.backend.common.response.BaseErrorResponse; + +@Slf4j +@Priority(0) +@RestControllerAdvice //모든 컨트롤러에서 발생하는 예외를 전역적으로 처리 +public class AuthExceptionControllerAdvice { + + @ResponseStatus(HttpStatus.BAD_REQUEST) //해당 메서드의 예외 발생시 보낼 + @ExceptionHandler(AuthException.class) + public BaseErrorResponse handle_UserException(AuthException e){ + log.error("[handle_AuthException]", e); + return new BaseErrorResponse(e.getExceptionStatus(), e.getMessage()); + } +} diff --git a/src/main/java/store/itpick/backend/common/exception_handler/UserExceptionControllerAdvice.java b/src/main/java/store/itpick/backend/common/exception_handler/UserExceptionControllerAdvice.java index e69abfd..4b2a83c 100644 --- a/src/main/java/store/itpick/backend/common/exception_handler/UserExceptionControllerAdvice.java +++ b/src/main/java/store/itpick/backend/common/exception_handler/UserExceptionControllerAdvice.java @@ -9,8 +9,6 @@ import store.itpick.backend.common.exception.UserException; import store.itpick.backend.common.response.BaseErrorResponse; -import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.INVALID_USER_VALUE; - @Slf4j @Priority(0) @RestControllerAdvice //모든 컨트롤러에서 발생하는 예외를 전역적으로 처리 diff --git a/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java b/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java index 1dc9031..d724185 100644 --- a/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java +++ b/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java @@ -55,6 +55,9 @@ public enum BaseExceptionResponseStatus implements ResponseStatus { NO_SUCH_ALGORITHM(5009, HttpStatus.BAD_REQUEST.value(), "인증 번호 생성을 위한 알고리즘을 찾을 수 없습니다."), AUTH_CODE_IS_NOT_SAME(5010, HttpStatus.BAD_REQUEST.value(), "인증 번호가 일치하지 않습니다."), MEMBER_EXISTS(5011,HttpStatus.BAD_REQUEST.value(), "이미 존재하는 회원입니다."), + INVALID_PROFILE_IMG(5012,HttpStatus.BAD_REQUEST.value(), "잘못된 이미지 파일입니다."), + UPLOAD_FAIL(5013,HttpStatus.BAD_REQUEST.value(), "파일 업로드에 실패했습니다. 인터넷 연결을 확인하거나, 나중에 다시 시도해 주세요."), + INVALID_USER_DB_VALUE(5014,HttpStatus.BAD_REQUEST.value(), "유저 정보에 오류가 발생했습니다. 관리자에게 문의해주세요."), /** * 6000: Debate 오류 diff --git a/src/main/java/store/itpick/backend/config/S3Config.java b/src/main/java/store/itpick/backend/config/S3Config.java new file mode 100644 index 0000000..02c3dc2 --- /dev/null +++ b/src/main/java/store/itpick/backend/config/S3Config.java @@ -0,0 +1,28 @@ +package store.itpick.backend.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCredentials= new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/controller/AuthController.java b/src/main/java/store/itpick/backend/controller/AuthController.java index 7ca48ff..c84ad25 100644 --- a/src/main/java/store/itpick/backend/controller/AuthController.java +++ b/src/main/java/store/itpick/backend/controller/AuthController.java @@ -17,7 +17,7 @@ import store.itpick.backend.dto.auth.PostUserRequest; import store.itpick.backend.dto.auth.PostUserResponse; import store.itpick.backend.service.AuthService; -import store.itpick.backend.common.exception.UserException; +import store.itpick.backend.common.exception.AuthException; import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.INVALID_USER_VALUE; import static store.itpick.backend.util.BindingResultUtils.getErrorMessages; @@ -44,7 +44,7 @@ public BaseResponse refresh(@Validated @RequestBody RefreshRequ public BaseResponse login(@Validated @RequestBody LoginRequest authRequest, BindingResult bindingResult) { log.info("[AuthController.login]"); if (bindingResult.hasErrors()) { - throw new UserException(INVALID_USER_VALUE, getErrorMessages(bindingResult)); + throw new AuthException(INVALID_USER_VALUE, getErrorMessages(bindingResult)); } return new BaseResponse<>(authService.login(authRequest)); } @@ -62,7 +62,7 @@ public BaseResponse logout(@PreAuthorize long userId) { @PostMapping("/signup") public BaseResponse signUp(@Valid @RequestBody PostUserRequest postUserRequest, BindingResult bindingResult) { if(bindingResult.hasErrors()){ - throw new UserException(INVALID_USER_VALUE, getErrorMessages(bindingResult)); + throw new AuthException(INVALID_USER_VALUE, getErrorMessages(bindingResult)); } return new BaseResponse<>(authService.signUp(postUserRequest)); } diff --git a/src/main/java/store/itpick/backend/controller/RankController.java b/src/main/java/store/itpick/backend/controller/RankController.java index 3915a6e..5cc376f 100644 --- a/src/main/java/store/itpick/backend/controller/RankController.java +++ b/src/main/java/store/itpick/backend/controller/RankController.java @@ -2,19 +2,15 @@ import org.openqa.selenium.TimeoutException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import store.itpick.backend.common.exception.UserException; import store.itpick.backend.common.response.BaseErrorResponse; import store.itpick.backend.common.response.BaseResponse; import store.itpick.backend.common.response.status.ResponseStatus; -import store.itpick.backend.model.CommunityType; -import store.itpick.backend.model.PeriodType; -import store.itpick.backend.dto.rank.RankResponseDTO; -import store.itpick.backend.common.response.BaseResponse; +import store.itpick.backend.model.rank.CommunityType; +import store.itpick.backend.model.rank.PeriodType; import store.itpick.backend.dto.rank.RankResponseDTO; import store.itpick.backend.model.Reference; import store.itpick.backend.service.*; @@ -27,8 +23,6 @@ import java.util.concurrent.TimeUnit; import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.BAD_REQUEST; -import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.INVALID_USER_VALUE; -import static store.itpick.backend.util.BindingResultUtils.getErrorMessages; @RestController @RequestMapping("/rank") @@ -55,7 +49,6 @@ public RankController(RankService rankService) { private SchedulerService schedulerService; - // 최대 재시도 횟수와 재시도 간격 (초) private static final int MAX_RETRIES = 5; private static final int RETRY_DELAY_SECONDS = 5; @@ -97,9 +90,6 @@ public String getRankFromNamuwiki() { return executeWithRetries(() -> seleniumService.useDriverForNamuwiki(url), "Namuwiki 데이터 수집"); } - - - @GetMapping("/reference") public BaseResponse getReference( @RequestParam String community, @@ -108,8 +98,6 @@ public BaseResponse getReference( RankResponseDTO rankResponse = rankService.getReferenceByKeyword(community, period, keyword); - - if (rankResponse == null) { // 키워드가 없거나 커뮤니티/기간이 없을 경우 적절한 응답 처리 return new BaseResponse<>(null); @@ -119,22 +107,22 @@ public BaseResponse getReference( } @GetMapping("/update/naver") - public void getUpdate(){ + public void getUpdate() { keywordService.performDailyTasksNaver(); } @GetMapping("/update/nate") - public void updateNate(){ + public void updateNate() { keywordService.performDailyTasksNate(); } @GetMapping("/update/zum") - public void updateZum(){ + public void updateZum() { keywordService.performDailyTasksZum(); } @GetMapping("/update") - public void update(){ + public void update() { schedulerService.performHourlyTasks(); } @@ -161,6 +149,15 @@ public ResponseStatus getRankingList(@RequestParam String community, @RequestPar return new BaseResponse<>(redis.getRankingList(communityType, periodType, date)); } + @GetMapping("/badge") + public ResponseStatus getRankingBadgeForKeyword(@RequestParam String keyword, @RequestParam String period, @RequestParam String date) { + PeriodType periodType = getPeriodType(period); + if (periodType == null || !isValidatedDate(periodType, date)) { + return new BaseErrorResponse(BAD_REQUEST); + } + return new BaseResponse<>(redis.getRankingBadgeResponse(keyword, periodType, date)); + } + @GetMapping("/day/test") public void dayTest() { redis.saveDay(); diff --git a/src/main/java/store/itpick/backend/controller/UserController.java b/src/main/java/store/itpick/backend/controller/UserController.java index c3c239f..b4d8d33 100644 --- a/src/main/java/store/itpick/backend/controller/UserController.java +++ b/src/main/java/store/itpick/backend/controller/UserController.java @@ -6,15 +6,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import store.itpick.backend.common.argument_resolver.PreAccessToken; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import store.itpick.backend.common.argument_resolver.PreAuthorize; -import store.itpick.backend.common.exception.UserException; +import store.itpick.backend.common.exception.AuthException; import store.itpick.backend.common.response.BaseResponse; import store.itpick.backend.dto.user.*; +import store.itpick.backend.service.S3ImageBucketService; import store.itpick.backend.service.UserService; import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.INVALID_USER_VALUE; @@ -26,13 +24,14 @@ @RequestMapping("/user") public class UserController { - @Autowired private final UserService userService; + private final S3ImageBucketService s3ImageBucketService; + @PatchMapping("/nickname") public BaseResponse changeNickname(@PreAuthorize long userId, @Validated @RequestBody NicknameRequest nicknameRequest, BindingResult bindingResult){ if (bindingResult.hasErrors()) { - throw new UserException(INVALID_USER_VALUE, getErrorMessages(bindingResult)); + throw new AuthException(INVALID_USER_VALUE, getErrorMessages(bindingResult)); } userService.changeNickname(userId, nicknameRequest.getNickname()); return new BaseResponse<>(null); @@ -53,7 +52,7 @@ public BaseResponse changeBrithDate(@PreAuthorize long userId, @Validated @Re @PatchMapping("/liked-topics") public BaseResponse changeLikedTopics(@PreAuthorize long userId, @Validated @RequestBody LikedTopicsRequest likedTopicsRequest, BindingResult bindingResult){ if (bindingResult.hasErrors()) { - throw new UserException(INVALID_USER_VALUE, getErrorMessages(bindingResult)); + throw new AuthException(INVALID_USER_VALUE, getErrorMessages(bindingResult)); } log.info(String.valueOf(userId)); userService.changeLikedTopics(userId, likedTopicsRequest.getLikedTopicList()); @@ -73,10 +72,21 @@ public BaseResponse changeEmail(@PreAuthorize long userId, @Validated @Reques @PatchMapping("/password") public BaseResponse changePassword(@PreAuthorize long userId, @Validated @RequestBody PasswordRequest passwordRequest, BindingResult bindingResult){ if (bindingResult.hasErrors()) { - throw new UserException(INVALID_USER_VALUE, getErrorMessages(bindingResult)); + throw new AuthException(INVALID_USER_VALUE, getErrorMessages(bindingResult)); } userService.changePassword(userId, passwordRequest.getPassword()); return new BaseResponse<>(null); } + @PostMapping("/profile-img") + public BaseResponse changeProfileImg(@PreAuthorize long userId,@RequestParam("file") MultipartFile file){ + String previousImgUrl = userService.getProfileImgUrl(userId); + if (previousImgUrl != null) { + s3ImageBucketService.deleteImage(previousImgUrl); + } + String imgUrl = s3ImageBucketService.saveProfileImg(file); + userService.changeProfileImg(userId, imgUrl); + return new BaseResponse<>(new ProfileImgResponse(imgUrl)); + } + } diff --git a/src/main/java/store/itpick/backend/dto/redis/GetRankingBadgeResponse.java b/src/main/java/store/itpick/backend/dto/redis/GetRankingBadgeResponse.java new file mode 100644 index 0000000..13c0500 --- /dev/null +++ b/src/main/java/store/itpick/backend/dto/redis/GetRankingBadgeResponse.java @@ -0,0 +1,14 @@ +package store.itpick.backend.dto.redis; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class GetRankingBadgeResponse { + private Long nateRank; + private Long naverRank; + private Long zumRank; +} diff --git a/src/main/java/store/itpick/backend/dto/user/ProfileImgResponse.java b/src/main/java/store/itpick/backend/dto/user/ProfileImgResponse.java new file mode 100644 index 0000000..5f72497 --- /dev/null +++ b/src/main/java/store/itpick/backend/dto/user/ProfileImgResponse.java @@ -0,0 +1,10 @@ +package store.itpick.backend.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class ProfileImgResponse { + private String url; +} diff --git a/src/main/java/store/itpick/backend/model/CommunityType.java b/src/main/java/store/itpick/backend/model/CommunityType.java deleted file mode 100644 index 6b22139..0000000 --- a/src/main/java/store/itpick/backend/model/CommunityType.java +++ /dev/null @@ -1,17 +0,0 @@ -package store.itpick.backend.model; - -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public enum CommunityType { - TOTAL("total"), - NAVER("naver"), - NATE("nate"), - ZUM("zum"); - - private final String communityType; - - public String get() { - return communityType; - } -} diff --git a/src/main/java/store/itpick/backend/model/rank/CommunityType.java b/src/main/java/store/itpick/backend/model/rank/CommunityType.java new file mode 100644 index 0000000..f631109 --- /dev/null +++ b/src/main/java/store/itpick/backend/model/rank/CommunityType.java @@ -0,0 +1,25 @@ +package store.itpick.backend.model.rank; + +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@RequiredArgsConstructor +public enum CommunityType { + TOTAL("total"), + NAVER("naver"), + NATE("nate"), + ZUM("zum"); + + private final String communityType; + + public String value() { + return communityType; + } + + public static List getAllExceptTotal() { + return new ArrayList<>(Arrays.asList(NATE, NAVER, ZUM)); + } +} diff --git a/src/main/java/store/itpick/backend/model/PeriodType.java b/src/main/java/store/itpick/backend/model/rank/PeriodType.java similarity index 87% rename from src/main/java/store/itpick/backend/model/PeriodType.java rename to src/main/java/store/itpick/backend/model/rank/PeriodType.java index 38004a1..94bdaae 100644 --- a/src/main/java/store/itpick/backend/model/PeriodType.java +++ b/src/main/java/store/itpick/backend/model/rank/PeriodType.java @@ -1,4 +1,4 @@ -package store.itpick.backend.model; +package store.itpick.backend.model.rank; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/store/itpick/backend/model/RankingWeight.java b/src/main/java/store/itpick/backend/model/rank/RankingWeight.java similarity index 85% rename from src/main/java/store/itpick/backend/model/RankingWeight.java rename to src/main/java/store/itpick/backend/model/rank/RankingWeight.java index 065bc87..c8efe2a 100644 --- a/src/main/java/store/itpick/backend/model/RankingWeight.java +++ b/src/main/java/store/itpick/backend/model/rank/RankingWeight.java @@ -1,4 +1,4 @@ -package store.itpick.backend.model; +package store.itpick.backend.model.rank; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/store/itpick/backend/service/AuthService.java b/src/main/java/store/itpick/backend/service/AuthService.java index 42e9748..2bb5a3e 100644 --- a/src/main/java/store/itpick/backend/service/AuthService.java +++ b/src/main/java/store/itpick/backend/service/AuthService.java @@ -7,7 +7,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import store.itpick.backend.common.exception.UserException; +import store.itpick.backend.common.exception.AuthException; import store.itpick.backend.common.exception.jwt.unauthorized.JwtExpiredTokenException; import store.itpick.backend.common.exception.jwt.unauthorized.JwtInvalidTokenException; import store.itpick.backend.common.response.status.BaseExceptionResponseStatus; @@ -35,7 +35,6 @@ import java.util.Random; import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.*; -import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.INVALID_TOKEN; @Slf4j @@ -68,7 +67,7 @@ public LoginResponse login(LoginRequest authRequest) { try { user = userRepository.getUserByEmail(email).get(); } catch (NoSuchElementException e) { - throw new UserException(EMAIL_NOT_FOUND); + throw new AuthException(EMAIL_NOT_FOUND); } long userId = user.getUserId(); @@ -88,7 +87,7 @@ public LoginResponse login(LoginRequest authRequest) { private void validatePassword(String password, long userId) { String encodedPassword = userRepository.getUserByUserId(userId).get().getPassword(); if (!passwordEncoder.matches(password, encodedPassword)) { - throw new UserException(PASSWORD_NO_MATCH); + throw new AuthException(PASSWORD_NO_MATCH); } } @@ -141,7 +140,7 @@ public RefreshResponse refresh(String refreshToken){ try { user = userRepository.getUserByEmail(email).get(); } catch (IncorrectResultSizeDataAccessException e) { - throw new UserException(EMAIL_NOT_FOUND); + throw new AuthException(EMAIL_NOT_FOUND); } long userId = user.getUserId(); @@ -156,7 +155,7 @@ public void logout(long userId) { try { user = userRepository.getUserByUserId(userId).get(); } catch (NoSuchElementException e) { - throw new UserException(USER_NOT_FOUND); + throw new AuthException(USER_NOT_FOUND); } user.setRefreshToken(null); userRepository.save(user); @@ -173,7 +172,7 @@ public void modifyUserStatus_deleted(String token) { user.setStatus("deleted"); userRepository.save(user); } else { - throw new UserException(USER_NOT_FOUND); + throw new AuthException(USER_NOT_FOUND); } } @@ -198,7 +197,7 @@ private String createCode() { return builder.toString(); } catch (NoSuchAlgorithmException e) { log.debug("MemberService.createCode() exception occur"); - throw new UserException(BaseExceptionResponseStatus.NO_SUCH_ALGORITHM); + throw new AuthException(BaseExceptionResponseStatus.NO_SUCH_ALGORITHM); } } @@ -208,7 +207,7 @@ public void verifiedCode(String email, String authCode) { boolean authResult = redisService.checkExistsValue(redisAuthCode) && redisAuthCode.equals(authCode); if(!authResult){ - throw new UserException(BaseExceptionResponseStatus.AUTH_CODE_IS_NOT_SAME); + throw new AuthException(BaseExceptionResponseStatus.AUTH_CODE_IS_NOT_SAME); } } @@ -220,13 +219,13 @@ public void verifiedCode(String email, String authCode) { public void validateEmail(String email) { if (userRepository.existsByEmailAndStatusIn(email, List.of("active", "dormant"))) { - throw new UserException(BaseExceptionResponseStatus.DUPLICATE_EMAIL); + throw new AuthException(BaseExceptionResponseStatus.DUPLICATE_EMAIL); } } public void validateNickname(String nickname) { if (userRepository.existsByNicknameAndStatusIn(nickname, List.of("active", "dormant"))) { - throw new UserException(BaseExceptionResponseStatus.DUPLICATE_NICKNAME); + throw new AuthException(BaseExceptionResponseStatus.DUPLICATE_NICKNAME); } } diff --git a/src/main/java/store/itpick/backend/service/DebateService.java b/src/main/java/store/itpick/backend/service/DebateService.java index cf4e40a..efa2e51 100644 --- a/src/main/java/store/itpick/backend/service/DebateService.java +++ b/src/main/java/store/itpick/backend/service/DebateService.java @@ -5,7 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import store.itpick.backend.common.exception.DebateException; -import store.itpick.backend.common.exception.UserException; +import store.itpick.backend.common.exception.AuthException; import store.itpick.backend.dto.debate.*; import store.itpick.backend.dto.vote.PostVoteRequest; import store.itpick.backend.jwt.JwtProvider; @@ -71,7 +71,7 @@ public PostCommentResponse createComment(PostCommentRequest postCommentRequest) Optional userOptional = userRepository.findById(postCommentRequest.getUserId()); if (!userOptional.isPresent()) { - throw new UserException(USER_NOT_FOUND); + throw new AuthException(USER_NOT_FOUND); } User user = userOptional.get(); @@ -87,7 +87,7 @@ public PostCommentHeartResponse creatCommentHeart(PostCommentHeartRequest postCo Optional userOptional = userRepository.findById(postCommentHeartRequest.getUserId()); if (!userOptional.isPresent()) { - throw new UserException(USER_NOT_FOUND); + throw new AuthException(USER_NOT_FOUND); } Optional commentOptional = commentRepository.findById(postCommentHeartRequest.getCommentId()); @@ -104,6 +104,7 @@ public PostCommentHeartResponse creatCommentHeart(PostCommentHeartRequest postCo .commentHeartId(deletedCommentHeartId) .build(); } else { + // 존재하지 않으면 새로운 CommentHeart 생성 및 저장 CommentHeart commentHeart = CommentHeart.builder() .user(userOptional.get()) .comment(commentOptional.get()) diff --git a/src/main/java/store/itpick/backend/service/S3ImageBucketService.java b/src/main/java/store/itpick/backend/service/S3ImageBucketService.java new file mode 100644 index 0000000..83c35e6 --- /dev/null +++ b/src/main/java/store/itpick/backend/service/S3ImageBucketService.java @@ -0,0 +1,114 @@ +package store.itpick.backend.service; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import store.itpick.backend.common.exception.UserException; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.UUID; + +import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.*; + + +@Service +@RequiredArgsConstructor +@Transactional +@Slf4j +public class S3ImageBucketService { + + private final AmazonS3Client amazonS3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private String PROFILE_IMG_DIR = "profile/"; + private String DEBATE_IMG_DIR = "debate/"; + + + // 토론 이미지 업로드 + public String saveDebateImg(MultipartFile uploadFile){ + // img url 반환 + return saveImg(uploadFile, DEBATE_IMG_DIR); + } + + // 프로필 이미지 업로드 + public String saveProfileImg(MultipartFile uploadFile) { + // img url 반환 + return saveImg(uploadFile, PROFILE_IMG_DIR); + } + + + // 이미지 전처리 + private String saveImg(MultipartFile uploadFile, String profileImgDir) { + if (uploadFile.isEmpty()) { + log.debug("Upload file is empty"); + throw new UserException(INVALID_PROFILE_IMG); + } + String fileName = profileImgDir + UUID.randomUUID() + uploadFile.getOriginalFilename(); + + ObjectMetadata metadata= new ObjectMetadata(); + metadata.setContentType(uploadFile.getContentType()); + metadata.setContentLength(uploadFile.getSize()); + + return putS3(uploadFile, fileName, metadata); + } + + + // S3로 업로드 + private String putS3(MultipartFile uploadFile, String fileName, ObjectMetadata metadata) { + + // s3 업로드 + try { + PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, fileName, uploadFile.getInputStream(), metadata) + .withCannedAcl(CannedAccessControlList.PublicRead); + amazonS3Client.putObject(putObjectRequest); + } catch (IOException e) { + throw new UserException(UPLOAD_FAIL); + } + + // url 반환 + return amazonS3Client.getUrl(bucket, fileName).toString(); + } + + + // 이미지 삭제 + public void deleteImage(String imgUrl) { + try { + + // url로 파일 이름 인덱싱 + String fileName = extractFileNameFromUrl(imgUrl); + amazonS3Client.deleteObject(bucket, fileName); + log.info("Image deleted successfully: {}", fileName); + } catch (Exception e) { + log.error("Error occurred while deleting image: {}", imgUrl, e); + throw new UserException(UPLOAD_FAIL); + } + } + + private String extractFileNameFromUrl(String fileNameOrUrl) { + if (fileNameOrUrl.startsWith("https://")) { + try { + URI uri = new URI(fileNameOrUrl); + // URI의 path 부분만 반환 ("/profile/some-image.jpg"와 같은 형식) + return uri.getPath().substring(1); // 앞의 "/" 제거 + } catch (URISyntaxException e) { + throw new UserException(UPLOAD_FAIL); + } + } + else { + // URL이 아닌 경우 + throw new UserException(INVALID_USER_DB_VALUE); + } + } + +} diff --git a/src/main/java/store/itpick/backend/service/SchedulerService.java b/src/main/java/store/itpick/backend/service/SchedulerService.java index cf3cacd..7dd9cc0 100644 --- a/src/main/java/store/itpick/backend/service/SchedulerService.java +++ b/src/main/java/store/itpick/backend/service/SchedulerService.java @@ -6,7 +6,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import store.itpick.backend.model.PeriodType; +import store.itpick.backend.model.rank.PeriodType; import store.itpick.backend.util.Redis; import java.time.DayOfWeek; diff --git a/src/main/java/store/itpick/backend/service/SeleniumService.java b/src/main/java/store/itpick/backend/service/SeleniumService.java index 9093a44..8107c61 100644 --- a/src/main/java/store/itpick/backend/service/SeleniumService.java +++ b/src/main/java/store/itpick/backend/service/SeleniumService.java @@ -7,26 +7,16 @@ import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; -import org.openqa.selenium.chrome.ChromeDriver; -import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import store.itpick.backend.common.exception.ReferenceException; import store.itpick.backend.model.*; -import store.itpick.backend.repository.KeywordRepository; -import store.itpick.backend.service.KeywordService; -import store.itpick.backend.service.CommunityPeriodService; - -import java.sql.Date; -import java.sql.Timestamp; -import java.time.Instant; -import java.time.LocalDateTime; +import store.itpick.backend.model.rank.CommunityType; +import store.itpick.backend.model.rank.PeriodType; + import store.itpick.backend.util.Redis; import store.itpick.backend.util.SeleniumUtil; @@ -36,10 +26,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.EMPTY_REFERENCE; @Slf4j @Component diff --git a/src/main/java/store/itpick/backend/service/UserService.java b/src/main/java/store/itpick/backend/service/UserService.java index 9e1efad..8dad987 100644 --- a/src/main/java/store/itpick/backend/service/UserService.java +++ b/src/main/java/store/itpick/backend/service/UserService.java @@ -1,12 +1,9 @@ package store.itpick.backend.service; -import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hibernate.validator.constraints.Length; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import store.itpick.backend.common.exception.UserException; import store.itpick.backend.dto.auth.JwtDTO; import store.itpick.backend.jwt.JwtProvider; import store.itpick.backend.model.LikedTopic; @@ -19,7 +16,6 @@ import java.util.*; import java.util.stream.Collectors; -import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.USER_NOT_FOUND; import static store.itpick.backend.util.UserUtils.getUser; @Slf4j @@ -111,4 +107,19 @@ public void changePassword(long userId, String password) { userRepository.save(user); } + + public void changeProfileImg(long userId, String imgUrl) { + User user = getUser(userId, userRepository); + // Encrypt password + user.setImageUrl(imgUrl); + user.setUpdateAt(new Timestamp(System.currentTimeMillis())); + + userRepository.save(user); + } + + + public String getProfileImgUrl(long userId) { + User user = getUser(userId, userRepository); + return user.getImageUrl(); + } } diff --git a/src/main/java/store/itpick/backend/util/Redis.java b/src/main/java/store/itpick/backend/util/Redis.java index 7d25725..b815c48 100644 --- a/src/main/java/store/itpick/backend/util/Redis.java +++ b/src/main/java/store/itpick/backend/util/Redis.java @@ -4,10 +4,11 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Component; +import store.itpick.backend.dto.redis.GetRankingBadgeResponse; import store.itpick.backend.dto.redis.GetRankingListResponse; -import store.itpick.backend.model.CommunityType; -import store.itpick.backend.model.PeriodType; -import store.itpick.backend.model.RankingWeight; +import store.itpick.backend.model.rank.CommunityType; +import store.itpick.backend.model.rank.PeriodType; +import store.itpick.backend.model.rank.RankingWeight; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -133,8 +134,26 @@ public GetRankingListResponse getRankingList(CommunityType communityType, Period return new GetRankingListResponse(key, rankingList); } + public GetRankingBadgeResponse getRankingBadgeResponse(String keyword, PeriodType periodType, String date) { + ZSetOperations zSetOperations = redisTemplate.opsForZSet(); + List communityTypeList = CommunityType.getAllExceptTotal(); + List rankByCommunity = new ArrayList<>(); + + for (CommunityType communityType : communityTypeList) { + String key = makeKey(communityType, periodType, date); + Long rank = zSetOperations.reverseRank(key, keyword); + if (rank == null) { + rankByCommunity.add((long) -1); + continue; + } + rankByCommunity.add(rank + 1); + } + + return new GetRankingBadgeResponse(rankByCommunity.get(0), rankByCommunity.get(1), rankByCommunity.get(2)); + } + private static String makeKey(CommunityType communityType, PeriodType periodType, String date) { - String key = communityType.get() + "_"; + String key = communityType.value() + "_"; switch (periodType) { case BY_REAL_TIME -> key += periodType.get(); case BY_DAY -> key += DateUtils.getDate(DateUtils.getLocalDate(date)); @@ -152,13 +171,13 @@ private static List getKeyList(PeriodType periodType, String date) { } private static int getWeight(String key) { - if (key.startsWith(CommunityType.NAVER.get())) { + if (key.startsWith(CommunityType.NAVER.value())) { return RankingWeight.NAVER.get(); } - if (key.startsWith(CommunityType.NATE.get())) { + if (key.startsWith(CommunityType.NATE.value())) { return RankingWeight.NATE.get(); } - if (key.startsWith(CommunityType.ZUM.get())) { + if (key.startsWith(CommunityType.ZUM.value())) { return RankingWeight.ZUM.get(); } return -1; diff --git a/src/main/java/store/itpick/backend/util/UserUtils.java b/src/main/java/store/itpick/backend/util/UserUtils.java index ceb96db..31e6131 100644 --- a/src/main/java/store/itpick/backend/util/UserUtils.java +++ b/src/main/java/store/itpick/backend/util/UserUtils.java @@ -1,7 +1,6 @@ package store.itpick.backend.util; -import org.springframework.beans.factory.annotation.Autowired; -import store.itpick.backend.common.exception.UserException; +import store.itpick.backend.common.exception.AuthException; import store.itpick.backend.model.User; import store.itpick.backend.repository.UserRepository; @@ -14,7 +13,7 @@ public static User getUser(long userId, UserRepository userRepository){ try { return userRepository.getUserByUserId(userId).get(); } catch (NoSuchElementException e) { - throw new UserException(USER_NOT_FOUND); + throw new AuthException(USER_NOT_FOUND); } } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ec6996c..d214b26 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,9 @@ spring: profiles: group: - "local": "localDB, devPort, secret, web-mvc, web-driver" - "dev": "devDB, devPort, secret, web-mvc, web-driver" - "prod": "prodDB, prodPort, secret, web-mvc, web-driver" + "local": "localDB, devPort, secret, web-mvc, web-driver, aws" + "dev": "devDB, devPort, secret, web-mvc, web-driver, aws" + "prod": "prodDB, prodPort, secret, web-mvc, web-driver, aws" data: redis: @@ -165,3 +165,20 @@ spring: web-driver: path: ${WEB_DRIVER_PATH} +--- + +spring: + config: + activate: + on-profile: "aws" + +cloud: + aws: + s3: + bucket: ${AWS_S3_BUCKET_NAME} + stack.auto: false + region.static: ap-northeast-2 + credentials: + accessKey: ${AWS_S3_BUCKET_ACCESS_KEY} + secretKey: ${AWS_S3_BUCKET_SECRET_KEY} +