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();
}
Comment on lines 15 to 27
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Club 엔티티의 equals/hashCode 구현 확인 필요

Line 19에서 .distinct()를 사용해 중복 제거를 시도하는데, Club 엔티티가 equals()hashCode()를 올바르게 오버라이드하지 않으면 참조 동등성 비교가 되어서 중복 제거가 제대로 작동하지 않을 수 있습니다.

다음 스크립트로 Club 엔티티의 equals/hashCode 구현 여부를 확인해주세요:


🏁 Script executed:

#!/bin/bash
# Description: Check if Club entity implements equals/hashCode

# Search for Club class definition and equals/hashCode methods
ast-grep --pattern $'class Club {
  $$$
}'

echo "---"
echo "Checking for equals/hashCode methods in Club:"
rg -A 5 "public boolean equals|public int hashCode" --type java -g "**/Club.java"

Length of output: 49


#!/bin/bash

Club 엔티티 equals()/hashCode() 구현 여부 재확인

Club.java 파일 경로 확인

club_file=$(rg -l "class Club\b" -t java)
if [ -z "$club_file" ]; then
echo "Club.java 파일을 찾을 수 없습니다."
exit 0
fi
echo "Club.java 경로: $club_file"

클래스 선언부 출력

echo "--- 클래스 선언부 ---"
rg -n "class Club\b" $club_file

equals()/hashCode() 메서드 구현 여부 확인

echo "--- equals()/hashCode() 메서드 ---"
rg -nP "^(public|protected|private)\s+(boolean equals|int hashCode)\s*(" -C2 $club_file


🏁 Script executed:

#!/bin/bash
# Club.java 파일 경로 찾기
club_file=$(find src -type f -name 'Club.java' | head -n 1)
if [ -z "$club_file" ]; then
  echo "Club.java 파일을 찾을 수 없습니다."
  exit 0
fi
echo "Club.java 경로: $club_file"

# 클래스 선언부 출력
echo "--- 클래스 선언부 ---"
rg -n "class Club\b" "$club_file"

# equals()/hashCode() 메서드 구현 여부 확인
echo "--- equals()/hashCode() 구현 확인 ---"
rg -nP "^(public|protected|private)\s+(boolean\s+equals|int\s+hashCode)\s*\(" -C2 "$club_file"

Length of output: 174


Club 엔티티가 BaseEntity로부터 equals()/hashCode()를 상속받는지 확인하기 위해 BaseEntity.java를 검사해주세요. 아래 스크립트를 실행해주세요.


🏁 Script executed:

#!/bin/bash
# BaseEntity.java 파일 경로 찾기
base_file=$(find src -type f -name 'BaseEntity.java' | head -n 1)
if [ -z "$base_file" ]; then
  echo "BaseEntity.java 파일을 찾을 수 없습니다."
  exit 0
fi
echo "BaseEntity.java 경로: $base_file"

# 클래스 선언부
echo "--- 클래스 선언부 ---"
rg -n "class BaseEntity\b" "$base_file"

# Lombok @EqualsAndHashCode 어노테이션 확인
echo "--- Lombok EqualsAndHashCode 어노테이션 확인 ---"
rg -n "@EqualsAndHashCode" "$base_file"

# equals()/hashCode() 메서드 구현 여부
echo "--- equals()/hashCode() 구현 여부 확인 ---"
rg -nP "^(public|protected|private)\s+(boolean\s+equals|int\s+hashCode)\s*\(" -C2 "$base_file"

Length of output: 229


Club 엔티티에 equals()/hashCode() 오버라이드 필요
현재 src/main/java/com/WhoIsRoom/WhoIs_Server/domain/club/model/Club.java 및 BaseEntity에는 equals()/hashCode() 구현이나 Lombok 어노테이션이 없어 .distinct()가 참조 동등성으로만 동작합니다. ID나 고유 비즈니스 키를 기반으로 equals()와 hashCode()를 구현해 중복 제거가 제대로 이루어지도록 수정하세요.

🤖 Prompt for AI Agents
In
src/main/java/com/WhoIsRoom/WhoIs_Server/domain/user/dto/response/MyPageResponse.java
around lines 15–27, .distinct() on member.getClub() relies on Club's
equals()/hashCode(), but Club (and BaseEntity) lack an implementation so
distinct uses reference equality; implement equals() and hashCode() on the Club
entity (or in BaseEntity if you want reuse) based on the entity identifier or an
immutable business key: add a correct equals/hashCode pair (or use Lombok
@EqualsAndHashCode) that uses the non-null id (and fallbacks if id can be null
before persistence) to ensure logical equality and allow stream().distinct() to
remove duplicates properly.

}
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