From e60408585d08e00c2e6133b1f6cd8814ba73b864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EC=B0=BD=EC=97=BD?= Date: Fri, 16 Feb 2024 00:20:04 +0900 Subject: [PATCH] [BE] Feat #217: Hangul Initial Constant Search - User search feature with Hangul Initial Constant Change-Id: I24564d36b03bfd150cbcd3472cbece8442bc064a --- .../ggumtle/common/handler/HangulHandler.java | 60 +++++++++++++++++++ .../ggumtle/repository/UserRepository.java | 6 +- .../ggums/ggumtle/service/UserService.java | 49 ++++++++++++--- 3 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 backend/src/main/java/com/ggums/ggumtle/common/handler/HangulHandler.java diff --git a/backend/src/main/java/com/ggums/ggumtle/common/handler/HangulHandler.java b/backend/src/main/java/com/ggums/ggumtle/common/handler/HangulHandler.java new file mode 100644 index 00000000..7e14ad23 --- /dev/null +++ b/backend/src/main/java/com/ggums/ggumtle/common/handler/HangulHandler.java @@ -0,0 +1,60 @@ +package com.ggums.ggumtle.common.handler; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class HangulHandler { + + private static final int HANGUL_UNICODE_START = 44032; + + private static final int HANGUL_UNICODE_END = 55203; + + private static final int INITIAL_CONSONANT_UNICODE_START = 12593; + + private static final int INITIAL_CONSONANT_UNICODE_END = 12622; + + private static final char[] INITIAL_CONSONANT_UNICODES = { + 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', + 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' + }; + + public static List separateInitialConsonants(String word) { + List initials = new ArrayList<>(); + for (char c : word.toCharArray()) { + if (isHangul(c)) { + int consonantIndex = (c - HANGUL_UNICODE_START) / 28 / 21; + char initialConsonant = INITIAL_CONSONANT_UNICODES[consonantIndex]; + initials.add(String.valueOf(initialConsonant)); + } else { + initials.add(String.valueOf(c)); + } + } + for(String initial : initials){ + System.out.println(initial); + } + return initials; + } + + public static String getFirstInitialConsonant(String word) { + if (word == null || word.isEmpty()) { + return ""; + } + + char firstChar = word.charAt(0); + if (isHangul(firstChar)) { + int consonantIndex = (firstChar - HANGUL_UNICODE_START) / 28 / 21; + if (consonantIndex >= 0 && consonantIndex < INITIAL_CONSONANT_UNICODES.length) { + return String.valueOf(INITIAL_CONSONANT_UNICODES[consonantIndex]); + } + } + + return String.valueOf(firstChar); + } + + private static boolean isHangul(char c) { + return c >= HANGUL_UNICODE_START && c <= HANGUL_UNICODE_END; + } +} diff --git a/backend/src/main/java/com/ggums/ggumtle/repository/UserRepository.java b/backend/src/main/java/com/ggums/ggumtle/repository/UserRepository.java index 40bb443a..01bff794 100644 --- a/backend/src/main/java/com/ggums/ggumtle/repository/UserRepository.java +++ b/backend/src/main/java/com/ggums/ggumtle/repository/UserRepository.java @@ -1,16 +1,20 @@ package com.ggums.ggumtle.repository; import com.ggums.ggumtle.entity.User; +import io.lettuce.core.dynamic.annotation.Param; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { Optional findByIdAndDeletedDateIsNull(Long userId); Optional findByUserNickname(String nickname); - Page findByUserNicknameContainingAndDeletedDateIsNull(String userNickname, Pageable pageable); + + List findAllByDeletedDateIsNull(); } diff --git a/backend/src/main/java/com/ggums/ggumtle/service/UserService.java b/backend/src/main/java/com/ggums/ggumtle/service/UserService.java index 27e4af32..04e27b1f 100644 --- a/backend/src/main/java/com/ggums/ggumtle/service/UserService.java +++ b/backend/src/main/java/com/ggums/ggumtle/service/UserService.java @@ -1,6 +1,7 @@ package com.ggums.ggumtle.service; import com.ggums.ggumtle.common.handler.AlarmHandler; +import com.ggums.ggumtle.common.handler.HangulHandler; import com.ggums.ggumtle.common.handler.ImageHandler; import com.ggums.ggumtle.common.handler.TransactionHandler; import com.ggums.ggumtle.common.jwt.JwtTokenManager; @@ -19,9 +20,9 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.server.authorization.AuthorizationWebFilter; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -52,6 +53,7 @@ public class UserService { private final JwtTokenManager jwtTokenManager; private final ImageHandler imageHandler; private final AlarmHandler alarmHandler; + private final HangulHandler hangulHandler; private final PasswordEncoder passwordEncoder; public String updateUser(User user, MultipartFile userImage, UserUpdateRequestDto requestDto){ @@ -158,10 +160,45 @@ public String representativeBucket(User user, Long bucketId){ @Transactional(readOnly = true) public UserListResponseDto searchUsers(String word, Pageable pageable, User currentUser) { - Page users = userRepository.findByUserNicknameContainingAndDeletedDateIsNull(word, pageable); + List allUsers = userRepository.findAllByDeletedDateIsNull(); + + List searchKeywords = HangulHandler.separateInitialConsonants(word); + + List filteredUsers = allUsers.stream() + .filter(user -> { + String userNickname = user.getUserNickname(); + + if (userNickname.length() < searchKeywords.size()) { + return false; + } + + List userInitials = new ArrayList<>(); + for (char c : userNickname.toCharArray()) { + String initialConsonant = HangulHandler.getFirstInitialConsonant(String.valueOf(c)); + userInitials.add(initialConsonant); + } + + int keywordIndex = 0; + for (String userInitial : userInitials) { + if (userInitial.equals(searchKeywords.get(keywordIndex))) { + keywordIndex++; + } + if (keywordIndex == searchKeywords.size()) { + return true; + } + } + return false; + }) + .collect(Collectors.toList()); + + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), filteredUsers.size()); + List paginatedUsers = filteredUsers.subList(start, end); + Page users = new PageImpl<>(paginatedUsers, pageable, filteredUsers.size()); + List userIds = users.getContent().stream() - .filter(u -> !u.getId().equals(currentUser.getId())) - .map(User::getId) // only one user needs to be filtered + .map(User::getId) + .filter(id -> !id.equals(currentUser.getId())) .collect(Collectors.toList()); Map followingMap = getFollowingMap(currentUser, userIds); @@ -170,10 +207,6 @@ public UserListResponseDto searchUsers(String word, Pageable pageable, User curr return UserListResponseDto.builder().searchList(searchList).build(); } - private Map getRepresentativeBucketsMap(List userIds) { - List repBucket = bucketRepository.findByUserIdIn(userIds); - return repBucket.stream().collect(Collectors.toConcurrentMap(rb -> rb.getUser().getId(), Function.identity())); - } private Map getFollowingMap(User currentUser, List userIds) { List follows = followRepository.findByFollowerIdAndFolloweeIdIn(currentUser.getId(), userIds);