Skip to content

Commit 17aa696

Browse files
authored
feat: 차단 관련 api 구현 (#513)
* feat: 유저 차단 api 추가 - 커뮤니티 로직 수정 필요 * teat: 유저 차단 api 테스트 추가 * feat: 유저 차단 취소 api 추가 * test: 유저 차단 관련 fixture 추가 * test: 유저 차단 취소 api 테스트 추가 * feat: 유저 차단 조회 api 추가 * test: 유저 차단 조회 api 테스트 추가 * feat: 차단한 유저의 게시글은 보이지 않도록 수정 - 커뮤니티 목록은 로그인하지 않더라도 보이게 수정 * feat: 차단한 유저의 게시글 댓글도 보이지 않도록 수정 - 차단한 댓글이면 대댓글은 차단하지 않았더라도 보이지 않도록 함 * test: 코드리뷰 반영 - 인자 순서 변경 * style: 불필요한 개행 제거 * style: 불필요한 개행 제거 * style: 함수명 명확하게 변경 * style: 개행 컨벤션 준수 * refactor: validated 함수 분리 - 불필요한 지역변수 제거
1 parent 10969dd commit 17aa696

16 files changed

Lines changed: 460 additions & 28 deletions

File tree

src/main/java/com/example/solidconnection/common/exception/ErrorCode.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public enum ErrorCode {
4747
REPORT_TARGET_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 신고 대상입니다."),
4848
CHAT_PARTNER_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "채팅 상대를 찾을 수 없습니다."),
4949
CHAT_PARTICIPANT_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "채팅 참여자를 찾을 수 없습니다."),
50+
BLOCK_USER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "차단 대상 사용자를 찾을 수 없습니다."),
5051

