-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat[ 마이페이지 기능 #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feat[ 마이페이지 기능 #12
Changes from all commits
dd9851a
558b89a
9457191
cf3fd82
9e4b0ef
aaabe66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
|---|---|---|
| @@ -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 |
|---|---|---|
|
|
@@ -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,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) { | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 닉네임 업데이트 로직에 null 안전성 검증이 필요합니다. Line 106에서 컨트롤러나 DTO 레벨에서 // 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; | ||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 인증 객체와 타입 검증이 누락되었습니다.
다음과 같이 안전하게 처리하는 것을 권장합니다: - 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
Suggested change
|
||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Club 엔티티의 equals/hashCode 구현 확인 필요
Line 19에서
.distinct()를 사용해 중복 제거를 시도하는데,Club엔티티가equals()와hashCode()를 올바르게 오버라이드하지 않으면 참조 동등성 비교가 되어서 중복 제거가 제대로 작동하지 않을 수 있습니다.다음 스크립트로
Club엔티티의 equals/hashCode 구현 여부를 확인해주세요:🏁 Script executed:
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:
Length of output: 174
Club 엔티티가 BaseEntity로부터 equals()/hashCode()를 상속받는지 확인하기 위해 BaseEntity.java를 검사해주세요. 아래 스크립트를 실행해주세요.
🏁 Script executed:
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