Skip to content

Commit e303ff6

Browse files
committed
feat: 사용자 차단 기능 추가
- Block 엔티티 및 Repository 생성 - BlockService: 차단/차단해제/차단목록조회 기능 - BlockController: REST API 엔드포인트 (/blocks) - POST /blocks/{userId} - 사용자 차단 - DELETE /blocks/{userId} - 차단 해제 - GET /blocks - 차단한 사용자 목록 조회 - 차단 시 친구 관계 자동 삭제 (양방향) - 공개 편지 조회 시 차단한 사용자의 편지 제외 - 다국어 메시지 추가 (ko/en/ja)
1 parent c7615d6 commit e303ff6

File tree

10 files changed

+366
-2
lines changed

10 files changed

+366
-2
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.taba.block.controller;
2+
3+
import com.taba.block.dto.BlockedUserDto;
4+
import com.taba.block.service.BlockService;
5+
import com.taba.common.dto.ApiResponse;
6+
import com.taba.common.util.MessageUtil;
7+
import com.taba.common.util.SecurityUtil;
8+
import com.taba.user.entity.User;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
import java.util.List;
15+
16+
@RestController
17+
@RequestMapping("/blocks")
18+
@RequiredArgsConstructor
19+
public class BlockController {
20+
21+
private final BlockService blockService;
22+
23+
/**
24+
* 사용자 차단
25+
* POST /blocks/{userId}
26+
*/
27+
@PostMapping("/{userId}")
28+
public ResponseEntity<ApiResponse<?>> blockUser(@PathVariable String userId) {
29+
blockService.blockUser(userId);
30+
31+
User currentUser = SecurityUtil.getCurrentUser();
32+
String language = currentUser != null && currentUser.getLanguage() != null ? currentUser.getLanguage() : "ko";
33+
String message = MessageUtil.getMessage("api.block.blocked", language);
34+
35+
return ResponseEntity.status(HttpStatus.CREATED)
36+
.body(ApiResponse.success(message));
37+
}
38+
39+
/**
40+
* 차단 해제
41+
* DELETE /blocks/{userId}
42+
*/
43+
@DeleteMapping("/{userId}")
44+
public ResponseEntity<ApiResponse<?>> unblockUser(@PathVariable String userId) {
45+
blockService.unblockUser(userId);
46+
47+
User currentUser = SecurityUtil.getCurrentUser();
48+
String language = currentUser != null && currentUser.getLanguage() != null ? currentUser.getLanguage() : "ko";
49+
String message = MessageUtil.getMessage("api.block.unblocked", language);
50+
51+
return ResponseEntity.ok(ApiResponse.success(message));
52+
}
53+
54+
/**
55+
* 차단한 사용자 목록 조회
56+
* GET /blocks
57+
*/
58+
@GetMapping
59+
public ResponseEntity<ApiResponse<List<BlockedUserDto>>> getBlockedUsers() {
60+
List<BlockedUserDto> blockedUsers = blockService.getBlockedUsers();
61+
return ResponseEntity.ok(ApiResponse.success(blockedUsers));
62+
}
63+
}
64+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.taba.block.dto;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
import java.time.LocalDateTime;
7+
8+
/**
9+
* 차단한 사용자 정보 DTO
10+
*/
11+
@Getter
12+
@Builder
13+
public class BlockedUserDto {
14+
private String id; // 사용자 ID
15+
private String nickname; // 닉네임
16+
private String avatarUrl; // 프로필 이미지 URL
17+
private LocalDateTime blockedAt; // 차단한 시간
18+
}
19+
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.taba.block.entity;
2+
3+
import com.taba.common.entity.BaseEntity;
4+
import com.taba.user.entity.User;
5+
import jakarta.persistence.*;
6+
import lombok.AccessLevel;
7+
import lombok.Builder;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
11+
import java.util.UUID;
12+
13+
/**
14+
* 사용자 차단 엔티티
15+
* - blocker: 차단을 한 사용자
16+
* - blocked: 차단을 당한 사용자
17+
*/
18+
@Entity
19+
@Table(name = "blocks",
20+
uniqueConstraints = @UniqueConstraint(name = "uk_blocker_blocked", columnNames = {"blocker_id", "blocked_id"}),
21+
indexes = {
22+
@Index(name = "idx_blocker", columnList = "blocker_id"),
23+
@Index(name = "idx_blocked", columnList = "blocked_id")
24+
})
25+
@Getter
26+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
27+
public class Block extends BaseEntity {
28+
29+
@Id
30+
@Column(name = "id", length = 36)
31+
private String id;
32+
33+
@ManyToOne(fetch = FetchType.LAZY)
34+
@JoinColumn(name = "blocker_id", nullable = false)
35+
private User blocker; // 차단을 한 사용자
36+
37+
@ManyToOne(fetch = FetchType.LAZY)
38+
@JoinColumn(name = "blocked_id", nullable = false)
39+
private User blocked; // 차단을 당한 사용자
40+
41+
@PrePersist
42+
public void prePersist() {
43+
if (this.id == null) {
44+
this.id = UUID.randomUUID().toString();
45+
}
46+
}
47+
48+
@Builder
49+
public Block(User blocker, User blocked) {
50+
this.blocker = blocker;
51+
this.blocked = blocked;
52+
}
53+
}
54+
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.taba.block.repository;
2+
3+
import com.taba.block.entity.Block;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
6+
import org.springframework.data.repository.query.Param;
7+
import org.springframework.stereotype.Repository;
8+
9+
import java.util.List;
10+
import java.util.Optional;
11+
12+
@Repository
13+
public interface BlockRepository extends JpaRepository<Block, String> {
14+
15+
/**
16+
* 특정 사용자가 차단한 사용자 목록 조회
17+
*/
18+
@Query("SELECT b FROM Block b WHERE b.blocker.id = :blockerId AND b.deletedAt IS NULL")
19+
List<Block> findByBlockerId(@Param("blockerId") String blockerId);
20+
21+
/**
22+
* 특정 차단 관계 조회
23+
*/
24+
@Query("SELECT b FROM Block b WHERE b.blocker.id = :blockerId AND b.blocked.id = :blockedId AND b.deletedAt IS NULL")
25+
Optional<Block> findByBlockerIdAndBlockedId(@Param("blockerId") String blockerId, @Param("blockedId") String blockedId);
26+
27+
/**
28+
* 차단 관계 존재 여부 확인
29+
*/
30+
@Query("SELECT COUNT(b) > 0 FROM Block b WHERE b.blocker.id = :blockerId AND b.blocked.id = :blockedId AND b.deletedAt IS NULL")
31+
boolean existsByBlockerIdAndBlockedId(@Param("blockerId") String blockerId, @Param("blockedId") String blockedId);
32+
33+
/**
34+
* 특정 사용자가 차단한 사용자들의 ID 목록 조회
35+
*/
36+
@Query("SELECT b.blocked.id FROM Block b WHERE b.blocker.id = :blockerId AND b.deletedAt IS NULL")
37+
List<String> findBlockedUserIdsByBlockerId(@Param("blockerId") String blockerId);
38+
39+
/**
40+
* 양방향 차단 관계 확인 (A가 B를 차단했거나, B가 A를 차단한 경우)
41+
*/
42+
@Query("SELECT COUNT(b) > 0 FROM Block b WHERE " +
43+
"((b.blocker.id = :userId1 AND b.blocked.id = :userId2) OR " +
44+
"(b.blocker.id = :userId2 AND b.blocked.id = :userId1)) " +
45+
"AND b.deletedAt IS NULL")
46+
boolean existsBlockBetweenUsers(@Param("userId1") String userId1, @Param("userId2") String userId2);
47+
}
48+
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package com.taba.block.service;
2+
3+
import com.taba.block.dto.BlockedUserDto;
4+
import com.taba.block.entity.Block;
5+
import com.taba.block.repository.BlockRepository;
6+
import com.taba.common.exception.BusinessException;
7+
import com.taba.common.exception.ErrorCode;
8+
import com.taba.common.util.SecurityUtil;
9+
import com.taba.friendship.entity.Friendship;
10+
import com.taba.friendship.repository.FriendshipRepository;
11+
import com.taba.user.entity.User;
12+
import com.taba.user.repository.UserRepository;
13+
import com.taba.user.service.UserService;
14+
import lombok.RequiredArgsConstructor;
15+
import lombok.extern.slf4j.Slf4j;
16+
import org.springframework.stereotype.Service;
17+
import org.springframework.transaction.annotation.Transactional;
18+
19+
import java.util.List;
20+
import java.util.stream.Collectors;
21+
22+
@Slf4j
23+
@RequiredArgsConstructor
24+
@Service
25+
public class BlockService {
26+
27+
private final BlockRepository blockRepository;
28+
private final UserRepository userRepository;
29+
private final FriendshipRepository friendshipRepository;
30+
private final UserService userService;
31+
32+
/**
33+
* 사용자 차단
34+
* - 차단 관계 생성
35+
* - 친구 관계가 있으면 삭제 (양방향)
36+
*/
37+
@Transactional
38+
public void blockUser(String blockedUserId) {
39+
User currentUser = SecurityUtil.getCurrentUser();
40+
if (currentUser == null) {
41+
throw new BusinessException(ErrorCode.UNAUTHORIZED);
42+
}
43+
44+
// 자기 자신은 차단 불가
45+
if (currentUser.getId().equals(blockedUserId)) {
46+
throw new BusinessException(ErrorCode.INVALID_REQUEST);
47+
}
48+
49+
// 차단할 사용자 조회
50+
User blockedUser = userRepository.findActiveUserById(blockedUserId)
51+
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
52+
53+
// 이미 차단한 경우 확인
54+
if (blockRepository.existsByBlockerIdAndBlockedId(currentUser.getId(), blockedUserId)) {
55+
throw new BusinessException(ErrorCode.INVALID_REQUEST);
56+
}
57+
58+
// 차단 관계 생성
59+
Block block = Block.builder()
60+
.blocker(currentUser)
61+
.blocked(blockedUser)
62+
.build();
63+
blockRepository.save(block);
64+
65+
// 친구 관계가 있으면 삭제 (양방향)
66+
List<Friendship> friendships = friendshipRepository.findByUserIdsList(currentUser.getId(), blockedUserId);
67+
for (Friendship friendship : friendships) {
68+
if (!friendship.isDeleted()) {
69+
friendship.softDelete();
70+
friendshipRepository.save(friendship);
71+
}
72+
}
73+
74+
log.info("User {} blocked user {}", currentUser.getId(), blockedUserId);
75+
}
76+
77+
/**
78+
* 차단 해제
79+
*/
80+
@Transactional
81+
public void unblockUser(String blockedUserId) {
82+
User currentUser = SecurityUtil.getCurrentUser();
83+
if (currentUser == null) {
84+
throw new BusinessException(ErrorCode.UNAUTHORIZED);
85+
}
86+
87+
// 차단 관계 조회
88+
Block block = blockRepository.findByBlockerIdAndBlockedId(currentUser.getId(), blockedUserId)
89+
.orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REQUEST));
90+
91+
// 차단 해제 (soft delete)
92+
block.softDelete();
93+
blockRepository.save(block);
94+
95+
log.info("User {} unblocked user {}", currentUser.getId(), blockedUserId);
96+
}
97+
98+
/**
99+
* 차단한 사용자 목록 조회
100+
*/
101+
@Transactional(readOnly = true)
102+
public List<BlockedUserDto> getBlockedUsers() {
103+
User currentUser = SecurityUtil.getCurrentUser();
104+
if (currentUser == null) {
105+
throw new BusinessException(ErrorCode.UNAUTHORIZED);
106+
}
107+
108+
List<Block> blocks = blockRepository.findByBlockerId(currentUser.getId());
109+
110+
return blocks.stream()
111+
.map(block -> {
112+
User blockedUser = userService.refreshUser(block.getBlocked());
113+
return BlockedUserDto.builder()
114+
.id(blockedUser.getId())
115+
.nickname(blockedUser.getNickname())
116+
.avatarUrl(blockedUser.getAvatarUrl())
117+
.blockedAt(block.getCreatedAt())
118+
.build();
119+
})
120+
.collect(Collectors.toList());
121+
}
122+
123+
/**
124+
* 특정 사용자가 차단한 사용자 ID 목록 조회
125+
*/
126+
@Transactional(readOnly = true)
127+
public List<String> getBlockedUserIds(String userId) {
128+
return blockRepository.findBlockedUserIdsByBlockerId(userId);
129+
}
130+
131+
/**
132+
* 차단 여부 확인 (A가 B를 차단했는지)
133+
*/
134+
@Transactional(readOnly = true)
135+
public boolean isBlocked(String blockerId, String blockedId) {
136+
return blockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId);
137+
}
138+
139+
/**
140+
* 양방향 차단 여부 확인 (A가 B를 차단했거나, B가 A를 차단한 경우)
141+
*/
142+
@Transactional(readOnly = true)
143+
public boolean isBlockedBetween(String userId1, String userId2) {
144+
return blockRepository.existsBlockBetweenUsers(userId1, userId2);
145+
}
146+
}
147+

