From cf99c3cd37dc17e4e0802db897512c6c2cd7680b Mon Sep 17 00:00:00 2001 From: kdkdhoho Date: Mon, 27 Jan 2025 12:49:38 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=20=20feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20API=20=EC=9D=91=EB=8B=B5=EA=B0=92=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#343)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검색을 하는 유저가, 검색 대상 유저를 팔로우 하고 있는 지 여부를 포함 --- .../dto/search/UserSearchResponse.java | 53 ++++++++++++++----- .../user/application/service/UserService.java | 25 ++++++--- .../repository/follow/FollowRepository.java | 8 +-- .../user/custom/CustomUserRepository.java | 5 +- .../custom/impl/CustomUserRepositoryImpl.java | 5 +- .../follow/FollowAcceptanceTestHelper.java | 38 ++++++------- .../acceptance/user/UserAcceptanceTest.java | 33 ++++++++++-- 7 files changed, 117 insertions(+), 50 deletions(-) diff --git a/src/main/java/com/listywave/user/application/dto/search/UserSearchResponse.java b/src/main/java/com/listywave/user/application/dto/search/UserSearchResponse.java index 49663e76..4336f562 100644 --- a/src/main/java/com/listywave/user/application/dto/search/UserSearchResponse.java +++ b/src/main/java/com/listywave/user/application/dto/search/UserSearchResponse.java @@ -1,24 +1,53 @@ package com.listywave.user.application.dto.search; import java.util.List; -import lombok.Builder; +import java.util.Map; -@Builder public record UserSearchResponse( - List users, + List users, Long totalCount, Boolean hasNext ) { - public static UserSearchResponse of( - List users, - Long totalCount, - Boolean hasNext + public static UserSearchResponse createWithoutLogin(List userSearchResults, Long totalCount, Boolean hasNext) { + return new UserSearchResponse( + userSearchResults.stream().map(UserDto::from).toList(), + totalCount, + hasNext + ); + } + + public static UserSearchResponse createWithLogin(Map 팔로우_유무, Long totalCount, Boolean hasNext) { + return new UserSearchResponse( + 팔로우_유무.entrySet().stream().map(UserDto::fromEntry).toList(), + totalCount, + hasNext + ); + } + + public record UserDto( + Long id, + String nickname, + String profileImageUrl, + boolean isFollowing // 검색하는 자가 검색 대상인 유저를 팔로우하고 있는 지에 대한 여부입니다. ) { - return UserSearchResponse.builder() - .users(users) - .totalCount(totalCount) - .hasNext(hasNext) - .build(); + + public static UserDto from(UserSearchResult userSearchResult) { + return new UserDto( + userSearchResult.id, + userSearchResult.nickname, + userSearchResult.profileImageUrl, + false + ); + } + + public static UserDto fromEntry(Map.Entry entry) { + return new UserDto( + entry.getKey().id, + entry.getKey().nickname, + entry.getKey().profileImageUrl, + entry.getValue() + ); + } } } diff --git a/src/main/java/com/listywave/user/application/service/UserService.java b/src/main/java/com/listywave/user/application/service/UserService.java index 5c63abcc..fe711be5 100644 --- a/src/main/java/com/listywave/user/application/service/UserService.java +++ b/src/main/java/com/listywave/user/application/service/UserService.java @@ -4,6 +4,7 @@ import static com.listywave.common.exception.ErrorCode.ALREADY_NOT_FOLLOWED_EXCEPTION; import static com.listywave.common.exception.ErrorCode.DUPLICATE_NICKNAME_EXCEPTION; import static com.listywave.common.exception.ErrorCode.INVALID_ACCESS; +import static java.util.stream.Collectors.toMap; import com.listywave.alarm.application.domain.AlarmCreateEvent; import com.listywave.common.exception.CustomException; @@ -22,6 +23,8 @@ import com.listywave.user.repository.user.UserRepository; import com.listywave.user.repository.user.elastic.UserElasticRepository; import java.util.List; +import java.util.Map; +import java.util.function.Function; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Pageable; @@ -58,17 +61,23 @@ public UserInfoResponse getUserInfo(Long targetUserId, Long loginUserId) { @Transactional(readOnly = true) public UserSearchResponse searchUser(Long loginUserId, String search, Pageable pageable) { + Slice searchResult = userRepository.findAllBySearch(search, pageable, loginUserId); + Long count = userRepository.countBySearch(search, loginUserId); + if (loginUserId == null) { - return createUserSearchResponse(null, search, pageable); + return UserSearchResponse.createWithoutLogin(searchResult.getContent(), count, searchResult.hasNext()); } - User user = userRepository.getById(loginUserId); - return createUserSearchResponse(user.getId(), search, pageable); - } - private UserSearchResponse createUserSearchResponse(Long loginUserId, String search, Pageable pageable) { - Long count = userRepository.countBySearch(search, loginUserId); - Slice result = userRepository.findAllBySearch(search, pageable, loginUserId); - return UserSearchResponse.of(result.getContent(), count, result.hasNext()); + User 검색하는_유저 = userRepository.getById(loginUserId); + Map 팔로우_유무 = searchResult.getContent().stream() + .collect(toMap( + Function.identity(), + userSearchResult -> { + User 검색_대상_유저 = userRepository.getById(userSearchResult.getId()); + return followRepository.existsByFollowerUserAndFollowingUser(검색하는_유저, 검색_대상_유저); + } + )); + return UserSearchResponse.createWithLogin(팔로우_유무, count, searchResult.hasNext()); } public FollowingsResponse getFollowings(Long followerUserId, String search) { diff --git a/src/main/java/com/listywave/user/repository/follow/FollowRepository.java b/src/main/java/com/listywave/user/repository/follow/FollowRepository.java index 68378a85..bcf0ac07 100644 --- a/src/main/java/com/listywave/user/repository/follow/FollowRepository.java +++ b/src/main/java/com/listywave/user/repository/follow/FollowRepository.java @@ -8,11 +8,11 @@ public interface FollowRepository extends JpaRepository, CustomFollowRepository { - List getAllByFollowerUser(User followerUser); + List getAllByFollowerUser(User 팔로우_하는_유저); - List getAllByFollowingUser(User followingUser); + List getAllByFollowingUser(User 팔로우_당하는_유저); - void deleteByFollowingUserAndFollowerUser(User following, User follower); + void deleteByFollowingUserAndFollowerUser(User 팔로우_하는_유저, User 팔로우_당하는_유저); - boolean existsByFollowerUserAndFollowingUser(User followerUser, User followingUser); + boolean existsByFollowerUserAndFollowingUser(User 팔로우_하는_유저, User 팔로우_당하는_유저); } diff --git a/src/main/java/com/listywave/user/repository/user/custom/CustomUserRepository.java b/src/main/java/com/listywave/user/repository/user/custom/CustomUserRepository.java index 50c7ca72..e1a21809 100644 --- a/src/main/java/com/listywave/user/repository/user/custom/CustomUserRepository.java +++ b/src/main/java/com/listywave/user/repository/user/custom/CustomUserRepository.java @@ -2,6 +2,7 @@ import com.listywave.user.application.domain.User; import com.listywave.user.application.dto.search.UserSearchResult; +import jakarta.annotation.Nullable; import java.util.List; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -10,9 +11,9 @@ public interface CustomUserRepository { List getRecommendUsers(List myFollowingUsers, User user); - Long countBySearch(String search, Long loginUserId); + Long countBySearch(String search, @Nullable Long loginUserId); - Slice findAllBySearch(String search, Pageable pageable, Long loginUserId); + Slice findAllBySearch(String search, Pageable pageable, @Nullable Long loginUserId); void deleteNDaysAgo(int n); } diff --git a/src/main/java/com/listywave/user/repository/user/custom/impl/CustomUserRepositoryImpl.java b/src/main/java/com/listywave/user/repository/user/custom/impl/CustomUserRepositoryImpl.java index 824e5288..7c981d89 100644 --- a/src/main/java/com/listywave/user/repository/user/custom/impl/CustomUserRepositoryImpl.java +++ b/src/main/java/com/listywave/user/repository/user/custom/impl/CustomUserRepositoryImpl.java @@ -11,6 +11,7 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.annotation.Nullable; import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; @@ -53,7 +54,7 @@ private BooleanExpression userIdNotEqual(User me) { } @Override - public Long countBySearch(String search, Long loginUserId) { + public Long countBySearch(String search, @Nullable Long loginUserId) { if (search.isEmpty()) { return 0L; } @@ -73,7 +74,7 @@ private BooleanExpression userIdNe(Long loginUserId) { } @Override - public Slice findAllBySearch(String search, Pageable pageable, Long loginUserId) { + public Slice findAllBySearch(String search, Pageable pageable, @Nullable Long loginUserId) { if (search.isEmpty()) { return new SliceImpl<>(List.of(), pageable, false); } diff --git a/src/test/java/com/listywave/acceptance/follow/FollowAcceptanceTestHelper.java b/src/test/java/com/listywave/acceptance/follow/FollowAcceptanceTestHelper.java index 57d93256..6b3ec477 100644 --- a/src/test/java/com/listywave/acceptance/follow/FollowAcceptanceTestHelper.java +++ b/src/test/java/com/listywave/acceptance/follow/FollowAcceptanceTestHelper.java @@ -8,56 +8,56 @@ public abstract class FollowAcceptanceTestHelper { - public static ExtractableResponse 팔로우_요청_API(String accessToken, Long userId) { + public static ExtractableResponse 팔로우_요청_API(String 팔로우를_하는_유저의_액세스토큰, Long 팔로우_대상_유저_ID) { return given() - .header(AUTHORIZATION, "Bearer " + accessToken) - .when().post("/follow/{userId}", userId) + .header(AUTHORIZATION, "Bearer " + 팔로우를_하는_유저의_액세스토큰) + .when().post("/follow/{userId}", 팔로우_대상_유저_ID) .then().log().all() .extract(); } - public static ExtractableResponse 팔로우_취소_API(String accessToken, Long userId) { + public static ExtractableResponse 팔로우_취소_API(String 팔로우_취소를_하는_유저의_액세스토큰, Long 팔로우_취소_대상_유저_ID) { return given() - .header(AUTHORIZATION, "Bearer " + accessToken) - .when().delete("/follow/{userId}", userId) + .header(AUTHORIZATION, "Bearer " + 팔로우_취소를_하는_유저의_액세스토큰) + .when().delete("/follow/{userId}", 팔로우_취소_대상_유저_ID) .then().log().all() .extract(); } - public static ExtractableResponse 팔로워_목록_조회_API(Long userId) { + public static ExtractableResponse 팔로워_목록_조회_API(Long 조회하려는_유저_ID) { return given() - .when().get("/users/{userId}/followers", userId) + .when().get("/users/{userId}/followers", 조회하려는_유저_ID) .then().log().all() .extract(); } - public static ExtractableResponse 팔로워_검색_API(Long userId, String search) { + public static ExtractableResponse 팔로워_검색_API(Long 검색하려는_유저_ID, String 검색어) { return given() - .queryParam("search", search) - .when().get("/users/{userId}/followers", userId) + .queryParam("search", 검색어) + .when().get("/users/{userId}/followers", 검색하려는_유저_ID) .then().log().all() .extract(); } - public static ExtractableResponse 팔로잉_목록_조회_API(Long userId) { + public static ExtractableResponse 팔로잉_목록_조회_API(Long 조회하려는_유저_ID) { return given() - .when().get("/users/{userId}/followings", userId) + .when().get("/users/{userId}/followings", 조회하려는_유저_ID) .then().log().all() .extract(); } - public static ExtractableResponse 팔로잉_검색_API(Long userId, String search) { + public static ExtractableResponse 팔로잉_검색_API(Long 검색하려는_유저_ID, String 검색어) { return given() - .queryParam("search", search) - .when().get("/users/{userId}/followings", userId) + .queryParam("search", 검색어) + .when().get("/users/{userId}/followings", 검색하려는_유저_ID) .then().log().all() .extract(); } - public static ExtractableResponse 팔로워_삭제_API(String accessToken, Long followerId) { + public static ExtractableResponse 팔로워_삭제_API(String 삭제를_수행하려는_유저의_액세스토큰, Long 삭제의_대상이_되는_유저의_ID) { return given() - .header(AUTHORIZATION, "Bearer " + accessToken) - .when().delete("/followers/{userId}", followerId) + .header(AUTHORIZATION, "Bearer " + 삭제를_수행하려는_유저의_액세스토큰) + .when().delete("/followers/{userId}", 삭제의_대상이_되는_유저의_ID) .then().log().all() .extract(); } diff --git a/src/test/java/com/listywave/acceptance/user/UserAcceptanceTest.java b/src/test/java/com/listywave/acceptance/user/UserAcceptanceTest.java index ac5402d3..8f0c9091 100644 --- a/src/test/java/com/listywave/acceptance/user/UserAcceptanceTest.java +++ b/src/test/java/com/listywave/acceptance/user/UserAcceptanceTest.java @@ -26,10 +26,13 @@ import com.listywave.acceptance.common.AcceptanceTest; import com.listywave.list.application.dto.response.ListCreateResponse; +import com.listywave.user.application.domain.User; import com.listywave.user.application.dto.UserInfoResponse; import com.listywave.user.application.dto.UsersRecommendedResponse; import com.listywave.user.application.dto.search.UserSearchResponse; import io.restassured.common.mapper.TypeRef; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -112,11 +115,35 @@ class 회원_검색 { 회원을_저장한다(유진()); // when - var 결과 = 비회원이_사용자_검색(동호.getNickname()).as(UserSearchResponse.class); + UserSearchResponse 결과 = 비회원이_사용자_검색(동호.getNickname()).as(UserSearchResponse.class); // then - assertThat(결과.totalCount()).isOne(); - assertThat(결과.users().get(0).getNickname()).isEqualTo(동호.getNickname()); + assertAll( + () -> assertThat(결과.totalCount()).isOne(), + () -> assertThat(결과.users().get(0).nickname()).isEqualTo(동호.getNickname()), + () -> assertThat(결과.users().get(0).isFollowing()).isFalse() + ); + } + + @Test + void 검색_대상_회원이_내가_현재_팔로우하고_있는_유저인_경우() { + // given + User 동호 = 회원을_저장한다(동호()); + User 정수 = 회원을_저장한다(정수()); + String 정수의_액세스_토큰 = 액세스_토큰을_발급한다(정수); + + 팔로우_요청_API(정수의_액세스_토큰, 동호.getId()); + + // when + ExtractableResponse 응답 = 회원이_사용자_검색(정수의_액세스_토큰, "kdkdhoho"); + UserSearchResponse 결과 = 응답.as(UserSearchResponse.class); + + // then + assertAll( + () -> assertThat(결과.totalCount()).isOne(), + () -> assertThat(결과.users().get(0).id()).isEqualTo(동호.getId()), + () -> assertThat(결과.users().get(0).isFollowing()).isTrue() + ); } @Test From c7c857b90e8f33d09edd7bbf877f3eccf5e5bd33 Mon Sep 17 00:00:00 2001 From: kdkdhoho Date: Tue, 28 Jan 2025 14:12:12 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=20feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B5=AC=ED=98=84=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#343)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/application/service/UserService.java | 22 ++++++++++++------- .../repository/follow/FollowRepository.java | 8 +++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/listywave/user/application/service/UserService.java b/src/main/java/com/listywave/user/application/service/UserService.java index fe711be5..918eb393 100644 --- a/src/main/java/com/listywave/user/application/service/UserService.java +++ b/src/main/java/com/listywave/user/application/service/UserService.java @@ -4,7 +4,6 @@ import static com.listywave.common.exception.ErrorCode.ALREADY_NOT_FOLLOWED_EXCEPTION; import static com.listywave.common.exception.ErrorCode.DUPLICATE_NICKNAME_EXCEPTION; import static com.listywave.common.exception.ErrorCode.INVALID_ACCESS; -import static java.util.stream.Collectors.toMap; import com.listywave.alarm.application.domain.AlarmCreateEvent; import com.listywave.common.exception.CustomException; @@ -25,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.function.Function; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Pageable; @@ -69,15 +69,21 @@ public UserSearchResponse searchUser(Long loginUserId, String search, Pageable p } User 검색하는_유저 = userRepository.getById(loginUserId); - Map 팔로우_유무 = searchResult.getContent().stream() - .collect(toMap( + List 검색_결과_유저_ID_리스트 = searchResult.getContent().stream() + .map(UserSearchResult::getId) + .toList(); + List 검색하는_유저가_팔로우하고_있는_검색_결과_유저_ID_리스트 = followRepository.검색하는_유저가_검색_결과_유저_중_팔로우하고_있는_유저만을_조회한다(검색하는_유저, 검색_결과_유저_ID_리스트).stream() + .map(Follow::getFollowingUser) + .map(User::getId) + .toList(); + + Map 회원_검색_결과와_팔로우_여부 = searchResult.getContent().stream() + .collect(Collectors.toMap( Function.identity(), - userSearchResult -> { - User 검색_대상_유저 = userRepository.getById(userSearchResult.getId()); - return followRepository.existsByFollowerUserAndFollowingUser(검색하는_유저, 검색_대상_유저); - } + userSearchResult -> 검색하는_유저가_팔로우하고_있는_검색_결과_유저_ID_리스트.contains(userSearchResult.getId()) )); - return UserSearchResponse.createWithLogin(팔로우_유무, count, searchResult.hasNext()); + + return UserSearchResponse.createWithLogin(회원_검색_결과와_팔로우_여부, count, searchResult.hasNext()); } public FollowingsResponse getFollowings(Long followerUserId, String search) { diff --git a/src/main/java/com/listywave/user/repository/follow/FollowRepository.java b/src/main/java/com/listywave/user/repository/follow/FollowRepository.java index bcf0ac07..3b798af8 100644 --- a/src/main/java/com/listywave/user/repository/follow/FollowRepository.java +++ b/src/main/java/com/listywave/user/repository/follow/FollowRepository.java @@ -5,6 +5,7 @@ import com.listywave.user.repository.follow.custom.CustomFollowRepository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface FollowRepository extends JpaRepository, CustomFollowRepository { @@ -15,4 +16,11 @@ public interface FollowRepository extends JpaRepository, CustomFol void deleteByFollowingUserAndFollowerUser(User 팔로우_하는_유저, User 팔로우_당하는_유저); boolean existsByFollowerUserAndFollowingUser(User 팔로우_하는_유저, User 팔로우_당하는_유저); + + @Query(""" + select f + from Follow f + where f.followerUser = :검색하는_유저 and f.followingUser.isDelete = false and f.followingUser.id in :검색_대상_유저_ID_리스트 + """) + List 검색하는_유저가_검색_결과_유저_중_팔로우하고_있는_유저만을_조회한다(User 검색하는_유저, List 검색_대상_유저_ID_리스트); } From 06dc59da8bf64397bfa6a8ca4adc6addc099a0fe Mon Sep 17 00:00:00 2001 From: kdkdhoho Date: Tue, 28 Jan 2025 14:37:55 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=20refactor:=20JPQL=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=98=EC=97=AC=20List=20=EB=8C=80=EC=8B=A0=20Set=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EA=B2=80=EC=83=89=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20(#3?= =?UTF-8?q?43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../listywave/user/application/service/UserService.java | 7 ++----- .../listywave/user/repository/follow/FollowRepository.java | 5 +++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/listywave/user/application/service/UserService.java b/src/main/java/com/listywave/user/application/service/UserService.java index 918eb393..55b945ca 100644 --- a/src/main/java/com/listywave/user/application/service/UserService.java +++ b/src/main/java/com/listywave/user/application/service/UserService.java @@ -23,6 +23,7 @@ import com.listywave.user.repository.user.elastic.UserElasticRepository; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -72,11 +73,7 @@ public UserSearchResponse searchUser(Long loginUserId, String search, Pageable p List 검색_결과_유저_ID_리스트 = searchResult.getContent().stream() .map(UserSearchResult::getId) .toList(); - List 검색하는_유저가_팔로우하고_있는_검색_결과_유저_ID_리스트 = followRepository.검색하는_유저가_검색_결과_유저_중_팔로우하고_있는_유저만을_조회한다(검색하는_유저, 검색_결과_유저_ID_리스트).stream() - .map(Follow::getFollowingUser) - .map(User::getId) - .toList(); - + Set 검색하는_유저가_팔로우하고_있는_검색_결과_유저_ID_리스트 = followRepository.검색하는_유저가_검색_결과_유저_중_팔로우하고_있는_유저만을_조회한다(검색하는_유저, 검색_결과_유저_ID_리스트); Map 회원_검색_결과와_팔로우_여부 = searchResult.getContent().stream() .collect(Collectors.toMap( Function.identity(), diff --git a/src/main/java/com/listywave/user/repository/follow/FollowRepository.java b/src/main/java/com/listywave/user/repository/follow/FollowRepository.java index 3b798af8..cbbc0d9e 100644 --- a/src/main/java/com/listywave/user/repository/follow/FollowRepository.java +++ b/src/main/java/com/listywave/user/repository/follow/FollowRepository.java @@ -4,6 +4,7 @@ import com.listywave.user.application.domain.User; import com.listywave.user.repository.follow.custom.CustomFollowRepository; import java.util.List; +import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -18,9 +19,9 @@ public interface FollowRepository extends JpaRepository, CustomFol boolean existsByFollowerUserAndFollowingUser(User 팔로우_하는_유저, User 팔로우_당하는_유저); @Query(""" - select f + select f.followingUser.id from Follow f where f.followerUser = :검색하는_유저 and f.followingUser.isDelete = false and f.followingUser.id in :검색_대상_유저_ID_리스트 """) - List 검색하는_유저가_검색_결과_유저_중_팔로우하고_있는_유저만을_조회한다(User 검색하는_유저, List 검색_대상_유저_ID_리스트); + Set 검색하는_유저가_검색_결과_유저_중_팔로우하고_있는_유저만을_조회한다(User 검색하는_유저, List 검색_대상_유저_ID_리스트); }