Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
23d2a25
[refactor] 안쓰는 삭제용 양방향 매핑 삭제 (#176)
hd0rable Sep 2, 2025
91a02bc
[feat] 회원 탈퇴 시 연관된 오늘의 한마디 삭제 (#176)
hd0rable Sep 4, 2025
71b6142
[feat] 회원 탈퇴 시 연관된 책 저장관계 삭제 (#176)
hd0rable Sep 4, 2025
791c684
[feat] 회원 탈퇴 시 연관된 댓글 삭제 관련 메서드 작성 (#176)
hd0rable Sep 4, 2025
8a61d66
[feat] 회원 탈퇴 시 연관된 댓글 삭제시 SETTER 추가 (#176)
hd0rable Sep 4, 2025
951470a
[feat] 회원 탈퇴 시 연관된 댓글좋아요 삭제 관련 메서드 작성 (#176)
hd0rable Sep 4, 2025
7357f80
[feat] 회원 탈퇴 관련 에러코드 추가 (#176)
hd0rable Sep 4, 2025
aee4840
[feat] 회원 탈퇴 시 연관된 피드 삭제 관련 메서드 작성 (#176)
hd0rable Sep 4, 2025
5b4d0f1
[feat] 회원 탈퇴 시 연관된 피드 삭제 관련 메서드 작성 (#176)
hd0rable Sep 4, 2025
c9c7b1f
[feat] 회원 탈퇴 시 연관된 게시물 삭제시 SETTER 추가 (#176)
hd0rable Sep 4, 2025
cdaa6fd
[feat] 회원 탈퇴 시 연관된 게시물 좋아요 삭제 관련 메서드 작성 (#176)
hd0rable Sep 4, 2025
a294788
[feat] 회원 탈퇴 시 연관된 최근검색어 삭제 관련 메서드 작성 (#176)
hd0rable Sep 4, 2025
32c54a0
[feat] 회원 탈퇴 시 연관된 기록 삭제 관련 메서드 작성 (#176)
hd0rable Sep 4, 2025
c3ffa05
[feat] 회원 탈퇴 시 연관된 방참여관계 삭제시 SETTER 추가 (#176)
hd0rable Sep 4, 2025
da887ca
[feat] 회원 탈퇴 시 연관된 방 참여관계 삭제 관련 메서드 작성 (#176)
hd0rable Sep 4, 2025
186e6a0
[refactor] 기록 조회시 투표항목 득표수 보여주도록 수정 (#176)
hd0rable Sep 4, 2025
42418ea
[feat] 회원 탈퇴 시 연관된 책 저장관계 삭제 메서드 추가(#176)
hd0rable Sep 4, 2025
e5af506
[feat] 회원 탈퇴 시 oauth2Id변경 메서드 유저 도메인에 추가 (#176)
hd0rable Sep 4, 2025
42b4f73
[feat] 회원 탈퇴 컨트롤러 작성 (#176)
hd0rable Sep 4, 2025
6c85c3c
[feat] 회원 탈퇴 유즈케이스 작성 (#176)
hd0rable Sep 4, 2025
1451f16
[feat] 회원 탈퇴 유즈케이스 구현체 서비스메서드 작성 (#176)
hd0rable Sep 4, 2025
c678717
[feat] 회원 탈퇴 시 softDelete메서드 추가 (#176)
hd0rable Sep 4, 2025
1e8aefc
[feat] 회원 탈퇴 시 유저삭제 관련 메서드 추가 (#176)
hd0rable Sep 4, 2025
d50f730
[feat] 토큰 추출 어노테이션 작성 (#176)
hd0rable Sep 4, 2025
6da045e
[feat] 필터에서 토큰 등록/ 블랙리스트 토큰 검증 로직 추가 (#176)
hd0rable Sep 4, 2025
28dbba4
[feat] 토큰 유효기간 추출 메서드 작성 (#176)
hd0rable Sep 4, 2025
4f43183
[feat] 블랙리스트 토큰관련 레디스 어댑터 추가 (#176)
hd0rable Sep 4, 2025
4b70301
[feat] 회원 탈퇴 시 연관된 투표 삭제 관련 메서드 작성 (#176)
hd0rable Sep 4, 2025
04ca0b4
[feat] 회원 탈퇴 시 연관된 투표 삭제 관련 메서드 작성 (#176)
hd0rable Sep 4, 2025
79e1fbf
[feat] 회원 탈퇴 시 연관된 투표 삭제시 SETTER 추가 (#176)
hd0rable Sep 4, 2025
c1de20b
[refactor] 투표시 투표항목 득표수 보여주도록 수정 (#176)
hd0rable Sep 4, 2025
f4497ad
[feat] 토큰 추출 어노테이션 작성 (#176)
hd0rable Sep 4, 2025
583c6cc
[test] 테스트 팩토리 메서드 추가 (#176)
hd0rable Sep 4, 2025
b8d6899
[test] 회원 탈퇴 통합 테스트 코드 작성 (#176)
hd0rable Sep 4, 2025
17324db
Merge remote-tracking branch 'origin/develop' into feat/#176-user-delete
hd0rable Sep 4, 2025
868bc69
[refactor] 리뷰 반영 수정 (#176)
hd0rable Sep 4, 2025
cb58473
[refactor] 소프트 딜리트 명시 (#176)
hd0rable Sep 8, 2025
62a4568
[refactor] 댓글 중복 집계 수정 (#176)
hd0rable Sep 8, 2025
98fa05c
[refactor] 댓글 소프트 딜맅트 명시 및 변환 타입 명시 (#176)
hd0rable Sep 8, 2025
5dd094c
[refactor] 에러코드 추가 (#176)
hd0rable Sep 8, 2025
3666ce7
[refactor] 유저조회 UserJpaRepository에서 하도록 수정 (#176)
hd0rable Sep 8, 2025
2272324
[refactor] 에러처리 추가 (#176)
hd0rable Sep 8, 2025
3745996
[refactor] 주석정리 (#176)
hd0rable Sep 8, 2025
bdffe2e
[refactor] 득표 수 벌크쿼리 (#176)
hd0rable Sep 8, 2025
05d2ade
[refactor] 투표 소프트 딜리트 명시 (#176)
hd0rable Sep 8, 2025
720ec4f
[refactor] 블랙리스트 토큰 추가 시 관련 정보 함께 json형태로 추가 (#176)
hd0rable Sep 8, 2025
3c530d5
[refactor] 스웨거 에러코드 추가 (#176)
hd0rable Sep 8, 2025
7ea4c22
[refactor] 테스트코드 수정 (#176)
hd0rable Sep 8, 2025
15e0a8a
[refactor] 탈퇴할 유저가 작성한 댓글 삭제와 관련된 영속성 코드 수정 (#176)
seongjunnoh Sep 8, 2025
bcf15c5
[test] CommentCommandPersistenceAdapter 테스트 코드 추가 (#176)
seongjunnoh Sep 8, 2025
3bac26c
[refactor] 댓금 감소 로직 코드 순서 수정 (#176)
hd0rable Sep 8, 2025
be383b0
[refactor] 게시글 좋아요 감소 로직 코드 순서 수정 (#176)
hd0rable Sep 8, 2025
63ffe7b
[refactor] 방 멤버수/진행도 업데이트 프로젝션 사용하여 집계쿼리로 수정 (#176)
hd0rable Sep 8, 2025
7914efc
[refactor] 블랙리스트 토큰 로깅 수정 (#176)
hd0rable Sep 8, 2025
d53c265
[refactor] 테스트케이스 임포트문 수정 (#176)
hd0rable Sep 8, 2025
6f5b2fe
[refactor] RoomAggregateProjection으로 dto 네이밍 변경 (#176)
hd0rable Sep 8, 2025
858dc0f
Merge remote-tracking branch 'origin/develop' into feat/#176-user-delete
hd0rable Sep 8, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,9 @@ public void deleteSavedBook(Long userId, Long bookId) {
public void deleteAllByIdInBatch(Set<Long> unusedBookIds) {
bookJpaRepository.deleteAllByIdInBatch(unusedBookIds);
}

@Override
public void deleteAllSavedBookByUserId(Long userId) {
savedBookJpaRepository.deleteAllByUserId(userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@ public interface SavedBookJpaRepository extends JpaRepository<SavedBookJpaEntity

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM SavedBookJpaEntity s WHERE s.userJpaEntity.userId = :userId AND s.bookJpaEntity.bookId = :bookId")
void deleteByUserIdAndBookId(Long userId, Long bookId);
void deleteByUserIdAndBookId(@Param("userId") Long userId, @Param("bookId") Long bookId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM SavedBookJpaEntity s WHERE s.userJpaEntity.userId = :userId")
void deleteAllByUserId(@Param("userId") Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ default Book getByIsbnOrThrow(String isbn){
void deleteSavedBook(Long userId, Long bookId);

void deleteAllByIdInBatch(Set<Long> unusedBookIds);

void deleteAllSavedBookByUserId(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
import lombok.*;
import org.hibernate.annotations.SQLDelete;

import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "comments")
@Getter
Expand All @@ -34,11 +31,15 @@ public class CommentJpaEntity extends BaseJpaEntity {
@Column(name = "report_count", nullable = false)
private int reportCount = 0;

/**
* -- SETTER --
* 회원 탈퇴용
*/
@Setter
@Builder.Default
@Column(name = "like_count", nullable = false)
private int likeCount = 0;

//TODO 상속구조 해지하면서 postType만 가질지, postId + postType가질지 논의 필요
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "post_id", nullable = false)
private PostJpaEntity postJpaEntity;
Expand All @@ -58,11 +59,6 @@ public class CommentJpaEntity extends BaseJpaEntity {
@JoinColumn(name = "parent_id")
private CommentJpaEntity parent;

// 삭제용 댓글 좋아요 양방향 매핑 관계
@Builder.Default
@OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<CommentLikeJpaEntity> commentLikeList = new ArrayList<>();

public CommentJpaEntity updateFrom(Comment comment) {
this.reportCount = comment.getReportCount();
this.likeCount = comment.getLikeCount();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;

import static konkuk.thip.common.exception.code.ErrorCode.*;

Expand Down Expand Up @@ -57,17 +58,6 @@ public Long save(Comment comment) {
).getCommentId();
}

private PostJpaEntity findPostJpaEntity(PostType postType, Long postId) {
return switch (postType) {
case FEED -> feedJpaRepository.findByPostId(postId)
.orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND));
case RECORD -> recordJpaRepository.findByPostId(postId)
.orElseThrow(() -> new EntityNotFoundException(RECORD_NOT_FOUND));
case VOTE -> voteJpaRepository.findByPostId(postId)
.orElseThrow(() -> new EntityNotFoundException(VOTE_NOT_FOUND));
};
}

@Override
public Optional<Comment> findById(Long id) {
return commentJpaRepository.findByCommentId(id)
Expand Down Expand Up @@ -97,4 +87,37 @@ public void softDeleteAllByPostId(Long postId) {
commentJpaRepository.softDeleteAllByPostId(postId);
}

@Override
public void deleteAllByUserId(Long userId) {
// 1. 탈퇴 유저가 작성한 댓글과 연관된 게시글을 JOIN FETCH로 함께 조회
List<CommentJpaEntity> commentsWithPosts = commentJpaRepository.findAllCommentsWithPostsByUserId(userId);
if (commentsWithPosts == null || commentsWithPosts.isEmpty()) {
return; //early return
}

// 2. 삭제될 댓글이 어느 Post에 몇 개씩 붙어있는지 집계 (postId 기준 추천)
Map<PostJpaEntity, Long> decMap = commentsWithPosts.stream()
.collect(Collectors.groupingBy(CommentJpaEntity::getPostJpaEntity, Collectors.counting()));

// 3. 댓글 수를 집계만큼 한 번에 감소
for (PostJpaEntity p : decMap.keySet()) {
long dec = decMap.getOrDefault(p, 0L);
p.setCommentCount(Math.max(0, p.getCommentCount() - (int) dec));
}

// 4. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제
commentLikeJpaRepository.deleteAllByCommentAuthorUserId(userId);
commentJpaRepository.softDeleteAllByUserId(userId);
}

private PostJpaEntity findPostJpaEntity(PostType postType, Long postId) {
return switch (postType) {
case FEED -> feedJpaRepository.findByPostId(postId)
.orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND));
case RECORD -> recordJpaRepository.findByPostId(postId)
.orElseThrow(() -> new EntityNotFoundException(RECORD_NOT_FOUND));
case VOTE -> voteJpaRepository.findByPostId(postId)
.orElseThrow(() -> new EntityNotFoundException(VOTE_NOT_FOUND));
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

import static konkuk.thip.common.exception.code.ErrorCode.*;

@Repository
Expand Down Expand Up @@ -47,4 +49,17 @@ public void deleteAllByCommentId(Long commentId) {
commentLikeJpaRepository.deleteAllByCommentId(commentId);
}

@Override
public void deleteAllByUserId(Long userId) {
// 1. 탈퇴 유저가 좋아요 누른 댓글 ID 리스트 조회
List<Long> likedCommentIds = commentLikeJpaRepository.findAllCommentIdsByUserId(userId);
if (likedCommentIds == null || likedCommentIds.isEmpty()) {
return; //early return
}
// 2. 탈퇴한 유저의 모든 댓글 좋아요 관계 삭제
commentLikeJpaRepository.deleteAllByLikerUserId(userId);
// 3. 탈퇴 유저가 좋아요 누른 댓글의 좋아요 수 감소
commentJpaRepository.bulkDecrementLikeCountByIds(likedCommentIds);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface CommentJpaRepository extends JpaRepository<CommentJpaEntity, Long>, CommentQueryRepository {
Expand All @@ -19,4 +20,21 @@ public interface CommentJpaRepository extends JpaRepository<CommentJpaEntity, Lo
@Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId = :postId")
void softDeleteAllByPostId(@Param("postId") Long postId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.userJpaEntity.userId = :userId")
void softDeleteAllByUserId(@Param("userId") Long userId);

@Query("SELECT c FROM CommentJpaEntity c JOIN FETCH c.postJpaEntity p " +
"WHERE c.userJpaEntity.userId = :userId")
List<CommentJpaEntity> findAllCommentsWithPostsByUserId(@Param("userId") Long userId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId IN :postIds")
void softDeleteAllByPostIds(@Param("postIds") List<Long> postIds);

Comment on lines +31 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

빈 컬렉션 전달 시 예외 위험(Hibernate Parameter list is empty) + 상태 조건 보강

  • IN :postIds에 빈 리스트가 넘어가면 런타임 예외가 납니다. 호출부에서 빈 컬렉션이면 조기 반환하도록 가드가 필요합니다.
  • 이미 INACTIVE인 행은 업데이트 제외 권장.

레포지토리 쿼리 보강 예시(diff):

 @Modifying(clearAutomatically = true, flushAutomatically = true)
-@Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId IN :postIds")
+@Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' " +
+       "WHERE c.postJpaEntity.postId IN :postIds AND c.status <> 'INACTIVE'")
 void softDeleteAllByPostIds(@Param("postIds") List<Long> postIds);

호출부 가드 예시(Java):

if (postIds == null || postIds.isEmpty()) {
  return;
}
commentJpaRepository.softDeleteAllByPostIds(postIds);

대량 ID 처리 시 DB 파라미터 한계에 걸릴 수 있으니(드라이버/DB별 제한) 어댑터 레벨에서 chunking(예: 1k~5k 단위)도 고려해 주세요.


🏁 Script executed:

#!/bin/bash
# Find all call sites of softDeleteAllByPostIds to verify if empty‐list guards exist
rg -n "softDeleteAllByPostIds" -C2 --include "*.java"

Length of output: 495


🏁 Script executed:

#!/bin/bash
# Search for usage of softDeleteAllByPostIds across the repository
rg -n "softDeleteAllByPostIds" .

# Search for any direct bean references to CommentJpaRepository in Java files
rg -n "CommentJpaRepository" -g '*.java'

Length of output: 6017


CommentJpaRepository.softDeleteAllByPostIds 호출부 및 쿼리 보강 필요

  • postIds가 null 또는 빈 리스트면 Hibernate에서 “Parameter list is empty” 런타임 예외 발생 → 호출부에 빈 리스트 조기 반환 로직 추가 필수
  • 이미 INACTIVE인 레코드에 대해서도 UPDATE 수행함 → 쿼리에 AND c.status <> 'INACTIVE' 조건 추가
  • (옵션) 대량 ID 파라미터 한계(DB/드라이버별 제한) 고려해 adapter 레벨에서 1k~5k 단위 chunking 구현 검토
 @Modifying(clearAutomatically = true, flushAutomatically = true)
-@Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId IN :postIds")
+@Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' " +
+       "WHERE c.postJpaEntity.postId IN :postIds AND c.status <> 'INACTIVE'")
 void softDeleteAllByPostIds(@Param("postIds") List<Long> postIds);

호출부 가드 예시:

if (postIds == null || postIds.isEmpty()) {
    return;
}
commentJpaRepository.softDeleteAllByPostIds(postIds);
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java
around lines 30-33, the softDeleteAllByPostIds method can throw a "Parameter
list is empty" runtime error when postIds is null/empty and also redundantly
updates already INACTIVE rows; update callers to early-return when postIds is
null or empty (i.e., if postIds == null || postIds.isEmpty() return), and change
the JPQL to include "AND c.status <> 'INACTIVE'" so only active rows are
updated; additionally consider implementing chunking of the postIds list (e.g.,
1k–5k per batch) at the adapter level to avoid DB/driver parameter limits for
very large lists.

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE CommentJpaEntity c " +
"SET c.likeCount = CASE WHEN c.likeCount > 0 THEN c.likeCount - 1 ELSE 0 END " +
"WHERE c.commentId IN :likedCommentIds")
void bulkDecrementLikeCountByIds(@Param("likedCommentIds") List<Long> likedCommentIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,31 @@ WHERE cl.commentJpaEntity.commentId IN (
)
""")
void deleteAllByPostId(@Param("postId") Long postId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
DELETE FROM CommentLikeJpaEntity cl
WHERE cl.commentJpaEntity.commentId IN (
SELECT c.commentId FROM CommentJpaEntity c
WHERE c.userJpaEntity.userId = :userId
)
""")
void deleteAllByCommentAuthorUserId(@Param("userId") Long userId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM CommentLikeJpaEntity cl WHERE cl.userJpaEntity.userId = :userId")
void deleteAllByLikerUserId(@Param("userId") Long userId);

@Query("SELECT cl.commentJpaEntity.commentId FROM CommentLikeJpaEntity cl WHERE cl.userJpaEntity.userId = :userId")
List<Long> findAllCommentIdsByUserId(@Param("userId") Long userId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
DELETE FROM CommentLikeJpaEntity cl
WHERE cl.commentJpaEntity.commentId IN (
SELECT c.commentId FROM CommentJpaEntity c
WHERE c.postJpaEntity.postId IN :postIds
)
""")
void deleteAllByPostIds(@Param("postIds") List<Long> postIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ default Comment getByIdOrThrow(Long id) {
void delete(Comment comment);

void softDeleteAllByPostId(Long postId);

void deleteAllByUserId(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public interface CommentLikeCommandPort {
void save(Long userId, Long commentId);
void delete(Long userId, Long commentId);
void deleteAllByCommentId(Long commentId);
void deleteAllByUserId(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public enum ErrorCode implements ResponseCode {
AUTH_LOGIN_FAILED(HttpStatus.UNAUTHORIZED, 40104, "로그인에 실패했습니다."),
AUTH_UNSUPPORTED_SOCIAL_LOGIN(HttpStatus.UNAUTHORIZED, 40105, "지원하지 않는 소셜 로그인입니다."),
AUTH_INVALID_LOGIN_TOKEN_KEY(HttpStatus.UNAUTHORIZED, 40106, "유효하지 않은 로그인 토큰 키입니다."),
AUTH_BLACKLIST_TOKEN(HttpStatus.UNAUTHORIZED, 40107, "블랙리스트에 등록된 토큰입니다."),

JSON_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50100, "JSON 직렬화/역직렬화에 실패했습니다."),
AWS_BUCKET_BASE_URL_NOT_CONFIGURED(HttpStatus.INTERNAL_SERVER_ERROR, 50101, "aws s3 bucket base url 설정이 누락되었습니다."),
Expand All @@ -48,6 +49,9 @@ public enum ErrorCode implements ResponseCode {
USER_NICKNAME_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, 70006, "다른 사용자가 이미 사용중인 닉네임입니다."),
USER_ALREADY_SIGNED_UP(HttpStatus.BAD_REQUEST, 70007, "이미 가입된 사용자입니다."),
USER_NOT_SIGNED_UP(HttpStatus.BAD_REQUEST, 70008, "가입되지 않은 사용자입니다."),
USER_CANNOT_DELETE_ROOM_HOST(HttpStatus.BAD_REQUEST, 70009, "모집/진행 중인 방의 방장은 회원탈퇴를 할 수 없습니다."),
USER_ALREADY_DELETED(HttpStatus.BAD_REQUEST, 70010, "이미 삭제된 사용자 입니다."),
USER_OAUTH2ID_CANNOT_BE_NULL(HttpStatus.INTERNAL_SERVER_ERROR, 70011, "유저의 OAuth2Id 값이 null일 수 없습니다."),

/**
* 75000 : follow error
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package konkuk.thip.common.security.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthToken {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package konkuk.thip.common.security.argument_resolver;

import jakarta.servlet.http.HttpServletRequest;
import konkuk.thip.common.exception.AuthException;
import konkuk.thip.common.security.annotation.AuthToken;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import static konkuk.thip.common.exception.code.ErrorCode.AUTH_TOKEN_NOT_FOUND;

@Component
@Slf4j
@RequiredArgsConstructor
public class AuthTokenArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthToken.class)
&& parameter.getParameterType().equals(String.class);
}

@Override
public String resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {

Object token = ((HttpServletRequest) webRequest.getNativeRequest()).getAttribute("token");
if (token == null) {
throw new AuthException(AUTH_TOKEN_NOT_FOUND);
}
return (String) token;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum AuthParameters {
GOOGLE_PROVIDER_ID_KEY("sub"),
JWT_ACCESS_TOKEN_KEY("userId"),
JWT_SIGNUP_TOKEN_KEY("oauth2Id"),
JWT_TOKEN_ATTRIBUTE("token"),
REDIRECT_SIGNUP_URL("/signup"),
REDIRECT_HOME_URL("/feed"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import konkuk.thip.common.security.oauth2.CustomOAuth2User;
import konkuk.thip.common.security.oauth2.LoginUser;
import konkuk.thip.common.security.util.JwtUtil;
import konkuk.thip.user.application.port.UserTokenBlacklistQueryPort;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand All @@ -27,6 +28,7 @@
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;
private final UserTokenBlacklistQueryPort userTokenBlacklistQueryPort;

@Override
protected void doFilterInternal(HttpServletRequest request,
Expand All @@ -38,6 +40,10 @@ protected void doFilterInternal(HttpServletRequest request,
throw new AuthException(AUTH_TOKEN_NOT_FOUND);
}

if (userTokenBlacklistQueryPort.isTokenBlacklisted(token)) {
throw new AuthException(AUTH_BLACKLIST_TOKEN);
}

if (!jwtUtil.validateToken(token)) {
throw new AuthException(AUTH_INVALID_TOKEN);
}
Expand All @@ -46,6 +52,7 @@ protected void doFilterInternal(HttpServletRequest request,
throw new AuthException(AUTH_EXPIRED_TOKEN);
}

request.setAttribute(JWT_TOKEN_ATTRIBUTE.getValue(), token);
LoginUser loginUser = jwtUtil.getLoginUser(token);

if (loginUser.userId() != null) {
Expand Down
18 changes: 14 additions & 4 deletions src/main/java/konkuk/thip/common/security/util/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package konkuk.thip.common.security.util;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.*;
import konkuk.thip.common.security.oauth2.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -86,4 +83,17 @@ public LoginUser getLoginUser(String token) {
}
return LoginUser.createExistingUser(oauth2Id, userId);
}

public Date getExpirationAllowExpired(String token) {
try {
Jws<Claims> jwt = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token);
return jwt.getPayload().getExpiration();
} catch (ExpiredJwtException e) {
return e.getClaims().getExpiration();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ public enum SwaggerResponseDescription {
USER_NICKNAME_UPDATE_TOO_FREQUENT,
USER_NICKNAME_ALREADY_EXISTS
))),

USER_DELETE(new LinkedHashSet<>(Set.of(
USER_CANNOT_DELETE_ROOM_HOST,
USER_NOT_FOUND,
USER_ALREADY_DELETED,
USER_OAUTH2ID_CANNOT_BE_NULL
))),

// Follow
CHANGE_FOLLOW_STATE(new LinkedHashSet<>(Set.of(
Expand Down Expand Up @@ -348,6 +353,7 @@ public enum SwaggerResponseDescription {
// AUTH_TOKEN_NOT_FOUND
// AUTH_LOGIN_FAILED,
// AUTH_UNSUPPORTED_SOCIAL_LOGIN,
// AUTH_BLACKLIST_TOKEN,

// JSON_PROCESSING_ERROR
)));
Expand Down
Loading