Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cd-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 &
2 changes: 1 addition & 1 deletion .github/workflows/cd-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 &
2 changes: 1 addition & 1 deletion server-config
12 changes: 2 additions & 10 deletions src/main/java/com/chooz/auth/application/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
}
48 changes: 48 additions & 0 deletions src/main/java/com/chooz/auth/application/WithdrawHandler.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String, String> params
);
}
19 changes: 19 additions & 0 deletions src/main/java/com/chooz/auth/application/oauth/OAuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -43,4 +44,22 @@ private MultiValueMap<String, String> 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<String, String> unlinkRequestParams(String socialId) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("target_id_type", "user_id");
params.add("target_id", socialId);
return params;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.chooz.auth.application.oauth.dto;

public record KakaoUnlinkResponse(String id) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<SocialAccount, Long> {

Optional<SocialAccount> findBySocialIdAndProvider(String socialId, Provider provider);

void deleteByUserId(Long userId);

Optional<SocialAccount> findByUserId(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ Slice<Comment> findByPostId(

List<Comment> findByPostIdAndDeletedFalse(@NotNull Long postId);

void deleteAllByPostId(Long postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public record KakaoOAuthConfig(
String authorizationUri,
String clientId,
String clientSecret,
String adminKey,
String[] scope,
String userInfoUri
) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/chooz/common/dev/DataInitConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class DataInitConfig {

private final DataInitializer dataInitializer;

@PostConstruct
// @PostConstruct
public void init() {
dataInitializer.init();
}
Expand Down
115 changes: 58 additions & 57 deletions src/main/java/com/chooz/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions src/main/java/com/chooz/common/exception/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/chooz/image/application/ImageService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/chooz/image/application/S3Client.java
Original file line number Diff line number Diff line change
@@ -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);
}
Loading