diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 757f922b..f01b89b8 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -73,4 +73,4 @@ jobs: script_stop: true script: | sudo fuser -k -n tcp 8080 || true - nohup java -Xms256m -Xmx742m -Dspring.profiles.active=dev -jar /home/ubuntu/chooz-dev.jar >> /home/ubuntu/output.log 2>&1 & + nohup java -Xms256m -Xmx742m -Dspring.profiles.active=dev -jar /home/ubuntu/chooz-dev.jar 1>/dev/null 2>&1 & diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml index 4eac696f..f456b834 100644 --- a/.github/workflows/cd-prod.yml +++ b/.github/workflows/cd-prod.yml @@ -72,4 +72,4 @@ jobs: script_stop: true script: | sudo fuser -k -n tcp 8080 || true - nohup java -Xms256m -Xmx742m -Dspring.profiles.active=prod -jar /home/ubuntu/chooz-prod.jar >> /home/ubuntu/output.log 2>&1 & + nohup java -Xms256m -Xmx742m -Dspring.profiles.active=prod -jar /home/ubuntu/chooz-prod.jar 1>/dev/null 2>&1 & diff --git a/server-config b/server-config index ffce8ef1..691d936b 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit ffce8ef11e0fd7453570cb63fa0ada64d46b2c0d +Subproject commit 691d936b31e68ae7516be47904988de069070a51 diff --git a/src/main/java/com/chooz/auth/application/AuthService.java b/src/main/java/com/chooz/auth/application/AuthService.java index 742e8c7e..8ea8d17c 100644 --- a/src/main/java/com/chooz/auth/application/AuthService.java +++ b/src/main/java/com/chooz/auth/application/AuthService.java @@ -8,12 +8,8 @@ import com.chooz.auth.domain.SocialAccount; import com.chooz.auth.domain.SocialAccountRepository; import com.chooz.auth.presentation.dto.TokenResponse; -import com.chooz.common.exception.BadRequestException; -import com.chooz.common.exception.ErrorCode; import com.chooz.user.application.UserService; import com.chooz.user.domain.Role; -import com.chooz.user.domain.User; -import com.chooz.user.domain.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -29,7 +25,7 @@ public class AuthService { private final OAuthService oAuthService; private final SocialAccountRepository socialAccountRepository; private final UserService userService; - private final UserRepository userRepository; + private final WithdrawHandler withdrawHandler; public TokenResponse oauthSignIn(String code, String redirectUri) { OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code, redirectUri); @@ -62,11 +58,7 @@ public void signOut(Long userId) { jwtService.removeToken(userId); } - @Transactional public void withdraw(Long userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - jwtService.removeToken(userId); - userRepository.delete(user); + withdrawHandler.withdraw(userId); } } diff --git a/src/main/java/com/chooz/auth/application/WithdrawHandler.java b/src/main/java/com/chooz/auth/application/WithdrawHandler.java new file mode 100644 index 00000000..4f959d67 --- /dev/null +++ b/src/main/java/com/chooz/auth/application/WithdrawHandler.java @@ -0,0 +1,48 @@ +package com.chooz.auth.application; + +import com.chooz.auth.application.jwt.JwtService; +import com.chooz.auth.application.oauth.OAuthService; +import com.chooz.auth.domain.SocialAccount; +import com.chooz.auth.domain.SocialAccountRepository; +import com.chooz.common.exception.BadRequestException; +import com.chooz.common.exception.ErrorCode; +import com.chooz.image.application.S3Client; +import com.chooz.notification.domain.NotificationRepository; +import com.chooz.post.application.PostCommandService; +import com.chooz.post.persistence.PostJpaRepository; +import com.chooz.user.domain.User; +import com.chooz.user.domain.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class WithdrawHandler { + + private final UserRepository userRepository; + private final JwtService jwtService; + private final SocialAccountRepository socialAccountRepository; + private final PostJpaRepository postRepository; + private final NotificationRepository notificationRepository; + private final PostCommandService postCommandService; + private final OAuthService oAuthService; + private final S3Client s3Client; + + public void withdraw(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + jwtService.removeToken(userId); + notificationRepository.deleteAllByUserId(userId); + postRepository.findAllByUserId(userId) + .forEach(post -> postCommandService.delete(userId, post.getId())); + + socialAccountRepository.findByUserId(userId).ifPresent(socialAccount -> { + socialAccountRepository.deleteByUserId(userId); + oAuthService.withdraw(socialAccount.getSocialId()); + }); + if (!User.DEFAULT_PROFILE_URL.equals(user.getProfileUrl())) { + s3Client.deleteImage(user.getProfileUrl()); + } + userRepository.delete(user); + } +} diff --git a/src/main/java/com/chooz/auth/application/oauth/KakaoOAuthClient.java b/src/main/java/com/chooz/auth/application/oauth/KakaoOAuthClient.java index 990e2f7a..07fbd97f 100644 --- a/src/main/java/com/chooz/auth/application/oauth/KakaoOAuthClient.java +++ b/src/main/java/com/chooz/auth/application/oauth/KakaoOAuthClient.java @@ -1,6 +1,7 @@ package com.chooz.auth.application.oauth; import com.chooz.auth.application.oauth.dto.KakaoAuthResponse; +import com.chooz.auth.application.oauth.dto.KakaoUnlinkResponse; import com.chooz.auth.application.oauth.dto.KakaoUserInfoResponse; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestHeader; @@ -18,4 +19,10 @@ public interface KakaoOAuthClient { @GetExchange("https://kapi.kakao.com/v2/user/me") KakaoUserInfoResponse fetchUserInfo(@RequestHeader(name = AUTHORIZATION) String bearerToken); + + @PostExchange(url = "https://kapi.kakao.com/v1/user/unlink", contentType = APPLICATION_FORM_URLENCODED_VALUE) + KakaoUnlinkResponse unlink( + @RequestHeader(name = AUTHORIZATION) String bearerToken, + @RequestParam("params") MultiValueMap params + ); } diff --git a/src/main/java/com/chooz/auth/application/oauth/OAuthService.java b/src/main/java/com/chooz/auth/application/oauth/OAuthService.java index a76d24b2..b2bc6b28 100644 --- a/src/main/java/com/chooz/auth/application/oauth/OAuthService.java +++ b/src/main/java/com/chooz/auth/application/oauth/OAuthService.java @@ -17,6 +17,7 @@ public class OAuthService { private static final String BEARER = "Bearer "; + private static final String KAKAO_AK = "KakaoAK "; private final KakaoOAuthConfig kakaoOAuthConfig; private final KakaoOAuthClient kakaoOAuthClient; @@ -43,4 +44,22 @@ private MultiValueMap tokenRequestParams(String authCode, String params.add("client_secret", kakaoOAuthConfig.clientSecret()); return params; } + + public void withdraw(String socialId) { + try { + kakaoOAuthClient.unlink( + KAKAO_AK + kakaoOAuthConfig.adminKey(), + unlinkRequestParams(socialId) + ); + } catch (Exception e) { + log.warn("회원 탈퇴 실패", e); + } + } + + private MultiValueMap unlinkRequestParams(String socialId) { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("target_id_type", "user_id"); + params.add("target_id", socialId); + return params; + } } diff --git a/src/main/java/com/chooz/auth/application/oauth/dto/KakaoUnlinkResponse.java b/src/main/java/com/chooz/auth/application/oauth/dto/KakaoUnlinkResponse.java new file mode 100644 index 00000000..e11a3afa --- /dev/null +++ b/src/main/java/com/chooz/auth/application/oauth/dto/KakaoUnlinkResponse.java @@ -0,0 +1,4 @@ +package com.chooz.auth.application.oauth.dto; + +public record KakaoUnlinkResponse(String id) { +} diff --git a/src/main/java/com/chooz/auth/domain/SocialAccountRepository.java b/src/main/java/com/chooz/auth/domain/SocialAccountRepository.java index 10f763f7..1311dc76 100644 --- a/src/main/java/com/chooz/auth/domain/SocialAccountRepository.java +++ b/src/main/java/com/chooz/auth/domain/SocialAccountRepository.java @@ -3,10 +3,15 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface SocialAccountRepository extends JpaRepository { Optional findBySocialIdAndProvider(String socialId, Provider provider); + + void deleteByUserId(Long userId); + + Optional findByUserId(Long userId); } diff --git a/src/main/java/com/chooz/comment/application/CommentCommandService.java b/src/main/java/com/chooz/comment/application/CommentCommandService.java index 3308bcb1..efc8fda2 100644 --- a/src/main/java/com/chooz/comment/application/CommentCommandService.java +++ b/src/main/java/com/chooz/comment/application/CommentCommandService.java @@ -60,4 +60,9 @@ public void deleteComment(Long postId, Long commentId, Long userId) { commentRepository.delete(comment); eventPublisher.publish(DeleteEvent.of(comment.getId(), comment.getClass().getSimpleName().toUpperCase())); } + + public void deleteComments(Long postId) { + commentLikeCommandService.deleteCommentLikeByCommentId(postId); + commentRepository.deleteAllByPostId(postId); + } } diff --git a/src/main/java/com/chooz/comment/domain/CommentRepository.java b/src/main/java/com/chooz/comment/domain/CommentRepository.java index 500edb2e..fb9e645d 100644 --- a/src/main/java/com/chooz/comment/domain/CommentRepository.java +++ b/src/main/java/com/chooz/comment/domain/CommentRepository.java @@ -31,4 +31,5 @@ Slice findByPostId( List findByPostIdAndDeletedFalse(@NotNull Long postId); + void deleteAllByPostId(Long postId); } diff --git a/src/main/java/com/chooz/common/config/KakaoOAuthConfig.java b/src/main/java/com/chooz/common/config/KakaoOAuthConfig.java index 3b213aca..b06d13bb 100644 --- a/src/main/java/com/chooz/common/config/KakaoOAuthConfig.java +++ b/src/main/java/com/chooz/common/config/KakaoOAuthConfig.java @@ -7,6 +7,7 @@ public record KakaoOAuthConfig( String authorizationUri, String clientId, String clientSecret, + String adminKey, String[] scope, String userInfoUri ) { diff --git a/src/main/java/com/chooz/common/dev/DataInitConfig.java b/src/main/java/com/chooz/common/dev/DataInitConfig.java index f6418ca2..35e6da5b 100644 --- a/src/main/java/com/chooz/common/dev/DataInitConfig.java +++ b/src/main/java/com/chooz/common/dev/DataInitConfig.java @@ -12,7 +12,7 @@ public class DataInitConfig { private final DataInitializer dataInitializer; - @PostConstruct +// @PostConstruct public void init() { dataInitializer.init(); } diff --git a/src/main/java/com/chooz/common/exception/ErrorCode.java b/src/main/java/com/chooz/common/exception/ErrorCode.java index 37ec126b..9394a44c 100644 --- a/src/main/java/com/chooz/common/exception/ErrorCode.java +++ b/src/main/java/com/chooz/common/exception/ErrorCode.java @@ -7,74 +7,75 @@ @RequiredArgsConstructor public enum ErrorCode { //400 - USER_NOT_FOUND("존재하지 않는 유저입니다."), - INVALID_ARGUMENT("잘못된 파라미터 요청입니다."), - REFRESH_TOKEN_MISMATCHED("리프레시 토큰이 불일치합니다."), - REFRESH_TOKEN_NOT_FOUND("리프레시 토큰을 찾을 수 없습니다."), - INVALID_REFRESH_TOKEN_HEADER("잘못된 리프레시 토큰 헤더입니다."), - MISSING_FILE_EXTENSION("확장자가 누락됐습니다."), - UNSUPPORTED_IMAGE_EXTENSION("지원하지 않는 확장자입니다."), - EXCEED_MAX_FILE_SIZE("파일 크기가 초과했습니다."), - POST_NOT_FOUND("존재하지 않는 게시글입니다."), - DESCRIPTION_LENGTH_EXCEEDED("게시글 설명 길이가 초과했습니다."), - TITLE_IS_REQUIRED("게시글 제목은 필수입니다."), - TITLE_LENGTH_EXCEEDED("게시글 제목 길이가 초과했습니다."), - INVALID_POLL_CHOICE_COUNT("투표 선택지 개수가 범위를 벗어났습니다."), - POLL_CHOICE_TITLE_LENGTH_EXCEEDED("투표 선택지 제목 길이가 초과했습니다."), - NOT_POST_AUTHOR("게시글 작성자가 아닙니다."), - POST_ALREADY_CLOSED("이미 마감된 게시글입니다."), - FILE_NAME_TOO_LONG("파일 이름이 너무 깁니다."), - ACCESS_DENIED_VOTE_STATUS("투표 현황 조회 권한이 없습니다."), - COMMENT_NOT_FOUND("존재하지 않는 댓글입니다."), - VOTE_NOT_FOUND("존재하지 않는 투표입니다."), - NOT_VOTER("투표자가 아닙니다."), - CLOSED_AT_REQUIRED("마감 시간 설정이 필요합니다."), - MAX_VOTER_COUNT_REQUIRED("최대 투표자 수 설정이 필요합니다."), - INVALID_VOTER_CLOSE_OPTION("잘못된 최대 투표자 마감 설정입니다."), - INVALID_DATE_CLOSE_OPTION("잘못된 마감 시간 설정입니다"), - INVALID_SELF_CLOSE_OPTION("잘못된 자체 마감 옵션입니다."), - INVALID_CLOSE_OPTION("잘못된 마감 옵션입니다."), - THUMBNAIL_NOT_FOUND("썸네일을 찾을 수 없습니다."), - CLOSE_DATE_OVER("마감 시간이 지났습니다."), - EXCEED_MAX_VOTER_COUNT("투표 참여자 수가 초과했습니다."), - CLOSE_COMMENT_ACTIVE("댓글 기능이 비활성화 되어 있습니다."), - COMMENT_NOT_BELONG_TO_POST("게시글에 속한 댓글이 아닙니다."), - NOT_COMMENT_AUTHOR("댓글의 작성자가 아닙니다."), - COMMENT_LENGTH_OVER("댓글 길이가 200글자를 초과하였습니다."), - COMMENT_LIKE_NOT_FOUND("댓글좋아요를 찾을 수 없습니다."), - NOT_COMMENT_LIKE_AUTHOR("댓글 좋아요를 누른 유저가 아닙니다."), - SINGLE_POLL_ALLOWS_MAXIMUM_ONE_CHOICE("단일 투표인 경우 최대 하나의 선택지만 투표 가능"), - DUPLICATE_POLL_CHOICE("복수 투표의 경우 중복된 선택지가 있으면 안 됨"), - NOT_POST_POLL_CHOICE_ID("게시글의 투표 선택지가 아님"), - ONLY_SELF_CAN_CLOSE("작성자 마감의 경우, SELF 마감 방식만이 마감 가능합니다."), - INVALID_ONBOARDING_STEP("유효하지 않은 온보딩 단계."), - NICKNAME_LENGTH_EXCEEDED("닉네임 길이 초과"), - NOTIFICATION_NOT_FOUND("존재하지 않는 알림 입니다."), - POST_NOT_REVEALABLE("공개 불가능한 게시글입니다."), + USER_NOT_FOUND("회원정보를 찾을 수 없어요.", null), + INVALID_ARGUMENT("요청이 잘못되었어요.", "다시 시도해주세요."), + REFRESH_TOKEN_MISMATCHED("로그인 정보가 만료됐어요.", "다시 로그인 해주세요."), + REFRESH_TOKEN_NOT_FOUND("로그인 세션이 만료됐어요.", "다시 로그인 해주세요."), + INVALID_REFRESH_TOKEN_HEADER("로그인 정보가 올바르지 않아요.", "다시 로그인 해주세요."), + MISSING_FILE_EXTENSION("업로드 파일 형식을 확인해주세요.", null), + UNSUPPORTED_IMAGE_EXTENSION("업로드할 수 있는 확장자가 아니에요.", null), + EXCEED_MAX_FILE_SIZE("파일 용량이 너무 커요.", null), + POST_NOT_FOUND("게시글을 찾을 수 없어요.", null), + DESCRIPTION_LENGTH_EXCEEDED("설명은 100자 이내로 입력해주세요.", null), + TITLE_IS_REQUIRED("제목을 입력해주세요.", null), + TITLE_LENGTH_EXCEEDED("제목은 50자 이내로 입력해주세요.", null), + INVALID_POLL_CHOICE_COUNT("선택지는 최소 2개, 최대 10개까지 등록할 수 있어요.", null), + POLL_CHOICE_TITLE_LENGTH_EXCEEDED("선택지 이름은 10자 이내로 입력해주세요.", null), + NOT_POST_AUTHOR("본인이 작성한 게시글만 수정하거나 삭제할 수 있어요.", null), + POST_ALREADY_CLOSED("이미 마감된 투표예요.", "결과를 확인해보세요."), + FILE_NAME_TOO_LONG("파일 이름이 너무 길어요.", "짧게 수정해주세요."), + ACCESS_DENIED_VOTE_STATUS("아직 투표 현황을 조회할 수 없어요.", null), + COMMENT_NOT_FOUND("댓글을 찾을 수 없어요.", null), + VOTE_NOT_FOUND("투표를 찾을 수 없어요.", null), + NOT_VOTER("투표에 참여하지 않았어요.", null), + CLOSED_AT_REQUIRED("마감 시간을 설정해주세요.", null), + MAX_VOTER_COUNT_REQUIRED("참여 인원 제한을 설정해주세요.", null), + INVALID_VOTER_CLOSE_OPTION("투표자 수 마감 설정이 올바르지 않아요.", null), + INVALID_DATE_CLOSE_OPTION("마감시간이 올바르지 않아요.", "다시 선택해주세요."), + INVALID_SELF_CLOSE_OPTION("마감 옵션이 잘못됐어요.", "다시 선택해주세요."), + INVALID_CLOSE_OPTION("마감방식이 올바르지 않아요.", null), + THUMBNAIL_NOT_FOUND("미리보기 이미지를 불러올 수 없어요.", null), + CLOSE_DATE_OVER("이미 마감된 투표에요.", null), + EXCEED_MAX_VOTER_COUNT("이미 투표인원이 가득 찼어요.", null), + CLOSE_COMMENT_ACTIVE("현재 댓글 기능이 꺼져있어요.", null), + COMMENT_NOT_BELONG_TO_POST("댓글 정보가 올바르지 않아요.", null), + NOT_COMMENT_AUTHOR("본인이 작성한 댓글만 수정할 수 있어요.", null), + COMMENT_LENGTH_OVER("댓글은 200자 이내로 작성해주세요.", null), + COMMENT_LIKE_NOT_FOUND("좋아요 정보를 찾을 수 없어요.", null), + NOT_COMMENT_LIKE_AUTHOR("본인이 누른 좋아요만 취소할 수 있어요.", null), + SINGLE_POLL_ALLOWS_MAXIMUM_ONE_CHOICE("단일 투표는 한가지 선택지만 가능해요.", null), + DUPLICATE_POLL_CHOICE("같은 선택지는 중복으로 투표할 수 없어요.", null), + NOT_POST_POLL_CHOICE_ID("올바르지 않은 투표항목이에요.", null), + ONLY_SELF_CAN_CLOSE("직접 마감은 작성자만 할 수 있어요.", null), + INVALID_ONBOARDING_STEP("잘못된 온보딩 단계로 이동했어요.", null), + NICKNAME_LENGTH_EXCEEDED("닉네임은 15자 이내로 입력해주세요.", null), + NOTIFICATION_NOT_FOUND("알림을 찾을 수 없어요.", null), + POST_NOT_REVEALABLE("이 게시글은 특정 사용자에게만 공개돼 있어요.", null), //401 - EXPIRED_TOKEN("토큰이 만료됐습니다."), - INVALID_TOKEN("유효하지 않은 토큰입니다."), - INVALID_AUTH_HEADER("잘못된 인증 헤더입니다."), + EXPIRED_TOKEN("로그인 세션이 만료됐어요.", "다시 로그인 해주세요."), + INVALID_TOKEN("로그인 정보가 유효하지 않아요.", "다시 로그인 해주세요."), + INVALID_AUTH_HEADER("로그인 정보가 손상됐어요.", "다시 로그인 해주세요."), //403 - FORBIDDEN("권한 없음"), + FORBIDDEN("접근 권한이 없어요.", "로그인 후 다시 시도해주세요."), //404 - NOT_FOUND("리소스를 찾을 수 없음"), + NOT_FOUND("페이지를 찾을 수 없어요.", "주소를 다시 확인 해주세요."), //500 - INTERNAL_SERVER_ERROR("서버 내부 오류 발생"), - INVALID_INPUT_VALUE("잘못된 입력 값입니다."), - SOCIAL_AUTHENTICATION_FAILED("소셜 로그인이 실패했습니다."), - POLL_CHOICE_NAME_GENERATOR_INDEX_OUT_OF_BOUND("이미지 이름 생성기 인덱스 초과"), - IMAGE_FILE_NOT_FOUND("존재하지 않는 이미지입니다."), - POLL_CHOICE_NOT_FOUND("투표 선택지가 없습니다."), - SHARE_URL_ALREADY_EXISTS("공유 URL이 이미 존재합니다."), + INTERNAL_SERVER_ERROR("잠시 문제가 발생했어요.", "잠시 후 다시 시도해주세요."), + INVALID_INPUT_VALUE("입력 값을 다시 확인해주세요.", null), + SOCIAL_AUTHENTICATION_FAILED("소셜 로그인이 실패했어요.", null), + POLL_CHOICE_NAME_GENERATOR_INDEX_OUT_OF_BOUND("이미지 등록 중 오류가 발생했어요.", null), + IMAGE_FILE_NOT_FOUND("이미지를 찾을 수 없어요.", null), + POLL_CHOICE_NOT_FOUND("투표항목을 찾을 수 없어요.", null), + SHARE_URL_ALREADY_EXISTS("이미 공유된 링크예요.", null), //503 - SERVICE_UNAVAILABLE("서비스 이용 불가"), + SERVICE_UNAVAILABLE("잠시 점검 중이에요.", "조금만 기다려주세요.🙏"), ; private final String message; + private final String subMessage; } diff --git a/src/main/java/com/chooz/common/exception/ErrorResponse.java b/src/main/java/com/chooz/common/exception/ErrorResponse.java index 5b527892..b6a8ed69 100644 --- a/src/main/java/com/chooz/common/exception/ErrorResponse.java +++ b/src/main/java/com/chooz/common/exception/ErrorResponse.java @@ -1,8 +1,8 @@ package com.chooz.common.exception; -public record ErrorResponse(ErrorCode errorCode, String message) { +public record ErrorResponse(ErrorCode errorCode, String message, String subMessage) { public static ErrorResponse of(ErrorCode errorCode) { - return new ErrorResponse(errorCode, errorCode.getMessage()); + return new ErrorResponse(errorCode, errorCode.getMessage(), errorCode.getSubMessage()); } } diff --git a/src/main/java/com/chooz/image/application/ImageService.java b/src/main/java/com/chooz/image/application/ImageService.java index a0e156b5..b246d3c1 100644 --- a/src/main/java/com/chooz/image/application/ImageService.java +++ b/src/main/java/com/chooz/image/application/ImageService.java @@ -36,4 +36,8 @@ private String getSignedGetUrl(String filePath) { URI domain = URI.create(imageProperties.endpoint()); return domain.resolve(filePath).toString(); } + + public void deleteImage(String assetUrl) { + s3Client.deleteImage(assetUrl); + } } diff --git a/src/main/java/com/chooz/image/application/S3Client.java b/src/main/java/com/chooz/image/application/S3Client.java index dcfd33be..70db5dc0 100644 --- a/src/main/java/com/chooz/image/application/S3Client.java +++ b/src/main/java/com/chooz/image/application/S3Client.java @@ -1,8 +1,9 @@ package com.chooz.image.application; import com.chooz.image.application.dto.PresignedUrlRequestDto; -import com.chooz.image.presentation.dto.PresignedUrlRequest; public interface S3Client { String getPresignedPutUrl(PresignedUrlRequestDto presignedUrlRequestDto); + + void deleteImage(String assetUrl); } diff --git a/src/main/java/com/chooz/image/infrastructure/AwsS3Client.java b/src/main/java/com/chooz/image/infrastructure/AwsS3Client.java index fa252094..0b74deaf 100644 --- a/src/main/java/com/chooz/image/infrastructure/AwsS3Client.java +++ b/src/main/java/com/chooz/image/infrastructure/AwsS3Client.java @@ -4,6 +4,7 @@ import com.chooz.image.application.dto.PresignedUrlRequestDto; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; @@ -17,13 +18,16 @@ public class AwsS3Client implements S3Client { private final String bucket; private final S3Presigner s3Presigner; + private final software.amazon.awssdk.services.s3.S3Client s3Client; public AwsS3Client( @Value("${spring.cloud.aws.s3.bucket}") String bucket, - S3Presigner s3Presigner + S3Presigner s3Presigner, + software.amazon.awssdk.services.s3.S3Client s3Client ) { this.bucket = bucket; this.s3Presigner = s3Presigner; + this.s3Client = s3Client; } @Override @@ -45,4 +49,13 @@ private PutObjectPresignRequest buildPresignedRequest(PresignedUrlRequestDto dto .putObjectRequest(requestBuilder.build()) .build(); } + + @Override + public void deleteImage(String assetUrl) { + DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() + .bucket(bucket) + .key(assetUrl) + .build(); + s3Client.deleteObject(deleteObjectRequest); + } } diff --git a/src/main/java/com/chooz/notification/domain/NotificationRepository.java b/src/main/java/com/chooz/notification/domain/NotificationRepository.java index 968ca92d..c99413cd 100644 --- a/src/main/java/com/chooz/notification/domain/NotificationRepository.java +++ b/src/main/java/com/chooz/notification/domain/NotificationRepository.java @@ -9,4 +9,6 @@ public interface NotificationRepository { Optional findNotificationById(Long id); boolean existsByReceiverIdAndIsReadFalseAndDeletedFalse(Long userId); List findByTargetIdAndType(Long targetId, TargetType targetType); + + void deleteAllByUserId(Long userId); } diff --git a/src/main/java/com/chooz/notification/persistence/NotificationJpaRepository.java b/src/main/java/com/chooz/notification/persistence/NotificationJpaRepository.java index 42d24a75..c6d0ee7f 100644 --- a/src/main/java/com/chooz/notification/persistence/NotificationJpaRepository.java +++ b/src/main/java/com/chooz/notification/persistence/NotificationJpaRepository.java @@ -23,4 +23,6 @@ public interface NotificationJpaRepository extends JpaRepository findByTargetIdAndType(@Param("targetId") Long targetId, @Param("targetType") TargetType targetType); + + void deleteAllByReceiverId(Long receiverId); } diff --git a/src/main/java/com/chooz/notification/persistence/NotificationRepositoryImpl.java b/src/main/java/com/chooz/notification/persistence/NotificationRepositoryImpl.java index 9fcbbffc..906ba747 100644 --- a/src/main/java/com/chooz/notification/persistence/NotificationRepositoryImpl.java +++ b/src/main/java/com/chooz/notification/persistence/NotificationRepositoryImpl.java @@ -40,4 +40,9 @@ public boolean existsByReceiverIdAndIsReadFalseAndDeletedFalse(Long userId) { public List findByTargetIdAndType(Long targetId, TargetType targetType) { return notificationJpaRepository.findByTargetIdAndType(targetId, targetType); } + + @Override + public void deleteAllByUserId(Long userId) { + notificationJpaRepository.deleteAllByReceiverId(userId); + } } \ No newline at end of file diff --git a/src/main/java/com/chooz/post/application/PostCommandService.java b/src/main/java/com/chooz/post/application/PostCommandService.java index 9c0469c0..b00232cf 100644 --- a/src/main/java/com/chooz/post/application/PostCommandService.java +++ b/src/main/java/com/chooz/post/application/PostCommandService.java @@ -1,20 +1,21 @@ package com.chooz.post.application; +import com.chooz.comment.application.CommentCommandService; import com.chooz.common.event.DeleteEvent; import com.chooz.common.event.EventPublisher; import com.chooz.common.exception.BadRequestException; import com.chooz.common.exception.ErrorCode; +import com.chooz.image.application.ImageService; import com.chooz.post.application.dto.PostClosedNotificationEvent; import com.chooz.post.domain.CloseOption; +import com.chooz.post.domain.PollChoice; import com.chooz.post.domain.PollOption; import com.chooz.post.domain.Post; -import com.chooz.post.domain.PollChoice; import com.chooz.post.domain.PostRepository; import com.chooz.post.presentation.dto.CreatePostRequest; import com.chooz.post.presentation.dto.CreatePostResponse; import com.chooz.post.presentation.dto.UpdatePostRequest; -import com.chooz.thumbnail.domain.Thumbnail; -import com.chooz.thumbnail.domain.ThumbnailRepository; +import com.chooz.vote.application.VoteService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,13 +31,14 @@ public class PostCommandService { private final PostRepository postRepository; private final ShareUrlService shareUrlService; - private final ThumbnailRepository thumbnailRepository; private final PostValidator postValidator; + private final CommentCommandService commentCommandService; + private final VoteService voteService; private final EventPublisher eventPublisher; + private final ImageService imageService; public CreatePostResponse create(Long userId, CreatePostRequest request) { Post post = createPost(userId, request); - savePostThumbnail(post); return new CreatePostResponse(post.getId(), post.getShareUrl()); } @@ -73,18 +75,14 @@ private List createPollChoices(CreatePostRequest request) { .collect(Collectors.toList()); } - private void savePostThumbnail(Post post) { - PollChoice thumbnailPollChoice = post.getPollChoices().getFirst(); - thumbnailRepository.save( - Thumbnail.create(post.getId(), thumbnailPollChoice.getId(), thumbnailPollChoice.getImageUrl()) - ); - } - @Transactional public void delete(Long userId, Long postId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); - post.delete(userId); + voteService.delete(postId); + commentCommandService.deleteComments(postId); + imageService.deleteImage(post.getImageUrl()); + postRepository.delete(postId); eventPublisher.publish(DeleteEvent.of(post.getId(), post.getClass().getSimpleName().toUpperCase())); } diff --git a/src/main/java/com/chooz/post/domain/PostRepository.java b/src/main/java/com/chooz/post/domain/PostRepository.java index 7b370280..96ff8c5f 100644 --- a/src/main/java/com/chooz/post/domain/PostRepository.java +++ b/src/main/java/com/chooz/post/domain/PostRepository.java @@ -35,4 +35,8 @@ public interface PostRepository { Slice findVotedPostsWithVoteCount(Long userId, Long authorId, Long postId, Pageable pageable); Optional findByIdAndUserId(Long postId, Long userId); + + void deleteAllByUserId(Long userId); + + void delete(Long postId); } diff --git a/src/main/java/com/chooz/post/persistence/PostJpaRepository.java b/src/main/java/com/chooz/post/persistence/PostJpaRepository.java index 0c9f6a48..0377b5c8 100644 --- a/src/main/java/com/chooz/post/persistence/PostJpaRepository.java +++ b/src/main/java/com/chooz/post/persistence/PostJpaRepository.java @@ -72,4 +72,8 @@ public interface PostJpaRepository extends JpaRepository { Optional findCommentActiveByPostId(@Param("postId") Long postId); Optional findByIdAndUserIdAndDeletedFalse(Long postId, Long userId); + + void deleteAllByUserId(Long userId); + + List findAllByUserId(Long userId); } diff --git a/src/main/java/com/chooz/post/persistence/PostRepositoryImpl.java b/src/main/java/com/chooz/post/persistence/PostRepositoryImpl.java index 6432f971..b58c31e3 100644 --- a/src/main/java/com/chooz/post/persistence/PostRepositoryImpl.java +++ b/src/main/java/com/chooz/post/persistence/PostRepositoryImpl.java @@ -1,10 +1,10 @@ package com.chooz.post.persistence; +import com.chooz.post.application.dto.FeedDto; import com.chooz.post.application.dto.PostWithVoteCount; import com.chooz.post.domain.CommentActive; import com.chooz.post.domain.Post; import com.chooz.post.domain.PostRepository; -import com.chooz.post.application.dto.FeedDto; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -79,4 +79,14 @@ public Slice findVotedPostsWithVoteCount(Long userId, Long au public Optional findByIdAndUserId(Long postId, Long userId) { return postJpaRepository.findByIdAndUserIdAndDeletedFalse(postId, userId); } + + @Override + public void deleteAllByUserId(Long userId) { + postJpaRepository.deleteAllByUserId(userId); + } + + @Override + public void delete(Long postId) { + postJpaRepository.deleteById(postId); + } } diff --git a/src/main/java/com/chooz/thumbnail/domain/Thumbnail.java b/src/main/java/com/chooz/thumbnail/domain/Thumbnail.java deleted file mode 100644 index 3cb02d28..00000000 --- a/src/main/java/com/chooz/thumbnail/domain/Thumbnail.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.chooz.thumbnail.domain; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import static com.chooz.common.util.Validator.validateNull; - -@Getter -@Entity -@Table(name = "thumbnails") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Thumbnail { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private Long postId; - - private Long pollChoiceId; - - private String thumbnailUrl; - - @Builder - public Thumbnail(Long id, Long postId, Long pollChoiceId, String thumbnailUrl) { - validateNull(postId, pollChoiceId, thumbnailUrl); - this.id = id; - this.postId = postId; - this.pollChoiceId = pollChoiceId; - this.thumbnailUrl = thumbnailUrl; - } - - public static Thumbnail create(Long postId, Long pollChoiceId, String thumbnailUrl) { - return new Thumbnail(null, postId, pollChoiceId, thumbnailUrl); - } - - public boolean isThumbnailOf(Long postId) { - return this.postId.equals(postId); - } -} diff --git a/src/main/java/com/chooz/thumbnail/domain/ThumbnailRepository.java b/src/main/java/com/chooz/thumbnail/domain/ThumbnailRepository.java deleted file mode 100644 index b0e59b1d..00000000 --- a/src/main/java/com/chooz/thumbnail/domain/ThumbnailRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.chooz.thumbnail.domain; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -@Repository -public interface ThumbnailRepository extends JpaRepository { - - Optional findByPostId(Long postId); - - List findByPostIdIn(Collection postIds); -} diff --git a/src/main/java/com/chooz/user/domain/User.java b/src/main/java/com/chooz/user/domain/User.java index 5b75fae9..2295bea0 100644 --- a/src/main/java/com/chooz/user/domain/User.java +++ b/src/main/java/com/chooz/user/domain/User.java @@ -25,7 +25,7 @@ @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) public class User extends BaseEntity { - private static final String DEFAULT_PROFILE_URL = "https://cdn.chooz.site/default_profile.png"; + public static final String DEFAULT_PROFILE_URL = "https://cdn.chooz.site/default_profile.png"; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/chooz/vote/application/VoteService.java b/src/main/java/com/chooz/vote/application/VoteService.java index 06fb4984..665a09d9 100644 --- a/src/main/java/com/chooz/vote/application/VoteService.java +++ b/src/main/java/com/chooz/vote/application/VoteService.java @@ -49,4 +49,8 @@ public List findVoteResult(Long userId, Long postId) { return voteResultReader.getVoteResult(totalVoteList, post); } + + public void delete(Long postId) { + voteRepository.deleteAllByPostId(postId); + } } diff --git a/src/main/java/com/chooz/vote/domain/VoteRepository.java b/src/main/java/com/chooz/vote/domain/VoteRepository.java index ecef0f16..bbc2351c 100644 --- a/src/main/java/com/chooz/vote/domain/VoteRepository.java +++ b/src/main/java/com/chooz/vote/domain/VoteRepository.java @@ -14,4 +14,5 @@ public interface VoteRepository { long countVoterByPostId(Long postId); + void deleteAllByPostId(Long postId); } diff --git a/src/main/java/com/chooz/vote/persistence/VoteJpaRepository.java b/src/main/java/com/chooz/vote/persistence/VoteJpaRepository.java index e44a0a1e..857b9861 100644 --- a/src/main/java/com/chooz/vote/persistence/VoteJpaRepository.java +++ b/src/main/java/com/chooz/vote/persistence/VoteJpaRepository.java @@ -2,8 +2,6 @@ import com.chooz.vote.domain.Vote; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @@ -16,4 +14,5 @@ public interface VoteJpaRepository extends JpaRepository { List findByPostIdAndDeletedFalse(Long id); + void deleteAllByPostId(Long postId); } diff --git a/src/main/java/com/chooz/vote/persistence/VoteRepositoryImpl.java b/src/main/java/com/chooz/vote/persistence/VoteRepositoryImpl.java index 90afda0e..2b2c9949 100644 --- a/src/main/java/com/chooz/vote/persistence/VoteRepositoryImpl.java +++ b/src/main/java/com/chooz/vote/persistence/VoteRepositoryImpl.java @@ -38,4 +38,9 @@ public List findByPostIdAndDeletedFalse(Long id) { public long countVoterByPostId(Long postId) { return voteQueryDslRepository.countVoterByPostId(postId); } + + @Override + public void deleteAllByPostId(Long postId) { + voteRepository.deleteAllByPostId(postId); + } } diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..99234294 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,47 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + logs/chooz.log + + logs/chooz-%d{yyyy-MM-dd}.%i.log + 10MB + 7 + 500MB + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/com/chooz/comment/application/CommentQueryServiceTest.java b/src/test/java/com/chooz/comment/application/CommentQueryServiceTest.java index 6960b375..e4d5ce9b 100644 --- a/src/test/java/com/chooz/comment/application/CommentQueryServiceTest.java +++ b/src/test/java/com/chooz/comment/application/CommentQueryServiceTest.java @@ -6,6 +6,7 @@ import com.chooz.commentLike.domain.CommentLike; import com.chooz.commentLike.domain.CommentLikeRepository; import com.chooz.common.exception.BadRequestException; +import com.chooz.common.exception.ErrorCode; import com.chooz.post.domain.CommentActive; import com.chooz.post.domain.PollOption; import com.chooz.post.domain.Post; @@ -133,7 +134,7 @@ void findCommentsCloseCommentActive() { // when & then assertThatThrownBy(() -> commentQueryService.findComments(post.getId(), user.getId(), null, 10)) .isInstanceOf(BadRequestException.class) - .hasMessageContaining("댓글 기능이 비활성화 되어 있습니다."); + .hasMessageContaining(ErrorCode.CLOSE_COMMENT_ACTIVE.getMessage()); } diff --git a/src/test/java/com/chooz/post/application/PostCommandServiceTest.java b/src/test/java/com/chooz/post/application/PostCommandServiceTest.java index 620c259b..fec7d522 100644 --- a/src/test/java/com/chooz/post/application/PostCommandServiceTest.java +++ b/src/test/java/com/chooz/post/application/PostCommandServiceTest.java @@ -13,8 +13,6 @@ import com.chooz.support.fixture.PostFixture; import com.chooz.support.fixture.UserFixture; import com.chooz.support.fixture.VoteFixture; -import com.chooz.thumbnail.domain.Thumbnail; -import com.chooz.thumbnail.domain.ThumbnailRepository; import com.chooz.user.domain.User; import com.chooz.user.domain.UserRepository; import com.chooz.vote.domain.VoteRepository; @@ -46,9 +44,6 @@ public class PostCommandServiceTest extends IntegrationTest { @MockitoBean ShareUrlService shareUrlService; - @Autowired - ThumbnailRepository thumbnailRepository; - @Autowired VoteRepository voteRepository; @@ -76,7 +71,6 @@ void create() throws Exception { //then Post post = postRepository.findById(response.postId()).get(); - Thumbnail thumbnail = thumbnailRepository.findByPostId(post.getId()).get(); List pollChoices = post.getPollChoices(); assertAll( () -> assertThat(post.getDescription()).isEqualTo("description"), @@ -89,11 +83,7 @@ void create() throws Exception { () -> assertThat(pollChoices.get(0).getImageUrl()).isEqualTo("http://image1.com"), () -> assertThat(pollChoices.get(0).getTitle()).isEqualTo("title1"), () -> assertThat(pollChoices.get(1).getImageUrl()).isEqualTo("http://image2.com"), - () -> assertThat(pollChoices.get(1).getTitle()).isEqualTo("title2"), - - () -> assertThat(thumbnail.getThumbnailUrl()).isEqualTo("http://image1.com"), - () -> assertThat(thumbnail.getPostId()).isEqualTo(post.getId()), - () -> assertThat(thumbnail.getPollChoiceId()).isEqualTo(pollChoices.get(0).getId()) + () -> assertThat(pollChoices.get(1).getTitle()).isEqualTo("title2") ); } diff --git a/src/test/java/com/chooz/post/application/PostQueryServiceTest.java b/src/test/java/com/chooz/post/application/PostQueryServiceTest.java index f6791a5e..b189b3a5 100644 --- a/src/test/java/com/chooz/post/application/PostQueryServiceTest.java +++ b/src/test/java/com/chooz/post/application/PostQueryServiceTest.java @@ -19,7 +19,6 @@ import com.chooz.support.fixture.PostFixture; import com.chooz.support.fixture.UserFixture; import com.chooz.support.fixture.VoteFixture; -import com.chooz.thumbnail.domain.ThumbnailRepository; import com.chooz.user.domain.User; import com.chooz.user.domain.UserRepository; import com.chooz.vote.application.VoteService; @@ -35,7 +34,6 @@ import static com.chooz.support.fixture.CommentFixture.createDefaultComment; import static com.chooz.support.fixture.PostFixture.createDefaultPost; import static com.chooz.support.fixture.PostFixture.createPostBuilder; -import static com.chooz.support.fixture.ThumbnailFixture.createDefaultThumbnail; import static com.chooz.support.fixture.UserFixture.createDefaultUser; import static com.chooz.support.fixture.UserFixture.createUserBuilder; import static com.chooz.support.fixture.VoteFixture.createDefaultVote; @@ -61,8 +59,6 @@ class PostQueryServiceTest extends IntegrationTest { @Autowired CommentRepository commentRepository; - @Autowired - ThumbnailRepository thumbnailRepository; @Autowired private VoteService voteService; @@ -476,7 +472,6 @@ private List createPosts(User user, int size) { for (int i = 0; i < size; i++) { Post post = postRepository.save(createDefaultPost(user.getId())); posts.add(post); - thumbnailRepository.save(createDefaultThumbnail(post.getId(), post.getPollChoices().get(0).getId())); } return posts; } diff --git a/src/test/java/com/chooz/support/IntegrationTest.java b/src/test/java/com/chooz/support/IntegrationTest.java index f20a8cbf..db1ffae4 100644 --- a/src/test/java/com/chooz/support/IntegrationTest.java +++ b/src/test/java/com/chooz/support/IntegrationTest.java @@ -1,11 +1,28 @@ package com.chooz.support; +import com.chooz.image.application.S3Client; +import com.chooz.support.mock.AwsS3Mock; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; +@Import(IntegrationTest.IntegrationTestConfig.class) @ActiveProfiles("test") @Transactional @SpringBootTest public abstract class IntegrationTest { + + @Configuration + public static class IntegrationTestConfig { + + @Bean + @Primary + public S3Client s3ClientMock() { + return new AwsS3Mock(); + } + } } diff --git a/src/test/java/com/chooz/support/fixture/ThumbnailFixture.java b/src/test/java/com/chooz/support/fixture/ThumbnailFixture.java deleted file mode 100644 index 30b33c75..00000000 --- a/src/test/java/com/chooz/support/fixture/ThumbnailFixture.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.chooz.support.fixture; - -import com.chooz.thumbnail.domain.Thumbnail; -import com.chooz.vote.domain.Vote; - -public class ThumbnailFixture { - - public static Thumbnail createDefaultThumbnail(Long postId, Long pollChoiceId) { - return Thumbnail.create(postId, pollChoiceId, "http://example.com/image"); - } - - public static Thumbnail.ThumbnailBuilder createThumbnailBuilder() { - return Thumbnail.builder() - .postId(1L) - .pollChoiceId(1L) - .thumbnailUrl("http://example.com/image"); - } -} diff --git a/src/test/java/com/chooz/support/mock/AwsS3Mock.java b/src/test/java/com/chooz/support/mock/AwsS3Mock.java new file mode 100644 index 00000000..250ead49 --- /dev/null +++ b/src/test/java/com/chooz/support/mock/AwsS3Mock.java @@ -0,0 +1,17 @@ +package com.chooz.support.mock; + +import com.chooz.image.application.S3Client; +import com.chooz.image.application.dto.PresignedUrlRequestDto; + +public class AwsS3Mock implements S3Client { + + @Override + public String getPresignedPutUrl(PresignedUrlRequestDto presignedUrlRequestDto) { + return ""; + } + + @Override + public void deleteImage(String assetUrl) { + + } +}