Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,4 @@ public BaseResponse<Void> findPassword(@RequestBody MailRequest request) {
userService.sendNewPassword(request);
return BaseResponse.ok(null);
}

@PatchMapping("/password")
public BaseResponse<Void> updatePassword(@CurrentUserId Long userId,
@RequestBody PasswordRequest request) {
userService.updateMyPassword(userId, request);
return BaseResponse.ok(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {

// 인증을 안해도 되니 토큰이 필요없는 URL들 (에러: 로그인이 필요합니다)
public final static List<String> PASS_URIS = Arrays.asList(
"/api/users/signup",
"/api/auth/**"
"/api/users/signup", "/api/auth/**"
);

private static final AntPathMatcher ANT = new AntPathMatcher();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Club, Long> {
}
Original file line number Diff line number Diff line change
@@ -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<Member, Long> {
List<Member> findByUserId(Long userId);
// 현재 유저가 속한 clubId 목록만 빠르게 가져오기
@Query("select m.club.id from Member m where m.user.id = :userId")
List<Long> 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<Long> clubIds);
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -23,4 +24,24 @@ public BaseResponse<Void> signUp(@RequestBody SignupRequest request) {
userService.signUp(request);
return BaseResponse.ok(null);
}

@GetMapping("/myPage")
public BaseResponse<MyPageResponse> getMyPage(@CurrentUserId Long userId) {
MyPageResponse response = userService.getMyPage(userId);
return BaseResponse.ok(response);
}

@PatchMapping("/myPage/update")
public BaseResponse<MyPageResponse> updateMyPage(@CurrentUserId Long userId,
@RequestBody MyPageUpdateRequest request) {
MyPageResponse response = userService.updateMyPage(userId, request);
return BaseResponse.ok(response);
}

@PatchMapping("/password")
public BaseResponse<Void> updatePassword(@CurrentUserId Long userId,
@RequestBody PasswordRequest request) {
userService.updateMyPassword(userId, request);
return BaseResponse.ok(null);
}
}
Original file line number Diff line number Diff line change
@@ -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<Long> clubList;
}
Original file line number Diff line number Diff line change
@@ -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();
}


}
Original file line number Diff line number Diff line change
@@ -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<ClubResponse> clubList;

public static MyPageResponse from(String nickname, List<Member> memberList) {

List<ClubResponse> clubList = memberList.stream()
.map(Member::getClub)
.distinct()
.map(ClubResponse::from)
.toList();

return MyPageResponse.builder()
.nickName(nickname)
.clubList(clubList)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,13 +21,21 @@
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
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) {
Expand Down Expand Up @@ -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<Member> 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<Member> 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);
}
Comment on lines +103 to +116
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

닉네임 업데이트 로직에 null 안전성 검증이 필요합니다.

Line 106에서 user.getNickName().equals(newNickName)을 호출하는데, newNickName이 null이면 정상 동작하지만 user.getNickName()이 null일 경우 NPE가 발생할 수 있습니다. 엔티티 필드가 non-null이라면 괜찮지만, 명시적인 null 검증이나 @NotNull 밸리데이션을 추가하는 것이 안전합니다.

컨트롤러나 DTO 레벨에서 @NotNull 또는 @NotBlank 검증 추가:

// MyPageUpdateRequest에서
@NotBlank(message = "닉네임은 필수입니다")
private String nickName;

또는 서비스 메서드에서 명시적 null 체크:

 private void updateUserNickName(User user, String newNickName) {
+    if (newNickName == null || newNickName.trim().isEmpty()) {
+        throw new BusinessException(ErrorCode.INVALID_INPUT);
+    }
 
     // 변경 사항이 없으면 아무것도 하지 않음 (최적화)
     if (user.getNickName().equals(newNickName)) {
         return;
     }


private void updateUserClubs(User user, List<Long> newClubIdList) {

// null이면 빈 리스트로 간주 => 모두 탈퇴 처리
Set<Long> requested = newClubIdList == null ? Set.of()
: newClubIdList.stream()
.filter(Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new)); // 순서 유지 필요시

Long userId = user.getId();

// 현재 가입된 clubId 목록
Set<Long> current = new LinkedHashSet<>(memberRepository.findClubIdsByUserId(userId));

// 계산: 추가/삭제 집합
Set<Long> toAdd = new LinkedHashSet<>(requested);
toAdd.removeAll(current);

Set<Long> toRemove = new LinkedHashSet<>(current);
toRemove.removeAll(requested);

// 삭제 먼저 (없으면 no-op)
if (!toRemove.isEmpty()) {
memberRepository.deleteByUserIdAndClubIdIn(userId, toRemove);
}

// 추가할 Club의 존재성 검증
if (!toAdd.isEmpty()) {
List<Club> clubs = clubRepository.findAllById(toAdd);

if (clubs.size() != toAdd.size()) {
// 어떤 ID는 존재X
throw new BusinessException(ErrorCode.CLUB_NOT_FOUND);
}

// Member 엔티티 생성
List<Member> newMembers = clubs.stream()
.map(club -> Member.builder()
.user(user)
.club(club)
.build())
.toList();

// 저장 (유니크 제약 (user_id, club_id) 있어도 toAdd는 중복이 아님)
memberRepository.saveAll(newMembers);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
Comment on lines +33 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

인증 객체와 타입 검증이 누락되었습니다.

SecurityContextHolder에서 Authenticationprincipal을 가져올 때 null 체크나 타입 검증이 없어서, 인증되지 않은 요청이나 예상치 못한 principal 타입이 들어오면 NullPointerException 또는 ClassCastException이 발생할 수 있습니다.

다음과 같이 안전하게 처리하는 것을 권장합니다:

-    UserPrincipal principal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
-    return principal.getUserId();
+    var authentication = SecurityContextHolder.getContext().getAuthentication();
+    if (authentication == null || !(authentication.getPrincipal() instanceof UserPrincipal)) {
+        throw new BusinessException(ErrorCode.UNAUTHORIZED);
+    }
+    UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+    return principal.getUserId();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
UserPrincipal principal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return principal.getUserId();
var authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !(authentication.getPrincipal() instanceof UserPrincipal)) {
throw new BusinessException(ErrorCode.UNAUTHORIZED);
}
UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
return principal.getUserId();

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(), "토큰 타입이 유효하지 않습니다."),
Expand Down
Loading