src/main/java/com/taba/letter/repository/LetterRepository.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ List<Letter> findPublicLettersExcludingUserList(
3636
@Param("excludeUserId") String excludeUserId,
3737
@Param("languages") List<String> languages);
3838

39+
/**
40+
* 공개 편지 조회 (특정 사용자 및 차단한 사용자 제외)
41+
*/
42+
@EntityGraph(attributePaths = {"sender", "images"}, type = org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH)
43+
@Query("SELECT l FROM Letter l WHERE l.visibility = 'PUBLIC' AND l.sentAt IS NOT NULL AND l.deletedAt IS NULL " +
44+
"AND l.sender.deletedAt IS NULL " +
45+
"AND l.sender.id != :excludeUserId " +
46+
"AND l.sender.id NOT IN :blockedUserIds " +
47+
"AND (:languages IS NULL OR l.language IN :languages)")
48+
List<Letter> findPublicLettersExcludingUserAndBlockedList(
49+
@Param("excludeUserId") String excludeUserId,
50+
@Param("blockedUserIds") List<String> blockedUserIds,
51+
@Param("languages") List<String> languages);
52+
3953
@EntityGraph(attributePaths = {"sender", "recipient", "images"}, type = org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH)
4054
@Query("SELECT l FROM Letter l WHERE l.sender.id = :userId AND l.deletedAt IS NULL " +
4155
"AND l.sender.deletedAt IS NULL ORDER BY l.createdAt DESC")

