diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java index d02073f..71ac2b0 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java @@ -55,11 +55,4 @@ public BaseResponse findPassword(@RequestBody MailRequest request) { userService.sendNewPassword(request); return BaseResponse.ok(null); } - - @PatchMapping("/password") - public BaseResponse updatePassword(@CurrentUserId Long userId, - @RequestBody PasswordRequest request) { - userService.updateMyPassword(userId, request); - return BaseResponse.ok(null); - } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java index 0a884d9..e1ff12d 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/filter/JwtAuthenticationFilter.java @@ -40,8 +40,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { // 인증을 안해도 되니 토큰이 필요없는 URL들 (에러: 로그인이 필요합니다) public final static List PASS_URIS = Arrays.asList( - "/api/users/signup", - "/api/auth/**" + "/api/users/signup", "/api/auth/**" ); private static final AntPathMatcher ANT = new AntPathMatcher(); diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/club/repository/ClubRepository.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/club/repository/ClubRepository.java new file mode 100644 index 0000000..02fb5d2 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/club/repository/ClubRepository.java @@ -0,0 +1,7 @@ +package com.WhoIsRoom.WhoIs_Server.domain.club.repository; + +import com.WhoIsRoom.WhoIs_Server.domain.club.model.Club; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ClubRepository extends JpaRepository { +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/member/repository/MemberRepository.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/member/repository/MemberRepository.java new file mode 100644 index 0000000..62a8f25 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/member/repository/MemberRepository.java @@ -0,0 +1,21 @@ +package com.WhoIsRoom.WhoIs_Server.domain.member.repository; + +import com.WhoIsRoom.WhoIs_Server.domain.member.model.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; + +public interface MemberRepository extends JpaRepository { + List findByUserId(Long userId); + // 현재 유저가 속한 clubId 목록만 빠르게 가져오기 + @Query("select m.club.id from Member m where m.user.id = :userId") + List findClubIdsByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("delete from Member m where m.user.id = :userId and m.club.id in :clubIds") + void deleteByUserIdAndClubIdIn(@Param("userId") Long userId, @Param("clubIds") Collection clubIds); +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java index 94db5b2..89daa37 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/controller/UserController.java @@ -1,14 +1,15 @@ package com.WhoIsRoom.WhoIs_Server.domain.user.controller; +import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request.PasswordRequest; +import com.WhoIsRoom.WhoIs_Server.domain.user.dto.request.MyPageUpdateRequest; import com.WhoIsRoom.WhoIs_Server.domain.user.dto.request.SignupRequest; +import com.WhoIsRoom.WhoIs_Server.domain.user.dto.response.MyPageResponse; import com.WhoIsRoom.WhoIs_Server.domain.user.service.UserService; +import com.WhoIsRoom.WhoIs_Server.global.common.resolver.CurrentUserId; import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @Slf4j @RestController @@ -23,4 +24,24 @@ public BaseResponse signUp(@RequestBody SignupRequest request) { userService.signUp(request); return BaseResponse.ok(null); } + + @GetMapping("/myPage") + public BaseResponse getMyPage(@CurrentUserId Long userId) { + MyPageResponse response = userService.getMyPage(userId); + return BaseResponse.ok(response); + } + + @PatchMapping("/myPage/update") + public BaseResponse updateMyPage(@CurrentUserId Long userId, + @RequestBody MyPageUpdateRequest request) { + MyPageResponse response = userService.updateMyPage(userId, request); + return BaseResponse.ok(response); + } + + @PatchMapping("/password") + public BaseResponse updatePassword(@CurrentUserId Long userId, + @RequestBody PasswordRequest request) { + userService.updateMyPassword(userId, request); + return BaseResponse.ok(null); + } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/request/MyPageUpdateRequest.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/request/MyPageUpdateRequest.java new file mode 100644 index 0000000..d2feb6c --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/request/MyPageUpdateRequest.java @@ -0,0 +1,14 @@ +package com.WhoIsRoom.WhoIs_Server.domain.user.dto.request; + +import lombok.*; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class MyPageUpdateRequest { + String nickName; + List clubList; +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/response/ClubResponse.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/response/ClubResponse.java new file mode 100644 index 0000000..0da79c8 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/response/ClubResponse.java @@ -0,0 +1,21 @@ +package com.WhoIsRoom.WhoIs_Server.domain.user.dto.response; + +import com.WhoIsRoom.WhoIs_Server.domain.club.model.Club; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ClubResponse { + private Long id; + private String name; + + public static ClubResponse from(Club club) { + return ClubResponse.builder() + .id(club.getId()) + .name(club.getName()) + .build(); + } + + +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/response/MyPageResponse.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/response/MyPageResponse.java new file mode 100644 index 0000000..6787a42 --- /dev/null +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/response/MyPageResponse.java @@ -0,0 +1,28 @@ +package com.WhoIsRoom.WhoIs_Server.domain.user.dto.response; + +import com.WhoIsRoom.WhoIs_Server.domain.member.model.Member; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class MyPageResponse { + private String nickName; + private List clubList; + + public static MyPageResponse from(String nickname, List memberList) { + + List clubList = memberList.stream() + .map(Member::getClub) + .distinct() + .map(ClubResponse::from) + .toList(); + + return MyPageResponse.builder() + .nickName(nickname) + .clubList(clubList) + .build(); + } +} diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java index 7cc4e4d..b238c68 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserService.java @@ -3,7 +3,13 @@ import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request.MailRequest; import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request.PasswordRequest; import com.WhoIsRoom.WhoIs_Server.domain.auth.service.MailService; +import com.WhoIsRoom.WhoIs_Server.domain.club.model.Club; +import com.WhoIsRoom.WhoIs_Server.domain.club.repository.ClubRepository; +import com.WhoIsRoom.WhoIs_Server.domain.member.model.Member; +import com.WhoIsRoom.WhoIs_Server.domain.member.repository.MemberRepository; +import com.WhoIsRoom.WhoIs_Server.domain.user.dto.request.MyPageUpdateRequest; import com.WhoIsRoom.WhoIs_Server.domain.user.dto.request.SignupRequest; +import com.WhoIsRoom.WhoIs_Server.domain.user.dto.response.MyPageResponse; import com.WhoIsRoom.WhoIs_Server.domain.user.model.Role; import com.WhoIsRoom.WhoIs_Server.domain.user.model.User; import com.WhoIsRoom.WhoIs_Server.domain.user.repository.UserRepository; @@ -15,6 +21,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + @Slf4j @Service @RequiredArgsConstructor @@ -22,6 +34,8 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final MailService mailService; + private final MemberRepository memberRepository; + private final ClubRepository clubRepository; @Transactional public void signUp(SignupRequest request) { @@ -62,4 +76,89 @@ public void updateMyPassword(Long userId, PasswordRequest request) { } user.setPassword(passwordEncoder.encode(request.getNewPassword())); } + + @Transactional(readOnly = true) + public MyPageResponse getMyPage(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + List memberList = memberRepository.findByUserId(userId); + return MyPageResponse.from(user.getNickName(), memberList); + } + + @Transactional + public MyPageResponse updateMyPage(Long userId, MyPageUpdateRequest request) { + + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + updateUserNickName(user, request.getNickName()); + + updateUserClubs(user, request.getClubList()); + + List updatedMemberList = memberRepository.findByUserId(userId); + return MyPageResponse.from(user.getNickName(), updatedMemberList); + } + + private void updateUserNickName(User user, String newNickName) { + + // 변경 사항이 없으면 아무것도 하지 않음 (최적화) + if (user.getNickName().equals(newNickName)) { + return; + } + + // 닉네임 중복 검사 (자기 자신은 제외되므로 안전함) + if (userRepository.existsByNickName(newNickName)) { + throw new BusinessException(ErrorCode.USER_DUPLICATE_NICKNAME); + } + + user.setNickName(newNickName); + } + + private void updateUserClubs(User user, List newClubIdList) { + + // null이면 빈 리스트로 간주 => 모두 탈퇴 처리 + Set requested = newClubIdList == null ? Set.of() + : newClubIdList.stream() + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); // 순서 유지 필요시 + + Long userId = user.getId(); + + // 현재 가입된 clubId 목록 + Set current = new LinkedHashSet<>(memberRepository.findClubIdsByUserId(userId)); + + // 계산: 추가/삭제 집합 + Set toAdd = new LinkedHashSet<>(requested); + toAdd.removeAll(current); + + Set toRemove = new LinkedHashSet<>(current); + toRemove.removeAll(requested); + + // 삭제 먼저 (없으면 no-op) + if (!toRemove.isEmpty()) { + memberRepository.deleteByUserIdAndClubIdIn(userId, toRemove); + } + + // 추가할 Club의 존재성 검증 + if (!toAdd.isEmpty()) { + List clubs = clubRepository.findAllById(toAdd); + + if (clubs.size() != toAdd.size()) { + // 어떤 ID는 존재X + throw new BusinessException(ErrorCode.CLUB_NOT_FOUND); + } + + // Member 엔티티 생성 + List newMembers = clubs.stream() + .map(club -> Member.builder() + .user(user) + .club(club) + .build()) + .toList(); + + // 저장 (유니크 제약 (user_id, club_id) 있어도 toAdd는 중복이 아님) + memberRepository.saveAll(newMembers); + } + } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserIdArgumentResolver.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserIdArgumentResolver.java index c363b4c..cd36a8c 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserIdArgumentResolver.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/resolver/CurrentUserIdArgumentResolver.java @@ -1,5 +1,6 @@ package com.WhoIsRoom.WhoIs_Server.global.common.resolver; +import com.WhoIsRoom.WhoIs_Server.domain.auth.model.UserPrincipal; import com.WhoIsRoom.WhoIs_Server.domain.user.repository.UserRepository; import com.WhoIsRoom.WhoIs_Server.global.common.exception.BusinessException; import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode; @@ -29,9 +30,7 @@ public Object resolveArgument(MethodParameter parameter, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - return userRepository.findByEmail(email) - .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)) - .getId(); + UserPrincipal principal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return principal.getUserId(); } } diff --git a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java index 21891ec..8c543e0 100644 --- a/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java +++ b/src/main/java/com/WhoIsRoom/WhoIs_Server/global/common/response/ErrorCode.java @@ -23,6 +23,9 @@ public enum ErrorCode{ USER_DUPLICATE_EMAIL(201, HttpStatus.BAD_REQUEST.value(), "중복된 이메일의 시용자가 있습니다."), USER_DUPLICATE_NICKNAME(202, HttpStatus.BAD_REQUEST.value(), "중복된 닉네임의 사용자가 있습니다."), + // Club + CLUB_NOT_FOUND(300, HttpStatus.NOT_FOUND.value(), "동아리를 찾을 수 없습니다."), + // Auth SECURITY_UNAUTHORIZED(600,HttpStatus.UNAUTHORIZED.value(), "인증 정보가 유효하지 않습니다"), INVALID_TOKEN_TYPE(601, HttpStatus.UNAUTHORIZED.value(), "토큰 타입이 유효하지 않습니다."), diff --git a/src/test/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserServiceTest.java b/src/test/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserServiceTest.java new file mode 100644 index 0000000..dd3aed4 --- /dev/null +++ b/src/test/java/com/WhoIsRoom/WhoIs_Server/domain/user/service/UserServiceTest.java @@ -0,0 +1,141 @@ +package com.WhoIsRoom.WhoIs_Server.domain.user.service; + +import com.WhoIsRoom.WhoIs_Server.domain.club.model.Club; +import com.WhoIsRoom.WhoIs_Server.domain.club.repository.ClubRepository; +import com.WhoIsRoom.WhoIs_Server.domain.member.model.Member; +import com.WhoIsRoom.WhoIs_Server.domain.member.repository.MemberRepository; +import com.WhoIsRoom.WhoIs_Server.domain.user.dto.request.MyPageUpdateRequest; +import com.WhoIsRoom.WhoIs_Server.domain.user.dto.response.MyPageResponse; +import com.WhoIsRoom.WhoIs_Server.domain.user.model.User; +import com.WhoIsRoom.WhoIs_Server.domain.user.repository.UserRepository; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.Mockito.when; + +@Slf4j +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock private UserRepository userRepository; + @Mock private ClubRepository clubRepository; + @Mock private MemberRepository memberRepository; + + @InjectMocks + private UserService userService; + + private User user; + private List clubs; + + @BeforeEach + void setUp() { + System.out.println("\n[TEST] ========== setUp =========="); + user = User.builder() + .nickName("조익성") + .email("konkuk@gmail.com") + .password("1234") + .build(); + user.setId(1L); + + Club club1 = Club.builder().name("메이커스팜").build(); club1.setId(1L); + Club club2 = Club.builder().name("목방").build(); club2.setId(2L); + Club club3 = Club.builder().name("건대교지편집위원회").build(); club3.setId(3L); + Club club4 = Club.builder().name("국어국문학과").build(); club4.setId(4L); + + clubs = List.of(club1, club2, club3, club4); + + System.out.println("[TEST] userId=" + user.getId() + ", nick=" + user.getNickName()); + System.out.println("[TEST] clubs=" + clubs.stream() + .map(c -> c.getId() + ":" + c.getName()).toList()); + System.out.println("[TEST] =============================\n"); + } + + @Test + @DisplayName("닉네임과 클럽 목록을 업데이트하고 응답 DTO를 반환한다") + void updateMyPage_success() { + Long userId = user.getId(); + + MyPageUpdateRequest request = MyPageUpdateRequest.builder() + .nickName("조익성") + .clubList(List.of(1L, 2L, 3L, 4L)) + .build(); + + // --- 스텁 + 로그 --- + when(userRepository.findById(userId)) + .thenAnswer(inv -> { + System.out.println("[TEST] userRepository.findById(" + userId + ")"); + return Optional.of(user); + }); + + when(memberRepository.findClubIdsByUserId(userId)) + .thenAnswer(inv -> { + System.out.println("[TEST] memberRepository.findClubIdsByUserId(" + userId + ") -> [2]"); + return List.of(2L); + }); + + when(clubRepository.findAllById(ArgumentMatchers.anyIterable())) + .thenAnswer(invocation -> { + Iterable ids = invocation.getArgument(0); + List idList = new ArrayList<>(); + ids.forEach(idList::add); + System.out.println("[TEST] clubRepository.findAllById called with ids=" + idList); + var result = clubs.stream() + .filter(c -> idList.contains(c.getId())) + .collect(Collectors.toList()); + System.out.println("[TEST] clubRepository.findAllById returns ids=" + + result.stream().map(Club::getId).toList()); + return result; + }); + + when(memberRepository.saveAll(anyCollection())) + .thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + var c = (java.util.Collection) invocation.getArgument(0); + System.out.println("[TEST] memberRepository.saveAll called size=" + c.size() + + ", clubIds=" + c.stream().map(m -> m.getClub().getId()).toList()); + return new ArrayList<>(c); + }); + + when(memberRepository.findByUserId(userId)) + .thenAnswer(inv -> { + System.out.println("[TEST] memberRepository.findByUserId(" + userId + ")"); + var list = clubs.stream() + .map(c -> Member.builder().user(user).club(c).build()) + .collect(Collectors.toList()); + System.out.println("[TEST] memberRepository.findByUserId returns clubIds=" + + list.stream().map(m -> m.getClub().getId()).toList()); + return list; + }); + + // --- 실행 --- + System.out.println("\n[TEST] ===== call userService.updateMyPage ====="); + MyPageResponse response = userService.updateMyPage(userId, request); + System.out.println("[TEST] ===== returned MyPageResponse ====="); + System.out.println("[TEST] resp.nick=" + response.getNickName()); + System.out.println("[TEST] resp.clubs=" + response.getClubList().stream() + .map(c -> c.getId() + ":" + c.getName()).toList()); + System.out.println("[TEST] ===================================\n"); + + // --- 검증 --- + assertThat(response.getNickName()).isEqualTo("조익성"); + assertThat(response.getClubList()).hasSize(4); + assertThat(response.getClubList()) + .extracting("name") + .containsExactlyInAnyOrder("메이커스팜", "목방", "건대교지편집위원회", "국어국문학과"); + } +}