5152
// auth
5253
USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."),
@@ -126,6 +127,10 @@ public enum ErrorCode {
126127
// report
127128
ALREADY_REPORTED_BY_CURRENT_USER(HttpStatus.BAD_REQUEST.value(), "이미 신고한 상태입니다."),
128129

130+
// block
131+
ALREADY_BLOCKED_BY_CURRENT_USER(HttpStatus.BAD_REQUEST.value(), "이미 차단한 상태입니다."),
132+
CANNOT_BLOCK_YOURSELF(HttpStatus.BAD_REQUEST.value(), "자기 자신을 차단할 수 없습니다."),
133+
129134
// chat
130135
INVALID_CHAT_ROOM_STATE(HttpStatus.BAD_REQUEST.value(), "잘못된 채팅방 상태입니다."),
131136

src/main/java/com/example/solidconnection/community/board/controller/BoardController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ public ResponseEntity<?> findAccessibleCodes() {
3333

3434
@GetMapping("/{code}")
3535
public ResponseEntity<?> findPostsByCodeAndCategory(
36-
@AuthorizedUser long siteUserId, // todo: '사용하지 않는 인자'로 인증된 유저만 접근하게 하기보다는, 다른 방식으로 접근하는것이 좋을 것 같다
36+
@AuthorizedUser(required = false) Long siteUserId,
3737
@PathVariable(value = "code") String code,
3838
@RequestParam(value = "category", defaultValue = "전체") String category) {
3939
List<PostListResponse> postsByCodeAndPostCategory = postQueryService
40-
.findPostsByCodeAndPostCategory(code, category);
40+
.findPostsByCodeAndPostCategory(code, category, siteUserId);
4141
return ResponseEntity.ok().body(postsByCodeAndPostCategory);
4242
}
4343
}

src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,31 @@
1212
public interface CommentRepository extends JpaRepository<Comment, Long> {
1313

1414
@Query(value = """
15-
WITH RECURSIVE CommentTree AS (
16-
SELECT
17-
id, parent_id, post_id, site_user_id, content,
18-
created_at, updated_at, is_deleted,
19-
0 AS level, CAST(id AS CHAR(255)) AS path
20-
FROM comment
21-
WHERE post_id = :postId AND parent_id IS NULL
22-
UNION ALL
23-
SELECT
24-
c.id, c.parent_id, c.post_id, c.site_user_id, c.content,
25-
c.created_at, c.updated_at, c.is_deleted,
26-
ct.level + 1, CONCAT(ct.path, '->', c.id)
27-
FROM comment c
28-
INNER JOIN CommentTree ct ON c.parent_id = ct.id
29-
)
30-
SELECT * FROM CommentTree
31-
ORDER BY path
32-
""", nativeQuery = true)
33-
List<Comment> findCommentTreeByPostId(@Param("postId") Long postId);
15+
WITH RECURSIVE CommentTree AS (
16+
SELECT
17+
id, parent_id, post_id, site_user_id, content,
18+
created_at, updated_at, is_deleted,
19+
0 AS level, CAST(id AS CHAR(255)) AS path
20+
FROM comment
21+
WHERE post_id = :postId AND parent_id IS NULL
22+
AND site_user_id NOT IN (
23+
SELECT blocked_id FROM user_block WHERE blocker_id = :siteUserId
24+
)
25+
UNION ALL
26+
SELECT
27+
c.id, c.parent_id, c.post_id, c.site_user_id, c.content,
28+
c.created_at, c.updated_at, c.is_deleted,
29+
ct.level + 1, CONCAT(ct.path, '->', c.id)
30+
FROM comment c
31+
INNER JOIN CommentTree ct ON c.parent_id = ct.id
32+
WHERE c.site_user_id NOT IN (
33+
SELECT blocked_id FROM user_block WHERE blocker_id = :siteUserId
34+
)
35+
)
36+
SELECT * FROM CommentTree
37+
ORDER BY path
38+
""", nativeQuery = true)
39+
List<Comment> findCommentTreeByPostIdExcludingBlockedUsers(@Param("postId") Long postId, @Param("siteUserId") Long siteUserId);
3440

3541
default Comment getById(Long id) {
3642
return findById(id)

src/main/java/com/example/solidconnection/community/comment/service/CommentService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class CommentService {
4040
public List<PostFindCommentResponse> findCommentsByPostId(long siteUserId, Long postId) {
4141
SiteUser siteUser = siteUserRepository.findById(siteUserId)
4242
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
43-
List<Comment> allComments = commentRepository.findCommentTreeByPostId(postId);
43+
List<Comment> allComments = commentRepository.findCommentTreeByPostIdExcludingBlockedUsers(postId, siteUserId);
4444
List<Comment> filteredComments = filterCommentsByDeletionRules(allComments);
4545

4646
Set<Long> userIds = filteredComments.stream()

src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ public interface PostRepository extends JpaRepository<Post, Long> {
1616

1717
List<Post> findByBoardCode(String boardCode);
1818

19+
@Query("""
20+
SELECT p FROM Post p
21+
WHERE p.boardCode = :boardCode
22+
AND p.siteUserId NOT IN (
23+
SELECT ub.blockedId FROM UserBlock ub WHERE ub.blockerId = :siteUserId
24+
)
25+
""")
26+
List<Post> findByBoardCodeExcludingBlockedUsers(@Param("boardCode") String boardCode, @Param("siteUserId") Long siteUserId);
27+
1928
@EntityGraph(attributePaths = {"postImageList"})
2029
Optional<Post> findPostById(Long id);
2130

src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.example.solidconnection.community.post.service;
22

3+
import static com.example.solidconnection.common.exception.ErrorCode.ACCESS_DENIED;
34
import static com.example.solidconnection.common.exception.ErrorCode.INVALID_BOARD_CODE;
45
import static com.example.solidconnection.common.exception.ErrorCode.INVALID_POST_CATEGORY;
56
import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;
@@ -21,6 +22,7 @@
2122
import com.example.solidconnection.siteuser.domain.SiteUser;
2223
import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse;
2324
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
25+
import com.example.solidconnection.siteuser.repository.UserBlockRepository;
2426
import com.example.solidconnection.util.RedisUtils;
2527
import java.util.List;
2628
import java.util.Objects;
@@ -38,18 +40,24 @@ public class PostQueryService {
3840
private final PostRepository postRepository;
3941
private final PostLikeRepository postLikeRepository;
4042
private final SiteUserRepository siteUserRepository;
43+
private final UserBlockRepository userBlockRepository;
4144
private final CommentService commentService;
4245
private final RedisService redisService;
4346
private final RedisUtils redisUtils;
4447

4548
@Transactional(readOnly = true)
46-
public List<PostListResponse> findPostsByCodeAndPostCategory(String code, String category) {
49+
public List<PostListResponse> findPostsByCodeAndPostCategory(String code, String category, Long siteUserId) {
4750

4851
String boardCode = validateCode(code);
4952
PostCategory postCategory = validatePostCategory(category);
5053
boardRepository.getByCode(boardCode);
51-
List<Post> postList = postRepository.findByBoardCode(boardCode);
5254

55+
List<Post> postList; // todo : 추후 개선 필요(현재 최신순으로 응답나가지 않고 있음)
56+
if (siteUserId != null) {
57+
postList = postRepository.findByBoardCodeExcludingBlockedUsers(boardCode, siteUserId);
58+
} else {
59+
postList = postRepository.findByBoardCode(boardCode);
60+
}
5361
return PostListResponse.from(getPostListByPostCategory(postList, postCategory));
5462
}
5563

@@ -58,6 +66,9 @@ public PostFindResponse findPostById(long siteUserId, Long postId) {
5866
SiteUser siteUser = siteUserRepository.findById(siteUserId)
5967
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
6068
Post post = postRepository.getByIdUsingEntityGraph(postId);
69+
70+
validatedIsBlockedByMe(post, siteUser);
71+
6172
Boolean isOwner = getIsOwner(post, siteUser);
6273
Boolean isLiked = getIsLiked(post, siteUser);
6374

@@ -111,4 +122,10 @@ private List<Post> getPostListByPostCategory(List<Post> postList, PostCategory p
111122
.filter(post -> post.getCategory().equals(postCategory))
112123
.collect(Collectors.toList());
113124
}
125+
126+
private void validatedIsBlockedByMe(Post post, SiteUser siteUser) {
127+
if (userBlockRepository.existsByBlockerIdAndBlockedId(siteUser.getId(), post.getSiteUserId())) {
128+
throw new CustomException(ACCESS_DENIED);
129+
}
130+
}
114131
}

src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
package com.example.solidconnection.siteuser.controller;
22

3+
import com.example.solidconnection.common.dto.SliceResponse;
4+
import com.example.solidconnection.common.resolver.AuthorizedUser;
35
import com.example.solidconnection.siteuser.dto.NicknameExistsResponse;
6+
import com.example.solidconnection.siteuser.dto.UserBlockResponse;
47
import com.example.solidconnection.siteuser.service.SiteUserService;
58
import lombok.RequiredArgsConstructor;
9+
import org.springframework.data.domain.Pageable;
10+
import org.springframework.data.domain.Sort;
11+
import org.springframework.data.web.PageableDefault;
612
import org.springframework.http.ResponseEntity;
13+
import org.springframework.web.bind.annotation.DeleteMapping;
714
import org.springframework.web.bind.annotation.GetMapping;
15+
import org.springframework.web.bind.annotation.PathVariable;
16+
import org.springframework.web.bind.annotation.PostMapping;
817
import org.springframework.web.bind.annotation.RequestMapping;
918
import org.springframework.web.bind.annotation.RequestParam;
1019
import org.springframework.web.bind.annotation.RestController;
@@ -23,4 +32,31 @@ public ResponseEntity<NicknameExistsResponse> checkNicknameExists(
2332
NicknameExistsResponse nicknameExistsResponse = siteUserService.checkNicknameExists(nickname);
2433
return ResponseEntity.ok(nicknameExistsResponse);
2534
}
35+
36+
@GetMapping("/blocks")
37+
public ResponseEntity<SliceResponse<UserBlockResponse>> getBlockedUsers(
38+
@AuthorizedUser long siteUserId,
39+
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable
40+
) {
41+
SliceResponse<UserBlockResponse> response = siteUserService.getBlockedUsers(siteUserId, pageable);
42+
return ResponseEntity.ok(response);
43+
}
44+
45+
@PostMapping("/block/{blocked-id}")
46+
public ResponseEntity<Void> blockUser(
47+
@AuthorizedUser long siteUserId,
48+
@PathVariable("blocked-id") Long blockedId
49+
) {
50+
siteUserService.blockUser(siteUserId, blockedId);
51+
return ResponseEntity.ok().build();
52+
}
53+
54+
@DeleteMapping("/block/{blocked-id}")
55+
public ResponseEntity<Void> cancelUserBlock(
56+
@AuthorizedUser long siteUserId,
57+
@PathVariable("blocked-id") Long blockedId
58+
) {
59+
siteUserService.cancelUserBlock(siteUserId, blockedId);
60+
return ResponseEntity.ok().build();
61+
}
2662
}

src/main/java/com/example/solidconnection/siteuser/domain/UserBlock.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,9 @@ public class UserBlock extends BaseEntity {
3434

3535
@Column(name = "blocked_id", nullable = false)
3636
private long blockedId;
37+
38+
public UserBlock(long blockerId, long blockedId) {
39+
this.blockerId = blockerId;
40+
this.blockedId = blockedId;
41+
}
3742
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.example.solidconnection.siteuser.dto;
2+
3+
import java.time.ZonedDateTime;
4+
5+
public record UserBlockResponse(
6+
long id,
7+
long blockedId,
8+
String nickname,
9+
ZonedDateTime createdAt
10+
) {
11+
12+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.example.solidconnection.siteuser.repository;
2+
3+
import com.example.solidconnection.siteuser.domain.UserBlock;
4+
import com.example.solidconnection.siteuser.dto.UserBlockResponse;
5+
import java.util.Optional;
6+
import org.springframework.data.domain.Pageable;
7+
import org.springframework.data.domain.Slice;
8+
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
11+
12+
public interface UserBlockRepository extends JpaRepository<UserBlock, Long> {
13+
14+
boolean existsByBlockerIdAndBlockedId(long blockerId, long blockedId);
15+
16+
Optional<UserBlock> findByBlockerIdAndBlockedId(long blockerId, long blockedId);
17+
18+
@Query("""
19+
SELECT new com.example.solidconnection.siteuser.dto.UserBlockResponse(
20+
ub.id, ub.blockedId, su.nickname, ub.createdAt
21+
)
22+
FROM UserBlock ub
23+
JOIN SiteUser su ON ub.blockedId = su.id
24+
WHERE ub.blockerId = :blockerId
25+
""")
26+
Slice<UserBlockResponse> findBlockedUsersWithNickname(@Param("blockerId") long blockerId, Pageable pageable);
27+
}

0 commit comments

Comments
 (0)