src/main/java/com/taba/letter/service/LetterService.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.taba.letter.service;
22

3+
import com.taba.block.repository.BlockRepository;
34
import com.taba.common.exception.BusinessException;
45
import com.taba.common.exception.ErrorCode;
56
import com.taba.common.util.SecurityUtil;
@@ -37,6 +38,7 @@ public class LetterService {
3738
private final FriendshipService friendshipService;
3839
private final NotificationService notificationService;
3940
private final com.taba.user.service.UserService userService;
41+
private final BlockRepository blockRepository;
4042

4143
@Transactional
4244
public LetterDto createLetter(LetterCreateRequest request) {
@@ -297,9 +299,19 @@ public Page<LetterDto> getPublicLetters(Pageable pageable, List<String> language
297299
// languages가 null이거나 비어있으면 null로 변환 (모든 언어 조회)
298300
List<String> languagesParam = (languages == null || languages.isEmpty()) ? null : languages;
299301

300-
// 로그인한 사용자의 경우 자신이 작성한 편지 제외
302+
// 로그인한 사용자의 경우 자신이 작성한 편지 및 차단한 사용자의 편지 제외
301303
if (currentUserId != null && !currentUserId.isEmpty()) {
302-
allLetters = letterRepository.findPublicLettersExcludingUserList(currentUserId, languagesParam);
304+
// 차단한 사용자 ID 목록 조회
305+
List<String> blockedUserIds = blockRepository.findBlockedUserIdsByBlockerId(currentUserId);
306+
307+
if (blockedUserIds != null && !blockedUserIds.isEmpty()) {
308+
// 차단한 사용자가 있으면 해당 사용자의 편지도 제외
309+
allLetters = letterRepository.findPublicLettersExcludingUserAndBlockedList(
310+
currentUserId, blockedUserIds, languagesParam);
311+
} else {
312+
// 차단한 사용자가 없으면 기존 쿼리 사용
313+
allLetters = letterRepository.findPublicLettersExcludingUserList(currentUserId, languagesParam);
314+
}
303315
} else {
304316
// 비로그인 사용자는 모든 공개 편지 조회
305317
allLetters = letterRepository.findPublicLettersList(languagesParam);

0 commit comments

Comments
 (0)