Repository: Moadong/moadong
Pull request #385: [refactor & test] 유저 정보 컬렉션 통합 및 테스트 코드 작성(윤성원) 
Author: login: Zepelown
Labels: name: 📬 API
description: 서버 API 통신 작업
name: 🔨 Refactor
description: 코드 리팩토링
name: ✅ Test
description: test 관련(storybook, jest...)
name: 💾 BE
description: Backend
Created: 2025-05-08T12:48:20Z | Merged: 2025-05-15T11:29:18Z

## #️⃣연관된 이슈

#368 

## 📝작업 내용
리팩토링
- user 컬렉션과 userinfo 컬렉션 병합
- 단위 테스트와 통합테스트 분리를 위한 어노테이션 추가
- dto 검증 시에 null 검사를 명확히 하기위해 @NotBlank로 변경
- 회원가입 시 아이디와 비밀번호가 같은 지 점검 기능 추가
- 비밀번호 검증이 올바르게 되지 않던 문제 수정
- Custom Annoation Validator 주석 추가


테스트 관련
- 로그인과 회원가입 테스트 코드 추가
- 클럽 생성 및 약력 업데이트 테스트코드 추가

차후에 해야할 작업
모집 정보 테스트코드를 시간 계산법을 LocalDateTime에서 Instant로 변경 후에 활성화할 것


## 중점적으로 리뷰받고 싶은 부분(선택)
> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요
>
> ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?


## 논의하고 싶은 부분(선택)
>논의하고 싶은 부분이 있다면 작성해주세요.

## 🫡 참고사항

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **신규 기능**
  - 단위 테스트와 통합 테스트를 분리 실행할 수 있도록 Gradle 태스크(`unitTest`, `integrationTest`) 및 관련 어노테이션(`@UnitTest`, `@IntegrationTest`)을 추가했습니다.
  - 클럽 및 사용자 도메인 테스트용 Fixture 클래스가 추가되었습니다.
  - 클럽 프로필, 사용자 로그인/회원가입에 대한 단위 테스트가 추가되었습니다.
  - 사용자 회원가입 요청에서 아이디와 비밀번호의 유효성 검증이 강화되었습니다.

- **버그 수정**
  - 사용자 회원가입 요청에서 아이디와 비밀번호의 유효성 검증이 강화되었습니다(아이디/비밀번호 동일 불가, 빈 값 불가 등).

- **리팩터링**
  - 사용자 정보 엔티티 및 회원가입 로직이 단순화되었습니다.
  - 불필요한 저장소 및 테스트 코드가 삭제되었습니다.

- **기타**
  - 테스트용 Lombok 의존성이 추가되었습니다.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/backend/build.gradle b/backend/build.gradle
index 205680c18..8c72ccfd9 100644
--- a/backend/build.gradle
+++ b/backend/build.gradle
@@ -40,8 +40,12 @@ dependencies {
 	implementation 'org.springframework.boot:spring-boot-starter-actuator'
 	compileOnly 'org.projectlombok:lombok'
 	annotationProcessor 'org.projectlombok:lombok'
+
+
 	testImplementation 'org.springframework.boot:spring-boot-starter-test'
 	testImplementation 'org.springframework.security:spring-security-test'
+	testCompileOnly 'org.projectlombok:lombok'
+	testAnnotationProcessor 'org.projectlombok:lombok'
 	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
 
 	// google drive
@@ -50,6 +54,29 @@ dependencies {
 	implementation 'com.google.apis:google-api-services-drive:v3-rev20220815-2.0.0'
 }
 
-tasks.named('test') {
+//전체 테스트
+test {
+	description = 'Runs the total tests.'
 	useJUnitPlatform()
 }
+
+//유닛 테스트
+task unitTest(type: Test) {
+	group = 'verification'
+	description = 'Runs the unit tests.'
+	useJUnitPlatform{
+		includeTags 'UnitTest'
+		excludeTags 'IntegrationTest'
+
+	}
+}
+
+//통합 테스트
+task integrationTest(type: Test) {
+	group = 'verification'
+	description = 'Runs the integration tests.'
+	useJUnitPlatform{
+		includeTags 'IntegrationTest'
+		excludeTags 'UnitTest'
+	}
+}
diff --git a/backend/src/main/java/moadong/global/validator/KoreanValidator.java b/backend/src/main/java/moadong/global/validator/KoreanValidator.java
index 1652e06e1..828eff41d 100644
--- a/backend/src/main/java/moadong/global/validator/KoreanValidator.java
+++ b/backend/src/main/java/moadong/global/validator/KoreanValidator.java
@@ -7,6 +7,8 @@
 import java.util.regex.Pattern;
 
 public class KoreanValidator implements ConstraintValidator<Korean, String> {
+    // 1 ~ 10자 오직 한글만 가능, 단 자음 또는 모음만 있는 경우는 제외
+    // TODO: 테스트 코드 작성
     private static final Pattern KOREAN_ONLY_PATTERN = Pattern.compile("^[가-힣]{1,10}$");
 
     @Override
diff --git a/backend/src/main/java/moadong/global/validator/PasswordValidator.java b/backend/src/main/java/moadong/global/validator/PasswordValidator.java
index 0e67cdebd..aa2f95adc 100644
--- a/backend/src/main/java/moadong/global/validator/PasswordValidator.java
+++ b/backend/src/main/java/moadong/global/validator/PasswordValidator.java
@@ -7,7 +7,8 @@
 import java.util.regex.Pattern;
 
 public class PasswordValidator implements ConstraintValidator<Password, String> {
-    private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^]).{8,20}$");
+    // 8 ~ 20자이고, 반드시 한 개의 알파벳, 한 개의 숫자, !@#$%^ 중 한 개가 모두 들어 갔는지 확인
+    private static final Pattern PASSWORD_PATTERN = Pattern.compile("\"^(?=.*[a-zA-Z])(?=.*\\\\d)(?=.*[!@#$%^])[a-zA-Z\\\\d!@#$%^]{8,20}$\"\n");
     @Override
     public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
         if (s == null || s.isEmpty()) {
diff --git a/backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java b/backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java
index c884066cb..282d35b06 100644
--- a/backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java
+++ b/backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java
@@ -7,6 +7,12 @@
 import java.util.regex.Pattern;
 
 public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
+    /*
+        두 개의 검사가 결합되어 있음.
+        (1) 010-1234-5678 또는 01012345678 인지 검사
+        (2) 02-123-4567 또는 031-1234-5678 인지 검사
+        TODO: 테스트 코드 작성
+     */
     private static final Pattern PHONE_NUMBER_PATTERN =
             Pattern.compile("^(01[016789]-?\\d{3,4}-?\\d{4})|(0\\d{1,2}-?\\d{3,4}-?\\d{4})$");
     @Override
diff --git a/backend/src/main/java/moadong/global/validator/UserIdValidator.java b/backend/src/main/java/moadong/global/validator/UserIdValidator.java
index a0a248385..3c5cb9b3c 100644
--- a/backend/src/main/java/moadong/global/validator/UserIdValidator.java
+++ b/backend/src/main/java/moadong/global/validator/UserIdValidator.java
@@ -7,6 +7,7 @@
 import java.util.regex.Pattern;
 
 public class UserIdValidator implements ConstraintValidator<UserId, String> {
+    // 5 ~ 20자 사이의 최소 한 개의 소문자 영어, 최소 한 개의 숫자가 포함되도록 검사. 이때, !@#$~만 포함 가능
     private static final Pattern USER_ID_PATTERN = Pattern.compile("^(?=.*[a-z])(?=.*\\d)[a-zA-Z\\d!@#$~]{5,20}$");
     @Override
     public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
diff --git a/backend/src/main/java/moadong/user/entity/User.java b/backend/src/main/java/moadong/user/entity/User.java
index 8778f7c9c..a5f2ca22d 100644
--- a/backend/src/main/java/moadong/user/entity/User.java
+++ b/backend/src/main/java/moadong/user/entity/User.java
@@ -51,6 +51,9 @@ public class User implements UserDetails {
     @Field("refreshToken")
     private RefreshToken refreshToken;
 
+    @Field("userInformation")
+    private UserInformation userInformation;
+
     @Builder.Default
     @NotNull
     private UserStatus status = UserStatus.ACTIVE;
diff --git a/backend/src/main/java/moadong/user/entity/UserInformation.java b/backend/src/main/java/moadong/user/entity/UserInformation.java
index 9ff263810..9db45316d 100644
--- a/backend/src/main/java/moadong/user/entity/UserInformation.java
+++ b/backend/src/main/java/moadong/user/entity/UserInformation.java
@@ -17,17 +17,19 @@
 @Builder
 @AllArgsConstructor
 @NoArgsConstructor
-@Document("user_informations")
 public class UserInformation {
 
     @Id
     private String id;
-    @NotNull
-    @Indexed(unique = true)
-    private String userId;
+
     @NotNull
     @Korean
     private String name;
     @PhoneNumber
     private String phoneNumber;
+
+    public UserInformation(String name, String phoneNumber) {
+        this.name = name;
+        this.phoneNumber = phoneNumber;
+    }
 }
diff --git a/backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java b/backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java
index 6cbc14506..65c1fa3a3 100644
--- a/backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java
+++ b/backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java
@@ -1,36 +1,47 @@
 package moadong.user.payload.request;
 
+import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
 import moadong.global.annotation.Korean;
 import moadong.global.annotation.Password;
 import moadong.global.annotation.PhoneNumber;
 import moadong.global.annotation.UserId;
+import moadong.global.exception.ErrorCode;
+import moadong.global.exception.RestApiException;
 import moadong.user.entity.User;
 import moadong.user.entity.UserInformation;
+import org.springframework.security.crypto.password.PasswordEncoder;
 
 public record UserRegisterRequest(
-        @NotNull
+        @NotBlank
         @UserId
         String userId,
-        @NotNull
+        @NotBlank
         @Password
         String password,
-        @NotNull
+        @NotBlank
         @Korean
         String name,
         @PhoneNumber
         String phoneNumber
 ) {
-    public User toUserEntity(String password) {
+    public UserRegisterRequest{
+        if (userId.equals(password)) {
+            throw new RestApiException(ErrorCode.USER_INVALID_FORMAT);
+        }
+    }
+
+    public User toUserEntity(PasswordEncoder passwordEncoder) {
         return User.builder()
                 .userId(userId)
-                .password(password)
+                .password(passwordEncoder.encode(password))
+                .userInformation(new UserInformation(name,phoneNumber))
                 .build();
     }
 
     public UserInformation toUserInformationEntity(String userId) {
         return UserInformation.builder()
-                .userId(userId)
                 .name(name)
                 .phoneNumber(phoneNumber)
                 .build();
diff --git a/backend/src/main/java/moadong/user/repository/UserInformationRepository.java b/backend/src/main/java/moadong/user/repository/UserInformationRepository.java
deleted file mode 100644
index fb388e3d9..000000000
--- a/backend/src/main/java/moadong/user/repository/UserInformationRepository.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package moadong.user.repository;
-
-import moadong.user.entity.UserInformation;
-import org.springframework.data.mongodb.repository.MongoRepository;
-import org.springframework.stereotype.Repository;
-
-@Repository
-public interface UserInformationRepository extends MongoRepository<UserInformation, String> {
-}
diff --git a/backend/src/main/java/moadong/user/service/UserCommandService.java b/backend/src/main/java/moadong/user/service/UserCommandService.java
index cbd90eca6..adab85964 100644
--- a/backend/src/main/java/moadong/user/service/UserCommandService.java
+++ b/backend/src/main/java/moadong/user/service/UserCommandService.java
@@ -17,7 +17,6 @@
 import moadong.user.payload.request.UserUpdateRequest;
 import moadong.user.payload.response.LoginResponse;
 import moadong.user.payload.response.RefreshResponse;
-import moadong.user.repository.UserInformationRepository;
 import moadong.user.repository.UserRepository;
 import moadong.user.util.CookieMaker;
 import org.springframework.http.ResponseCookie;
@@ -32,20 +31,17 @@
 public class UserCommandService {
 
     private final UserRepository userRepository;
-    private final UserInformationRepository userInformationRepository;
     private final AuthenticationManager authenticationManager;
     private final JwtProvider jwtProvider;
     private final PasswordEncoder passwordEncoder;
     private final ClubRepository clubRepository;
     private final CookieMaker cookieMaker;
 
-    public void registerUser(UserRegisterRequest userRegisterRequest) {
+    public User registerUser(UserRegisterRequest userRegisterRequest) {
         try {
-            String encodedPw = passwordEncoder.encode(userRegisterRequest.password());
-            User user = userRepository.save(userRegisterRequest.toUserEntity(encodedPw));
-            userInformationRepository.save(
-                userRegisterRequest.toUserInformationEntity(user.getId()));
+            User user = userRepository.save(userRegisterRequest.toUserEntity(passwordEncoder));
             createClub(user.getId());
+            return user;
         } catch (MongoWriteException e) {
             throw new RestApiException(ErrorCode.USER_ALREADY_EXIST);
         }
diff --git a/backend/src/test/java/moadong/fixture/ClubFixture.java b/backend/src/test/java/moadong/fixture/ClubFixture.java
new file mode 100644
index 000000000..d071b24fd
--- /dev/null
+++ b/backend/src/test/java/moadong/fixture/ClubFixture.java
@@ -0,0 +1,48 @@
+package moadong.fixture;
+
+import moadong.club.entity.Club;
+import moadong.club.entity.ClubRecruitmentInformation;
+import moadong.club.enums.ClubRecruitmentStatus;
+
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ClubFixture {
+    public static Club createClub(String clubId, String name) {
+        Club club = mock(Club.class);
+        when(club.getId()).thenReturn(clubId);
+        when(club.getName()).thenReturn(name);
+        return club;
+    }
+
+    public static ClubRecruitmentInformation createRecruitmentInfo(
+            String id,
+            String logo,
+            String introduction,
+            String description,
+            String presidentName,
+            String presidentTelephoneNumber,
+            LocalDateTime recruitmentStart,
+            LocalDateTime recruitmentEnd,
+            List<String> feedImages,
+            ClubRecruitmentStatus clubRecruitmentStatus) {
+        ClubRecruitmentInformation clubRecruitmentInfo = mock(ClubRecruitmentInformation.class);
+        when(clubRecruitmentInfo.getId()).thenReturn(id);
+        when(clubRecruitmentInfo.getLogo()).thenReturn(logo);
+        when(clubRecruitmentInfo.getIntroduction()).thenReturn(introduction);
+        when(clubRecruitmentInfo.getDescription()).thenReturn(description);
+        when(clubRecruitmentInfo.getPresidentName()).thenReturn(presidentName);
+        when(clubRecruitmentInfo.getPresidentTelephoneNumber()).thenReturn(presidentTelephoneNumber);
+        when(clubRecruitmentInfo.getRecruitmentStart()).thenReturn(ZonedDateTime.from(recruitmentStart));
+        when(clubRecruitmentInfo.getRecruitmentEnd()).thenReturn(ZonedDateTime.from(recruitmentEnd));
+        when(clubRecruitmentInfo.getFeedImages()).thenReturn(feedImages);
+        when(clubRecruitmentInfo.getClubRecruitmentStatus()).thenReturn(clubRecruitmentStatus);
+        return clubRecruitmentInfo;
+    }
+
+
+}
\ No newline at end of file
diff --git a/backend/src/test/java/moadong/fixture/ClubRequestFixture.java b/backend/src/test/java/moadong/fixture/ClubRequestFixture.java
new file mode 100644
index 000000000..294d64b7b
--- /dev/null
+++ b/backend/src/test/java/moadong/fixture/ClubRequestFixture.java
@@ -0,0 +1,50 @@
+package moadong.fixture;
+
+import moadong.club.payload.request.ClubInfoRequest;
+
+import java.util.List;
+import java.util.Map;
+
+public class ClubRequestFixture {
+    public static ClubInfoRequest createValidClubInfoRequest() {
+        return new ClubInfoRequest(
+                "club_123",
+                "테스트동아리",
+                "학술",
+                "프로그래밍",
+                List.of("개발", "스터디"),
+                "동아리 소개입니다.",
+                "홍길동",
+                "010-1234-5678",
+                Map.of("insta", "https://test")
+        );
+
+    }
+    //ToDo: 시간 계산법을 LocalDateTime에서 Instant로 변경 후에 활성화할 것
+//    public static ClubRecruitmentInfoUpdateRequest createValidRequest() {
+//        return new ClubRecruitmentInfoUpdateRequest(
+//                "club_123",
+//                Instant.now().plus(1, ChronoUnit.DAYS),  // 1일 후
+//                Instant.now().plus(7, ChronoUnit.DAYS),   // 7일 후
+//                "1~3학년",
+//                "새로운 모집 설명입니다."
+//        );
+//    }
+//
+//    // 커스텀 파라미터로 생성
+//    public static ClubRecruitmentInfoUpdateRequest createCustomRequest(
+//            String id,
+//            Instant start,
+//            Instant end,
+//            String target,
+//            String description
+//    ) {
+//        return new ClubRecruitmentInfoUpdateRequest(
+//                id,
+//                start,
+//                end,
+//                target,
+//                description
+//        );
+//    }
+}
diff --git a/backend/src/test/java/moadong/fixture/UserFixture.java b/backend/src/test/java/moadong/fixture/UserFixture.java
new file mode 100644
index 000000000..80ec986ca
--- /dev/null
+++ b/backend/src/test/java/moadong/fixture/UserFixture.java
@@ -0,0 +1,29 @@
+package moadong.fixture;
+
+import moadong.user.entity.User;
+import moadong.user.entity.UserInformation;
+import moadong.user.payload.CustomUserDetails;
+import moadong.user.payload.request.UserRegisterRequest;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+public class UserFixture {
+    public static final String collectUserId = "test12345";
+    public static final String collectPassword = "test12345@";
+    public static final String collectName = "테스터";
+    public static final String collectPhoneNumber = "010-1234-5678";
+
+    public static User createUser(PasswordEncoder passwordEncoder, String userId, String password, String name, String phoneNumber) {
+        return new UserRegisterRequest(userId, password, name, phoneNumber).toUserEntity(passwordEncoder);
+    }
+
+    public static CustomUserDetails createUserDetails(String userId) {
+        return new CustomUserDetails(
+                User.builder()
+                        .id(userId)
+                        .userInformation(new UserInformation(collectUserId, collectPhoneNumber))
+                        .password(collectPassword)
+                        .build()
+        );
+    }
+
+}
diff --git a/backend/src/test/java/moadong/fixture/UserRequestFixture.java b/backend/src/test/java/moadong/fixture/UserRequestFixture.java
new file mode 100644
index 000000000..5d4448951
--- /dev/null
+++ b/backend/src/test/java/moadong/fixture/UserRequestFixture.java
@@ -0,0 +1,13 @@
+package moadong.fixture;
+
+import moadong.user.payload.request.UserLoginRequest;
+import moadong.user.payload.request.UserRegisterRequest;
+
+public class UserRequestFixture {
+    public static UserRegisterRequest createUserRegisterRequest(String userId, String password, String name, String phoneNumber) {
+        return new UserRegisterRequest(userId, password, name, phoneNumber);
+    }
+    public static UserLoginRequest createUserLoginRequest(String userId, String password){
+        return new UserLoginRequest(userId,password);
+    }
+}
diff --git a/backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java b/backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java
new file mode 100644
index 000000000..55625ca2c
--- /dev/null
+++ b/backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java
@@ -0,0 +1,122 @@
+package moadong.unit.club;
+
+import moadong.club.entity.Club;
+import moadong.club.payload.request.ClubCreateRequest;
+import moadong.club.payload.request.ClubInfoRequest;
+import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest;
+import moadong.club.repository.ClubRepository;
+import moadong.club.service.ClubProfileService;
+import moadong.club.service.RecruitmentScheduler;
+import moadong.fixture.ClubFixture;
+import moadong.fixture.ClubRequestFixture;
+import moadong.global.exception.RestApiException;
+import moadong.user.payload.CustomUserDetails;
+import moadong.util.annotations.UnitTest;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.Optional;
+
+import static moadong.fixture.UserFixture.createUserDetails;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@UnitTest
+public class ClubProfileServiceTest {
+    private final String clubId = "club_123";
+    private final String userId = "user_456";
+    @Mock
+    private ClubRepository clubRepository;
+    @Mock
+    private RecruitmentScheduler recruitmentScheduler;
+    @InjectMocks
+    private ClubProfileService clubProfileService;
+
+    @Test
+    public void 정상적으로_클럽을_생성한다() {
+        // Given
+        ClubCreateRequest request = new ClubCreateRequest("테스트", "카테고리", "분과");
+        when(clubRepository.save(any())).thenReturn(mock(Club.class));
+
+        // When
+        clubProfileService.createClub(request);
+
+        // Then
+        verify(clubRepository, times(1)).save(any(Club.class));
+    }
+    @Test
+    void 정상적으로_클럽_약력을_업데이트한다() {
+        // Given
+        ClubInfoRequest request = ClubRequestFixture.createValidClubInfoRequest();
+        CustomUserDetails user = createUserDetails(userId);
+        Club mockClub = mock(Club.class);
+
+        when(clubRepository.findById(clubId)).thenReturn(Optional.of(mockClub));
+        when(mockClub.getUserId()).thenReturn(userId);
+
+        // When
+        clubProfileService.updateClubInfo(request, user);
+
+        // Then
+        verify(mockClub).update(request);
+        verify(clubRepository).save(mockClub);
+    }
+
+    @Test
+    void 클럽이_없을_땐_클럽_약력_업데이트가_실패한다() {
+        when(clubRepository.findById(any())).thenReturn(Optional.empty());
+        assertThrows(RestApiException.class,
+                () -> clubProfileService.updateClubInfo(ClubRequestFixture.createValidClubInfoRequest(), createUserDetails(userId)));
+    }
+
+    @Test
+    void 권한이_없는_클럽은_클럽_약력_업데이트_할_수_없다() {
+        Club mockClub = mock(Club.class);
+        when(clubRepository.findById(clubId)).thenReturn(Optional.of(mockClub));
+        when(mockClub.getUserId()).thenReturn("different_user");
+
+        assertThrows(RestApiException.class,
+                () -> clubProfileService.updateClubInfo(ClubRequestFixture.createValidClubInfoRequest(), createUserDetails(userId)));
+    }
+
+
+//    ToDo: 시간 계산법을 LocalDateTime에서 Instant로 변경 후에 활성화할 것
+//    @Test
+//    void updateClubRecruitmentInfo_WithDates_SchedulesRecruitment() {
+//        // Given
+//        Instant start = Instant.now().plus(1, ChronoUnit.DAYS);
+//        Instant end = Instant.now().plus(7,ChronoUnit.DAYS);
+//        ClubRecruitmentInfoUpdateRequest request = new ClubRecruitmentInfoUpdateRequest(
+//               "testId",start,end,"모집대상테스트","테스트용 설명" );
+//        Club mockClub = mock(Club.class);
+//
+//        when(clubRepository.findById(clubId)).thenReturn(Optional.of(mockClub));
+//        when(mockClub.getUserId()).thenReturn(userId);
+//
+//        // When
+//        clubProfileService.updateClubRecruitmentInfo(request, createUserDetails(userId));
+//
+//        // Then
+//        verify(recruitmentScheduler).scheduleRecruitment(clubId, start, end);
+//        verify(mockClub).update(request);
+//    }
+//
+//    @Test
+//    void updateClubRecruitmentInfo_NoDates_NoScheduling() {
+//        // Given
+//        ClubRecruitmentInfoUpdateRequest request = new ClubRecruitmentInfoUpdateRequest(
+//                clubId, null, null, /* other fields */);
+//
+//        // When
+//        clubProfileService.updateClubRecruitmentInfo(request, createUserDetails(userId));
+//
+//        // Then
+//        verifyNoInteractions(recruitmentScheduler);
+//    }
+
+}
diff --git a/backend/src/test/java/moadong/unit/user/UserLoginTest.java b/backend/src/test/java/moadong/unit/user/UserLoginTest.java
new file mode 100644
index 000000000..cf943ce59
--- /dev/null
+++ b/backend/src/test/java/moadong/unit/user/UserLoginTest.java
@@ -0,0 +1,113 @@
+package moadong.unit.user;
+
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import moadong.club.entity.Club;
+import moadong.club.repository.ClubRepository;
+import moadong.global.exception.RestApiException;
+import moadong.global.util.JwtProvider;
+import moadong.user.entity.RefreshToken;
+import moadong.user.entity.User;
+import moadong.user.payload.CustomUserDetails;
+import moadong.user.payload.request.UserLoginRequest;
+import moadong.user.payload.response.LoginResponse;
+import moadong.user.repository.UserRepository;
+import moadong.user.service.UserCommandService;
+import moadong.user.util.CookieMaker;
+import moadong.util.annotations.UnitTest;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.http.ResponseCookie;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.Optional;
+
+import static com.mongodb.assertions.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.*;
+
+@UnitTest
+public class UserLoginTest {
+    private static Validator validator;
+    @Spy
+    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+    @Mock
+    private UserRepository userRepository;
+    @Mock
+    private ClubRepository clubRepository;
+    @Mock
+    private AuthenticationManager authenticationManager;
+    @Mock
+    private JwtProvider jwtProvider;
+    @Mock
+    private CookieMaker cookieMaker;
+    @InjectMocks
+    private UserCommandService userCommandService;
+    private MockHttpServletResponse realHttpServletResponse = new MockHttpServletResponse();
+
+    @BeforeAll
+    public static void setUp() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+
+    @Test
+    void 정상적으로_로그인_시도를_할_경우_작동한다(){
+        // Given
+        UserLoginRequest request = new UserLoginRequest("testUser", "password");
+        String mockClubId = "clubId";
+        Club mockClub = mock(Club.class);
+        User mockUser = new User();
+        CustomUserDetails userDetails = new CustomUserDetails(mockUser);
+        long refreshTokenExpiredTime = 1123L;
+
+        // When
+        when(authenticationManager.authenticate(any()))
+                .thenReturn(new UsernamePasswordAuthenticationToken(userDetails, null));
+        when(clubRepository.findClubByUserId(any())).thenReturn(Optional.of(mockClub));
+        when(mockClub.getId()).thenReturn(mockClubId);
+        when(userRepository.findUserByUserId(any())).thenReturn(Optional.of(mockUser));
+        when(jwtProvider.generateAccessToken(any())).thenReturn("accessToken123");
+        when(jwtProvider.generateRefreshToken(any()))
+                .thenReturn(new RefreshToken("refreshToken123", Date.from(Instant.now().plusSeconds(refreshTokenExpiredTime))));
+        when(cookieMaker.makeRefreshTokenCookie(any())).thenReturn(ResponseCookie.from("refreshToken", "refreshToken123").build());
+
+
+        LoginResponse result = userCommandService.loginUser(request, realHttpServletResponse);
+
+        // Then
+        assertEquals("accessToken123", result.accessToken());
+        assertEquals(mockClubId, result.clubId());
+
+        verify(userRepository).save(any(User.class));
+        assertTrue(realHttpServletResponse.getHeader("Set-Cookie").contains("refreshToken=refreshToken123"));
+    }
+
+    @Test
+    void 없는_아이디와_비밀번호를_입력_시에_실패한다(){
+        UserLoginRequest request = new UserLoginRequest("testUser", "password");
+        User mockUser = new User();
+        CustomUserDetails userDetails = new CustomUserDetails(mockUser);
+
+        // When
+        when(authenticationManager.authenticate(any()))
+                .thenReturn(new UsernamePasswordAuthenticationToken(userDetails, null));
+        when(clubRepository.findClubByUserId(any())).thenReturn(Optional.empty());
+
+        assertThrows(RestApiException.class, ()->{
+            LoginResponse result = userCommandService.loginUser(request, realHttpServletResponse);
+        });
+    }
+}
diff --git a/backend/src/test/java/moadong/unit/user/UserRegisterTest.java b/backend/src/test/java/moadong/unit/user/UserRegisterTest.java
new file mode 100644
index 000000000..c53eeb664
--- /dev/null
+++ b/backend/src/test/java/moadong/unit/user/UserRegisterTest.java
@@ -0,0 +1,175 @@
+package moadong.unit.user;
+
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import moadong.club.entity.Club;
+import moadong.club.repository.ClubRepository;
+import moadong.fixture.UserFixture;
+import moadong.fixture.UserRequestFixture;
+import moadong.global.exception.RestApiException;
+import moadong.global.util.JwtProvider;
+import moadong.user.entity.User;
+import moadong.user.payload.request.UserRegisterRequest;
+import moadong.user.repository.UserRepository;
+import moadong.user.service.UserCommandService;
+import moadong.user.util.CookieMaker;
+import moadong.util.annotations.UnitTest;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
+
+@UnitTest
+class UserRegisterTest {
+    private static Validator validator;
+    @Spy
+    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+    @Mock
+    private UserRepository userRepository;
+    @Mock
+    private ClubRepository clubRepository;
+    @Mock
+    private AuthenticationManager authenticationManager;
+    @Mock
+    private JwtProvider jwtProvider;
+    @Mock
+    private CookieMaker cookieMaker;
+    @InjectMocks
+    private UserCommandService userCommandService;
+
+    @BeforeAll
+    public static void setUp() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+
+    @Test
+    void 회원가입시_올바른_정보를_입력하면_성공한다() {
+        // given
+        String userId = UserFixture.collectUserId;
+        String password = UserFixture.collectPassword;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+
+        doAnswer(invocation -> invocation.getArgument(0))
+                .when(userRepository).save(any(User.class));
+        doReturn(new Club(userId)).when(clubRepository).save(any(Club.class));
+
+        UserRegisterRequest request = UserRequestFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+
+        // when
+        User userResponse = userCommandService.registerUser(request);
+
+        // then
+        assertThat(userResponse).isNotNull();
+        assertThat(userResponse.getUserId()).isEqualTo(userId);
+        assertThat(userResponse.getUserInformation().getName()).isEqualTo(name);
+        assertThat(userResponse.getUserInformation().getPhoneNumber()).isEqualTo(phoneNumber);
+        assertThat(passwordEncoder.matches(password, userResponse.getPassword())).isTrue();
+
+        verify(userRepository, times(1)).save(any(User.class));
+        verify(clubRepository, times(1)).save(any(Club.class));
+    }
+
+    /*
+    아이디 규칙
+    • 5자 ~ 20자
+    • 적어도 하나의 소문자, 하나의 숫자가 포함
+    • 소문자, 대문자, 숫자, 특수문자(!@#$~) 만 사용
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "",                         // 빈 문자열
+            "    ",                     // 공백만
+            "abcde",                    // 숫자 없음
+            "ABCDE",                    // 숫자, 소문자 없음
+            "12345",                    // 소문자 없음
+            "ab!@",                     // 5자 미만 + 숫자 없음
+            "abc123%^",                // 허용되지 않은 특수문자 (`^`)
+            "abc1234567890123456789",  // 21자 (초과)
+            "ab12",                     // 4자 (부족)
+            "ABC123",                   // 소문자 없음
+            "abcd@",                    // 숫자 없음
+            "1234@",                    // 소문자 없음
+            "abc 123",                  // 공백 포함
+            "abC!@#",                   // 숫자 없음
+            "abc123*",                  // `*`은 허용되지 않음
+            "a1@",                      // 길이 부족
+            UserFixture.collectPassword // 비밀번호와 동일 (검증에 따라 실패할 수도 있음)
+    })
+    void 회원가입시_유저_아이디가_조건에_맞지_않으면_실패한다(String userId) {
+        // given
+        String password = UserFixture.collectPassword;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+        //  when
+        try {
+            UserRegisterRequest request = UserRequestFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+            Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
+            // then
+            if (violations.isEmpty()) {
+                fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
+            }
+        } catch (RestApiException e) {
+
+        }
+    }
+
+    /*
+    비밀번호 규칙
+    • 8자 ~ 20자
+    • 숫자랑 영어 대소문자 반드시 하나이상 포함
+    • 특수문자는 반드시 하나가 필요하고 !@#$%^ 만 허용
+    • 공백 포함 불가
+    • 아이디와 동일한 비밀번호 불가
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "short1!",              // 7자 (길이 부족)
+            "longpassword1234567890!", // 21자 (길이 초과)
+            "abcdefgh",             // 영문 소문자만 (숫자 없음)
+            "ABCDEFGH",             // 영문 대문자만 (숫자 없음)
+            "12345678",             // 숫자만 (영문 없음)
+            "Abcdefgh",             // 영문만 (숫자 없음)
+            "abcd1234*",            // 허용되지 않은 특수문자 `*`
+            "abc def123!",          // 공백 포함
+            UserFixture.collectUserId,      // 아이디와 동일하거나 포함
+            "passWord!",            // 특수문자 있음, 숫자 없음
+            "1234!@#$",             // 숫자 + 특수문자, 문자 없음
+            "Abcdef12()",           // 괄호 포함 (허용되지 않은 특수문자)
+            "ABCD1234~",            // `~` 특수문자 허용 안됨
+            "abc1234^",             // `^`는 허용되지만 문자 형식에 따라 위험성 있음
+    })
+    void 회원가입시_유저_비밀번호가_조건에_맞지_않으면_실패한다(String password) {
+        String userId = UserFixture.collectUserId;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+
+        //  when
+        try {
+            UserRegisterRequest request = UserRequestFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+            Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
+            // then
+            if (violations.isEmpty()) {
+                fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
+            }
+        } catch (RestApiException e) {
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/test/java/moadong/user/service/UserCommandServiceTest.java b/backend/src/test/java/moadong/user/service/UserCommandServiceTest.java
deleted file mode 100644
index bbdc968d8..000000000
--- a/backend/src/test/java/moadong/user/service/UserCommandServiceTest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package moadong.user.service;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import moadong.club.repository.ClubRepository;
-import moadong.user.repository.UserInformationRepository;
-import moadong.user.repository.UserRepository;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-@SpringBootTest
-@SuppressWarnings("NonAsciiCharacters")
-public class UserCommandServiceTest {
-
-    @Autowired
-    private UserCommandService userCommandService;
-
-    @MockBean
-    private UserRepository userRepository;
-
-    @MockBean
-    private UserInformationRepository userInformationRepository;
-
-    @MockBean
-    private ClubRepository clubRepository;
-
-    @Test
-    void 회원가입시_id는_8자이상_15자이하여야_한다() {
-//        //given
-//        UserRegisterRequest request = new UserRegisterRequest("123",
-//            "wap123456@",
-//            "한글좋아",
-//            "010-1234-5678");
-//
-//        //when & then
-//        assertDoesNotThrow(()->userCommandService.registerUser(request));
-        assertThat(3).isEqualTo(3);
-    }
-
-}
diff --git a/backend/src/test/java/moadong/util/annotations/IntegrationTest.java b/backend/src/test/java/moadong/util/annotations/IntegrationTest.java
new file mode 100644
index 000000000..304f03ecf
--- /dev/null
+++ b/backend/src/test/java/moadong/util/annotations/IntegrationTest.java
@@ -0,0 +1,17 @@
+package moadong.util.annotations;
+
+import org.junit.jupiter.api.Tag;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@SpringBootTest
+@Tag(TestTypeConstants.INTEGRATION_TEST)
+public @interface IntegrationTest {
+
+}
diff --git a/backend/src/test/java/moadong/util/annotations/TestTypeConstants.java b/backend/src/test/java/moadong/util/annotations/TestTypeConstants.java
new file mode 100644
index 000000000..5410e6c93
--- /dev/null
+++ b/backend/src/test/java/moadong/util/annotations/TestTypeConstants.java
@@ -0,0 +1,10 @@
+package moadong.util.annotations;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class TestTypeConstants {
+    public static final String UNIT_TEST = "UnitTest";
+    public static final String INTEGRATION_TEST = "IntegrationTest";
+}
diff --git a/backend/src/test/java/moadong/util/annotations/UnitTest.java b/backend/src/test/java/moadong/util/annotations/UnitTest.java
new file mode 100644
index 000000000..86a5f6517
--- /dev/null
+++ b/backend/src/test/java/moadong/util/annotations/UnitTest.java
@@ -0,0 +1,20 @@
+package moadong.util.annotations;
+
+
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Tag(TestTypeConstants.UNIT_TEST)
+@ExtendWith(MockitoExtension.class)
+public @interface UnitTest {
+
+}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 8**

<details>
<summary>🔭 Outside diff range comments (3)</summary><blockquote>

<details>
<summary>backend/src/main/java/moadong/user/service/UserCommandService.java (2)</summary><blockquote>

`50-74`: _⚠️ Potential issue_

**로그인 로직에서 비밀번호 검증 확인 필요**

로그인 로직에서 예외처리가 `MongoWriteException`만 포착하고 있는데, 이는 주로 저장 작업에서 발생합니다. 로그인 과정에서는 인증 실패에 대한 예외 처리(예: `BadCredentialsException`)가 누락된 것 같습니다.

인증 실패에 대한 적절한 예외 처리를 추가하는 것이 좋겠습니다:

```diff
try {
    Authentication authenticate = authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(userLoginRequest.userId(),
            userLoginRequest.password()));
    // ... 기존 코드 ...
} catch (MongoWriteException e) {
    throw new RestApiException(ErrorCode.USER_ALREADY_EXIST);
+} catch (BadCredentialsException e) {
+    throw new RestApiException(ErrorCode.INVALID_CREDENTIALS);
}
```

---

`109-121`: _🛠️ Refactor suggestion_

**업데이트 메서드에서의 상태 관리 최적화 필요**

`update` 메서드에서 사용자 프로필을 업데이트한 후 새로운 리프레시 토큰을 생성하고 있습니다. 하지만 사용자 객체의 리프레시 토큰도 업데이트해야 일관성이 유지됩니다.

아래와 같이 수정이 필요합니다:

```diff
user.updateUserProfile(userUpdateRequest.encryptPassword(passwordEncoder));

+ RefreshToken newRefreshTokenObj = jwtProvider.generateRefreshToken(user.getUsername());
+ user.updateRefreshToken(newRefreshTokenObj);
userRepository.save(user);

- String newRefreshToken = jwtProvider.generateRefreshToken(user.getUsername()).getToken();
+ String newRefreshToken = newRefreshTokenObj.getToken();
ResponseCookie cookie = cookieMaker.makeRefreshTokenCookie(newRefreshToken);
response.addHeader("Set-Cookie", cookie.toString());
```

</blockquote></details>
<details>
<summary>backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java (1)</summary><blockquote>

`43-48`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**UserInformation 엔티티 생성 메서드가 아직 사용되고 있습니다.**

`toUserInformationEntity` 메서드가 여전히 존재하지만, PR 설명에 따르면 user와 userinfo 컬렉션을 통합했으므로 이 메서드는 더 이상 필요하지 않을 수 있습니다. 다른 곳에서 사용되지 않는다면 제거를 고려해보세요.


다음 스크립트를 실행하여 이 메서드의 사용처를 확인해 보세요:

---

🏁 Script executed:

```shell
#!/bin/bash
# toUserInformationEntity 메서드의 사용 여부를 확인합니다.
rg "toUserInformationEntity" --type java
```

Length of output: 184

---



</details>

**사용되지 않는 `toUserInformationEntity` 메서드 제거 필요**

확인 결과 해당 메서드는 `UserRegisterRequest` 클래스 내에 선언만 되어 있고, 레포지토리 내 어디에서도 호출되지 않습니다. PR 설명대로 user와 userinfo 컬렉션을 통합했다면 더 이상 필요하지 않으므로 안전하게 제거를 권장합니다.

• 파일: `backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java`  
  – `public UserInformation toUserInformationEntity(String userId) { … }` 메서드 삭제

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (7)</summary><blockquote>

<details>
<summary>backend/src/test/java/moadong/fixture/ClubFixture.java (1)</summary><blockquote>

`58-84`: **Instant로의 시간 계산 마이그레이션에 대한 계획**

`//ToDo` 주석에서 `LocalDateTime`에서 `Instant`로 시간 계산법을 변경할 계획이 있음을 확인했습니다.


시간 관련 코드를 `Instant`로 마이그레이션하는 작업은 다음을 포함해야 합니다:
1. `ClubRecruitmentInformation` 클래스에서 날짜 필드 타입 업데이트
2. 관련 DTO 및 요청 클래스에서 날짜 필드 타입 업데이트 
3. 관련 서비스 로직에서 시간 계산 로직 업데이트

이 마이그레이션이 우선순위가 높다면, 해당 주석을 제거하고 이 클래스의 코드를 활성화하는 것이 좋겠습니다. 혹은 별도의 이슈를 생성하여 추적하는 것도 좋은 방법입니다.

또한 테스트용 ID 값(`"club_123"`, `"recruit_789"`)이 고정되어 있어, 병렬 테스트 실행 시 잠재적인 충돌이 발생할 수 있습니다. 동적으로 생성된 ID를 사용하거나 테스트 간 격리를 보장하는 방법을 고려하세요.

</blockquote></details>
<details>
<summary>backend/src/test/java/moadong/util/annotations/UnitTest.java (1)</summary><blockquote>

`3-4`: **불필요한 빈 줄 제거 필요**

코드 가독성을 높이기 위해 3-4번 라인의 불필요한 빈 줄을 제거하는 것이 좋습니다.

```diff
package moadong.util.annotations;


-
-
import moadong.global.util.TestTypeConstants;
```

</blockquote></details>
<details>
<summary>backend/src/test/java/moadong/fixture/UserFixture.java (1)</summary><blockquote>

`10-14`: **사용자 테스트 데이터 상수가 명확하게 정의되었습니다.**

테스트에 사용할 사용자 데이터를 상수로 정의하여 일관성 있는 테스트 데이터를 보장하고 있습니다. 다만, 상수명에 `collect` 접두사가 붙은 이유가 명확하지 않으니 이름의 의미를 더 명확히 하는 것을 고려해보세요.


아래와 같이 상수명을 더 명확하게 수정하는 것을 고려해보세요:

```diff
-public static final String collectUserId = "test12345";
-public static final String collectPassword = "test12345@";
-public static final String collectName = "테스터";
-public static final String collectPhoneNumber = "010-1234-5678";
+public static final String DEFAULT_USER_ID = "test12345";
+public static final String DEFAULT_PASSWORD = "test12345@";
+public static final String DEFAULT_NAME = "테스터";
+public static final String DEFAULT_PHONE_NUMBER = "010-1234-5678";
```

</blockquote></details>
<details>
<summary>backend/src/test/java/moadong/unit/user/UserLoginTest.java (3)</summary><blockquote>

`58-58`: **MockHttpServletResponse 변수는 테스트 메서드 내에서 초기화하는 것이 좋습니다.**

`realHttpServletResponse`는 인스턴스 변수로 선언되어 있어 모든 테스트에서 공유됩니다. 테스트가 병렬로 실행될 경우 이로 인해 문제가 발생할 수 있습니다. 각 테스트 메서드 내에서 지역 변수로 선언하는 것이 좋습니다.

```diff
-    private MockHttpServletResponse realHttpServletResponse = new MockHttpServletResponse();
```

각 테스트 메서드 내부에서 다음과 같이 선언하세요:
```java
MockHttpServletResponse realHttpServletResponse = new MockHttpServletResponse();
```

---

`42-64`: **Validator 변수가 사용되지 않습니다.**

정적 `validator` 변수가 선언되고 초기화되었지만 테스트 메서드 내에서 사용되지 않습니다. 필요하지 않다면 제거하는 것이 좋습니다.

```diff
-    private static Validator validator;

@BeforeAll
public static void setUp() {
-    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
-    validator = factory.getValidator();
}
```

---

`98-112`: **테스트 메서드 이름이 테스트 내용과 일치하지 않습니다.**

테스트 메서드 이름은 "없는 아이디와 비밀번호를 입력 시에 실패한다"이지만, 실제 테스트는 사용자의 클럽을 찾을 수 없을 때 발생하는 예외를 검증합니다. 메서드 이름을 테스트 내용에 맞게 수정하는 것이 좋습니다.

```diff
-    void 없는_아이디와_비밀번호를_입력_시에_실패한다(){
+    void 사용자에_대한_클럽을_찾을_수_없을_때_예외가_발생한다(){
```

</blockquote></details>
<details>
<summary>backend/src/test/java/moadong/unit/user/UserRegisterTest.java (1)</summary><blockquote>

`167-167`: **오류 메시지를 수정하세요.**

비밀번호 검증 테스트에서 실패 메시지가 여전히 "유효하지 않은 userId"를 참조하고 있습니다. 이는 아이디 검증 테스트에서 복사되었을 가능성이 있습니다. 메시지를 적절하게 수정해야 합니다.

```diff
- fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
+ fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 password: " + password);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 596c098d8a187f9a420079344d8c6e96a969da3c and d4ac2231da82e478f476523af77fcdb1703003ae.

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `backend/build.gradle` (1 hunks)
* `backend/src/main/java/moadong/global/util/TestTypeConstants.java` (1 hunks)
* `backend/src/main/java/moadong/global/validator/PasswordValidator.java` (1 hunks)
* `backend/src/main/java/moadong/user/entity/User.java` (1 hunks)
* `backend/src/main/java/moadong/user/entity/UserInformation.java` (1 hunks)
* `backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java` (1 hunks)
* `backend/src/main/java/moadong/user/repository/UserInformationRepository.java` (0 hunks)
* `backend/src/main/java/moadong/user/service/UserCommandService.java` (1 hunks)
* `backend/src/test/java/moadong/fixture/ClubFixture.java` (1 hunks)
* `backend/src/test/java/moadong/fixture/UserFixture.java` (1 hunks)
* `backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java` (1 hunks)
* `backend/src/test/java/moadong/unit/user/UserLoginTest.java` (1 hunks)
* `backend/src/test/java/moadong/unit/user/UserRegisterTest.java` (1 hunks)
* `backend/src/test/java/moadong/util/annotations/IntegrationTest.java` (1 hunks)
* `backend/src/test/java/moadong/util/annotations/UnitTest.java` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* backend/src/main/java/moadong/user/repository/UserInformationRepository.java

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java (2)</summary><blockquote>

<details>
<summary>backend/src/test/java/moadong/fixture/ClubFixture.java (1)</summary>

* `ClubFixture` (15-85)

</details>
<details>
<summary>backend/src/test/java/moadong/fixture/UserFixture.java (1)</summary>

* `UserFixture` (10-35)

</details>

</blockquote></details>
<details>
<summary>backend/src/test/java/moadong/unit/user/UserLoginTest.java (1)</summary><blockquote>

<details>
<summary>backend/src/test/java/moadong/unit/user/UserRegisterTest.java (1)</summary>

* `UnitTest` (36-174)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Actions: PR Test</summary>

<details>
<summary>backend/src/test/java/moadong/fixture/ClubFixture.java</summary>

[error] 40-40: Cannot find symbol: method recruitmentForm(String) in class ClubRecruitmentInformationBuilder

---

[error] 54-54: Incompatible types: String cannot be converted to Map<String,String>

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (21)</summary><blockquote>

<details>
<summary>backend/src/main/java/moadong/global/util/TestTypeConstants.java (1)</summary>

`1-10`: **테스트 유형 상수 클래스 구현이 좋습니다.**

상수 정의 및 유틸리티 클래스 구현이 모범 사례를 잘 따르고 있습니다:
- 상수 네이밍 컨벤션(대문자와 언더스코어)
- Lombok의 `@NoArgsConstructor(access = AccessLevel.PRIVATE)`를 사용하여 인스턴스화 방지
- 단위 테스트와 통합 테스트를 구분하는 명확한 목적 제공

이 상수들이 커스텀 테스트 어노테이션과 Gradle 빌드 설정에서 일관되게 사용되어 테스트 실행을 분리하는 것은 좋은 접근 방식입니다.

</details>
<details>
<summary>backend/src/test/java/moadong/util/annotations/IntegrationTest.java (1)</summary>

`14-20`: **깔끔하고 명확한 통합 테스트 어노테이션 구현입니다.**

이 커스텀 어노테이션은 통합 테스트를 위한 설정을 단순화하고 표준화하는 좋은 방법입니다. `@SpringBootTest`와 `@Tag`를 조합하여 통합 테스트 클래스를 쉽게 식별하고 실행할 수 있게 해줍니다.

</details>
<details>
<summary>backend/src/test/java/moadong/util/annotations/UnitTest.java (1)</summary>

`15-21`: **단위 테스트 어노테이션 구현이 적절합니다.**

Mockito 확장을 포함한 단위 테스트 어노테이션 구현이 잘 되어 있습니다. `@Tag`와 `@ExtendWith`를 조합하여 단위 테스트를 쉽게 식별하고 필요한 의존성을 모의(mock)할 수 있게 해줍니다.

</details>
<details>
<summary>backend/src/main/java/moadong/user/entity/UserInformation.java (2)</summary>

`15-20`: **엔티티 어노테이션 검토 필요**

`UserInformation` 클래스에 `@Entity` 어노테이션이 있지만, MongoDB의 `@Document` 어노테이션이 제거되었습니다. 이제 사용자 엔티티에 임베드될 것으로 보이므로, `@Entity` 대신 `@Embeddable` 어노테이션이 더 적절할 수 있습니다.

`User` 엔티티에서 이 클래스가 어떻게 참조되는지 확인해 주세요.

---

`31-34`: **생성자 구현이 적절합니다.**

필요한 필드만 초기화하는 간결한 생성자를 추가한 것은 좋은 접근법입니다. 이는 `User` 엔티티 생성 시 `UserInformation`을 쉽게 초기화할 수 있게 해줍니다.

</details>
<details>
<summary>backend/src/main/java/moadong/user/service/UserCommandService.java (1)</summary>

`40-45`: **메서드 반환 타입 변경이 적절합니다.**

`registerUser` 메서드의 반환 타입을 `void`에서 `User`로 변경한 것은 좋은 개선입니다. 이렇게 하면 등록된 사용자 정보를 즉시 사용할 수 있어 더 유연한 API가 됩니다. 또한 `UserRegisterRequest`에 비밀번호 인코딩 책임을 위임하여 코드가 더 깔끔해졌습니다.

</details>
<details>
<summary>backend/build.gradle (3)</summary>

`53-57`: **테스트 태스크 설명이 명확하게 추가되었습니다.**

전체 테스트를 실행하는 기본 태스크에 대한 설명이 추가되어 더 명확해졌습니다. 개발자들이 이 태스크의 목적을 쉽게 이해할 수 있게 되었습니다.

---

`59-68`: **단위 테스트 태스크가 효과적으로 구성되었습니다.**

`UnitTest` 태그를 포함하고 `IntegrationTest` 태그를 제외함으로써 명확하게 단위 테스트만 실행할 수 있도록 설정되었습니다. 이는 테스트 실행 속도를 향상시키고 CI/CD 파이프라인에서 테스트 단계를 분리하는 데 유용합니다.

---

`70-78`: **통합 테스트 태스크가 적절히 구성되었습니다.**

통합 테스트를 위한 별도의 태스크를 만들어 `IntegrationTest` 태그를 포함하고 `UnitTest` 태그를 제외하는 설정이 잘 되어 있습니다. 이는 더 큰 프로젝트에서 테스트 실행 전략을 관리하는 데 도움이 됩니다.

</details>
<details>
<summary>backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java (3)</summary>

`17-28`: **@NotBlank 어노테이션으로 검증 로직이 개선되었습니다.**

`@NotNull`에서 `@NotBlank`로 변경함으로써 빈 문자열이나 공백만 있는 경우도 유효성 검사에서 걸러낼 수 있게 되었습니다. 이는 사용자 입력 검증의 품질을 향상시킵니다.

---

`29-33`: **아이디와 비밀번호 동일 여부 검증 로직이 추가되었습니다.**

사용자 아이디와 비밀번호가 동일한 경우 보안 취약점이 될 수 있으므로, 이를 방지하는 검증 로직이 추가된 것은 적절합니다. 컴팩트 생성자를 활용한 방식도 코드 가독성을 높입니다.

---

`35-41`: **비밀번호 인코딩 책임이 명확하게 할당되었습니다.**

`toUserEntity` 메서드가 `PasswordEncoder`를 매개변수로 받아 비밀번호 인코딩 책임을 명확히 하고, `UserInformation` 생성 방식이 단순화되었습니다. 이는 코드의 응집도를 높이고 유지보수성을 개선합니다.

</details>
<details>
<summary>backend/src/test/java/moadong/fixture/UserFixture.java (1)</summary>

`15-23`: **사용자 관련 객체 생성 메서드가 잘 구현되었습니다.**

사용자 등록 요청, 로그인 요청, 사용자 엔티티를 생성하는 메서드가 잘 구현되어 있습니다. 특히 `createUser` 메서드에서 `UserRegisterRequest`의 `toUserEntity`를 활용하는 방식이 중복 코드를 방지하고 일관성을 유지하는 좋은 방법입니다.

</details>
<details>
<summary>backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java (6)</summary>

`28-38`: **테스트 클래스 구조가 잘 설계되었습니다.**

`@UnitTest` 어노테이션을 사용하여 단위 테스트임을 명확히 하고, 필요한 종속성이 적절히 모킹되어 있습니다. 테스트에 필요한 상수도 클래스 레벨에서 정의하여 재사용성을 높였습니다.

---

`39-50`: **클럽 생성 테스트가 간결하게 작성되었습니다.**

Given-When-Then 구조를 따라 테스트가 명확하게 작성되었습니다. Mock 객체를 사용하여 저장소 의존성을 격리시킨 것도 좋은 방법입니다.

---

`52-68`: **클럽 약력 업데이트 성공 테스트가 잘 구현되었습니다.**

테스트가 Given-When-Then 패턴을 따라 명확하게 구성되어 있으며, 필요한 모든 의존성이 적절히 모킹되어 있습니다. 업데이트 후 저장 작업이 호출되는지 확인하는 부분도 잘 작성되었습니다.

---

`70-75`: **클럽 미존재 시 예외 발생 테스트가 간결하게 작성되었습니다.**

클럽을 찾을 수 없을 때 예외가 발생하는 상황을 테스트하는 코드가 간결하게 잘 작성되었습니다. 모킹을 사용하여 클럽을 찾을 수 없는 상황을 시뮬레이션하고, 예외가 발생하는지 검증하는 방식이 적절합니다.

---

`77-85`: **권한 검증 테스트가 명확하게 작성되었습니다.**

사용자가 클럽 소유자가 아닌 경우 예외가 발생하는 상황을 테스트하는 코드가 잘 작성되었습니다. 모킹을 사용하여 클럽 소유자가 다른 사용자인 상황을 시뮬레이션하고, 예외가 발생하는지 검증하는 방식이 적절합니다.

---

`88-121`: **TODO 주석이 추가된 비활성화된 테스트 코드가 있습니다.**

시간 계산법을 `LocalDateTime`에서 `Instant`로 변경 후 활성화할 테스트 코드가 있습니다. PR 설명에 언급된 것처럼 이것은 예정된 작업이지만, 이러한 주석 처리된 코드는 가능한 빨리 해결하는 것이 좋습니다.


이 TODO 항목에 대한 작업 계획이나 일정이 있는지 확인해 주세요. 해당 작업을 별도의 이슈로 관리하면 추적하기 용이할 수 있습니다.

</details>
<details>
<summary>backend/src/test/java/moadong/unit/user/UserLoginTest.java (1)</summary>

`66-96`: **로그인 성공 테스트가 잘 작성되었습니다!**

로그인 성공 시나리오에 대한 테스트가 잘 작성되었습니다. 필요한 모든 모킹이 적절하게 설정되었고, 결과에 대한 검증도 올바르게 이루어지고 있습니다. 토큰 생성, 저장소 호출, 응답 헤더 확인 등 핵심 기능을 모두 테스트하고 있습니다.

</details>
<details>
<summary>backend/src/test/java/moadong/unit/user/UserRegisterTest.java (1)</summary>

`60-86`: **회원가입 성공 테스트가 잘 작성되었습니다!**

회원가입 성공 시나리오에 대한 테스트 코드가 잘 작성되었습니다. Mock 객체 설정, 사용자 등록 요청 생성, 응답 검증 등이 명확하게 구현되어 있습니다. 특히 비밀번호 인코딩 확인과 저장소 호출 검증이 적절히 포함되어 있습니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T12:53:06Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java (2)</summary><blockquote>

`39-50`: **클럽 생성 테스트가 적절히 구현되었습니다**

클럽 생성 테스트는 간단하고 목적에 맞게 구현되었습니다. 다만, 저장된 Club 객체의 속성이 요청과 일치하는지 추가로 검증하면 더 완벽할 것 같습니다.

테스트 강화를 위해 다음과 같이 ArgumentCaptor를 사용하여 저장된 Club 객체의 속성을 검증하는 방법을 고려해보세요:

```diff
@Test
public void 정상적으로_클럽을_생성한다() {
    // Given
    ClubCreateRequest request = new ClubCreateRequest("테스트", "카테고리", "분과");
    when(clubRepository.save(any())).thenReturn(mock(Club.class));

    // When
    clubProfileService.createClub(request);

    // Then
    verify(clubRepository, times(1)).save(any(Club.class));
+   ArgumentCaptor<Club> clubCaptor = ArgumentCaptor.forClass(Club.class);
+   verify(clubRepository).save(clubCaptor.capture());
+   Club savedClub = clubCaptor.getValue();
+   assertEquals("테스트", savedClub.getName());
+   assertEquals("카테고리", savedClub.getCategory());
+   assertEquals("분과", savedClub.getDivision());
}
```

---

`87-120`: **TODO 주석 처리된 테스트에 대한 조언**

LocalDateTime에서 Instant로 변경 후 활성화할 예정인 테스트 코드에 대한 TODO 주석이 있습니다. 이러한 변경을 계획하고 미리 테스트 케이스를 작성해둔 점은 좋습니다. 하지만 주석 처리된 코드보다는 이슈 트래킹 시스템에 작업 항목으로 등록하는 것이 더 나을 수 있습니다.

완전히 주석 처리하기보다, `@Disabled` 어노테이션과 함께 비활성화 이유를 명시하는 방법을 고려해보세요:

```diff
- //    ToDo: 시간 계산법을 LocalDateTime에서 Instant로 변경 후에 활성화할 것
- //    @Test
- //    void updateClubRecruitmentInfo_WithDates_SchedulesRecruitment() {
+    @Test
+    @Disabled("시간 계산법을 LocalDateTime에서 Instant로 변경 후에 활성화할 것")
+    void updateClubRecruitmentInfo_WithDates_SchedulesRecruitment() {
```

이렇게 하면 테스트 실행 시 비활성화된 테스트가 스킵되지만, 테스트 보고서에는 포함되어 미완료 작업을 추적하기 용이합니다.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1a267ae506d492c014c1f6da9300c14daece4f5f and b357485bc89cf40f35ba834db3d52f7cf665d44c.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `backend/src/test/java/moadong/fixture/ClubFixture.java` (1 hunks)
* `backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java` (1 hunks)
* `backend/src/test/java/moadong/user/service/UserCommandServiceTest.java` (0 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* backend/src/test/java/moadong/user/service/UserCommandServiceTest.java

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* backend/src/test/java/moadong/fixture/ClubFixture.java

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java (2)</summary><blockquote>

<details>
<summary>backend/src/test/java/moadong/fixture/ClubFixture.java (1)</summary>

* `ClubFixture` (16-83)

</details>
<details>
<summary>backend/src/test/java/moadong/fixture/UserFixture.java (1)</summary>

* `UserFixture` (10-35)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java (4)</summary>

`28-38`: **깔끔한 테스트 클래스 구조 👍**

클래스 구조가 잘 정리되어 있고, 필요한 의존성을 명확하게 모킹하셨습니다. `@UnitTest` 어노테이션을 사용하여 단위 테스트임을 명시하신 것이 좋습니다. 상수값도 클래스 상단에 정의하여 가독성이 향상되었습니다.

---

`51-67`: **클럽 정보 업데이트 테스트가 잘 구현되었습니다**

클럽 정보 업데이트 테스트가 잘 구현되었습니다. 정상 케이스에 대한 검증이 적절히 이루어지고 있습니다. ClubFixture를 활용하여 테스트 데이터를 생성하는 접근 방식이 좋습니다.

---

`69-74`: **예외 케이스 테스트가 적절히 구현되었습니다**

클럽이 존재하지 않는 경우에 대한 예외 처리 테스트가 잘 구현되었습니다. `assertThrows`를 사용하여 예외 발생을 검증하는 방법이 적절합니다.

---

`76-84`: **권한 검증 테스트가 잘 구현되었습니다**

권한이 없는 사용자가 클럽 정보를 업데이트하려고 할 때 예외가 발생하는지 검증하는 테스트가 잘 구현되었습니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T02:01:04Z
user: login: lepitaaar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T02:24:12Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T02:24:18Z
user: login: Due-IT
body: 고생많으셨습니다! 테스트 코드 부분에 함께 맞춰가야 할 부분이 조금 있는것 같네요 👍 
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T05:57:50Z
user: login: PororoAndFriends
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T06:14:27Z
user: login: PororoAndFriends
body: 고생하셨습니다! @Tag를 통해 단위테스트와 통합테스트를 구분하는 부분은PR 머지 후에 제 테스트코드에도 추가해야겠네요 😊
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-10T06:17:09Z
user: login: Zepelown
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T06:34:51Z
user: login: Zepelown
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T06:35:48Z
user: login: Zepelown
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T07:05:24Z
user: login: Zepelown
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T07:09:32Z
user: login: Zepelown
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T07:09:41Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>backend/src/test/java/moadong/fixture/UserFixture.java (1)</summary><blockquote>

`10-13`: **상수 이름 관례 개선 제안**

상수 이름에 사용된 "collect" 접두사의 의미가 명확하지 않습니다. 일반적인 Java 상수 네이밍 컨벤션에 따라 `DEFAULT_USER_ID`, `TEST_USER_ID` 또는 `SAMPLE_USER_ID`와 같이 더 직관적인 이름을 사용하는 것이 코드의 가독성을 높일 수 있습니다.

```diff
-    public static final String collectUserId = "test12345";
-    public static final String collectPassword = "test12345@";
-    public static final String collectName = "테스터";
-    public static final String collectPhoneNumber = "010-1234-5678";
+    public static final String DEFAULT_USER_ID = "test12345";
+    public static final String DEFAULT_PASSWORD = "test12345@";
+    public static final String DEFAULT_NAME = "테스터";
+    public static final String DEFAULT_PHONE_NUMBER = "010-1234-5678";
```

</blockquote></details>
<details>
<summary>backend/src/test/java/moadong/fixture/ClubRequestFixture.java (1)</summary><blockquote>

`23-49`: **주석 처리된 코드와 TODO에 대한 계획 명확화 필요**

현재 `LocalDateTime`에서 `Instant`로 변경을 위해 코드가 주석 처리되어 있습니다. 이 변경에 대한 일정이나 이슈 추적 번호를 TODO 주석에 포함시키면 팀원들이 변경 계획을 더 명확하게 이해할 수 있을 것입니다. 또한, 주석 처리된 코드를 위한 import 문(`Instant`, `ChronoUnit` 등)이 누락되어 있으므로, 코드가 활성화될 때 추가해야 합니다.

```diff
- //ToDo: 시간 계산법을 LocalDateTime에서 Instant로 변경 후에 활성화할 것
+ //ToDo: 시간 계산법을 LocalDateTime에서 Instant로 변경 후에 활성화할 것 (이슈 #XXX, 예상 완료일: YYYY-MM-DD)
```

주석 처리된 코드를 위한 import 문도 추가해야 합니다:
```java
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fcd17ec58a6b9b8fe14698abf6cde3a72c419e8a and 71c6d2f06a6b6d4d7f59a22994e6cd59ec1ba212.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `backend/src/test/java/moadong/fixture/ClubFixture.java` (1 hunks)
* `backend/src/test/java/moadong/fixture/ClubRequestFixture.java` (1 hunks)
* `backend/src/test/java/moadong/fixture/UserFixture.java` (1 hunks)
* `backend/src/test/java/moadong/fixture/UserRequestFixture.java` (1 hunks)
* `backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java` (1 hunks)
* `backend/src/test/java/moadong/unit/user/UserRegisterTest.java` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* backend/src/test/java/moadong/fixture/UserRequestFixture.java

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* backend/src/test/java/moadong/fixture/ClubFixture.java
* backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java
* backend/src/test/java/moadong/unit/user/UserRegisterTest.java

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>backend/src/test/java/moadong/fixture/UserFixture.java (1)</summary>

`15-17`: **메서드의 기능이 적절하게 구현되었습니다.**

`PasswordEncoder`를 통해 비밀번호를 안전하게 인코딩하면서 사용자 엔티티를 생성하는 로직이 잘 구현되었습니다. `UserRegisterRequest`의 `toUserEntity` 메서드를 활용하여 변환하는 접근 방식도 적절합니다.

</details>
<details>
<summary>backend/src/test/java/moadong/fixture/ClubRequestFixture.java (1)</summary>

`9-22`: **테스트 픽스처 메서드가 잘 구현되었습니다.**

`createValidClubInfoRequest` 메서드가 명확하고 적절한 테스트 데이터와 함께 잘 구현되었습니다. 테스트에 필요한 모든 필드가 적절한 샘플 값으로 초기화되어 있습니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T07:29:19Z
user: login: tgyuuAn
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-10T12:04:08Z
user: login: Zepelown
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-10T12:23:22Z
user: login: tgyuuAn
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-10T12:25:03Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>backend/src/main/java/moadong/global/validator/PasswordValidator.java (1)</summary><blockquote>

`13-17`: _🛠️ Refactor suggestion_

**`isValid` 메서드가 null/빈 문자열을 유효하게 처리합니다.**

현재 구현에서는 null이나 빈 문자열이 유효한 비밀번호로 간주됩니다. 그러나 비밀번호는 일반적으로 필수 입력 값이어야 합니다.

다음과 같이 수정을 고려해보세요:

```diff
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
    if (s == null || s.isEmpty()) {
-        return true;
+        return false;
    }
    return PASSWORD_PATTERN.matcher(s).matches();
}
```

또는 `@NotBlank` 어노테이션을 함께 사용하여 입력값의 필수 여부를 검증하는 방법도 있습니다.

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>backend/src/main/java/moadong/global/validator/PasswordValidator.java (1)</summary><blockquote>

`10-11`: **정규식 패턴 정의에 오류가 있습니다.**

현재 비밀번호 패턴 정의에 다음과 같은 문제가 있습니다:
1. 정규식이 escaped 쌍따옴표(`\"`)로 둘러싸여 있음
2. 백슬래시가 이중으로 사용됨 (`\\\\d`가 `\\d`여야 함)
3. 패턴 끝에 줄바꿈 문자가 포함됨

다음과 같이 수정하세요:

```diff
-    // 8 ~ 20자이고, 반드시 한 개의 알파벳, 한 개의 숫자, !@#$%^ 중 한 개가 모두 들어 갔는지 확인
-    private static final Pattern PASSWORD_PATTERN = Pattern.compile("\"^(?=.*[a-zA-Z])(?=.*\\\\d)(?=.*[!@#$%^])[a-zA-Z\\\\d!@#$%^]{8,20}$\"\n");
+    // 8 ~ 20자이고, 반드시 한 개의 알파벳, 한 개의 숫자, !@#$%^ 중 한 개가 모두 들어 갔는지 확인
+    private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^])[a-zA-Z\\d!@#$%^]{8,20}$");
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>backend/src/main/java/moadong/global/validator/PasswordValidator.java (1)</summary><blockquote>

`9-18`: **정규 표현식 테스트 코드 추가를 권장합니다.**

복잡한 정규 표현식은 의도한 대로 동작하는지 확인하기 위해 테스트 코드를 작성하는 것이 좋습니다. 다양한 케이스(유효한 비밀번호, 너무 짧은 비밀번호, 특수문자가 없는 비밀번호 등)에 대한 테스트를 추가하여 유효성 검사 로직이 올바르게 작동하는지 확인하세요.

```java
@Test
void testPasswordValidation() {
    PasswordValidator validator = new PasswordValidator();
    
    // 유효한 비밀번호
    assertTrue(validator.isValid("Password1!", null));
    
    // 너무 짧은 비밀번호
    assertFalse(validator.isValid("Pas1!", null));
    
    // 특수문자가 없는 비밀번호
    assertFalse(validator.isValid("Password1", null));
    
    // 숫자가 없는 비밀번호
    assertFalse(validator.isValid("Password!", null));
    
    // 알파벳이 없는 비밀번호
    assertFalse(validator.isValid("12345678!", null));
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 71c6d2f06a6b6d4d7f59a22994e6cd59ec1ba212 and 65e64b55ee8fc7534f56896ad24e4728f6979fa0.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `backend/src/main/java/moadong/global/validator/KoreanValidator.java` (1 hunks)
* `backend/src/main/java/moadong/global/validator/PasswordValidator.java` (1 hunks)
* `backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java` (1 hunks)
* `backend/src/main/java/moadong/global/validator/UserIdValidator.java` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (3)</summary>

* backend/src/main/java/moadong/global/validator/KoreanValidator.java
* backend/src/main/java/moadong/global/validator/UserIdValidator.java
* backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>backend/src/main/java/moadong/global/validator/PasswordValidator.java (1)</summary>

`10-10`: **주석 추가에 감사합니다.**

정규식의 의도를 명확히 설명하는 주석을 추가해주셔서 코드 가독성이 향상되었습니다. 이는 좋은 개발 관행입니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T12:39:49Z
user: login: Due-IT
body: 고생하셨습니다 :)
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-13T12:39:29Z

Review comments:
diff_hunk: @@ -7,7 +7,7 @@
 import java.util.regex.Pattern;
 
 public class PasswordValidator implements ConstraintValidator<Password, String> {
-    private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^]).{8,20}$");
+    private static final Pattern PASSWORD_PATTERN = Pattern.compile("\"^(?=.*[a-zA-Z])(?=.*\\\\d)(?=.*[!@#$%^])[a-zA-Z\\\\d!@#$%^]{8,20}$\"\n");
path: backend/src/main/java/moadong/global/validator/PasswordValidator.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**정규식 패턴 정의에 오류가 있습니다.**

현재 비밀번호 패턴 정의에 다음과 같은 문제가 있습니다:
1. 정규식이 escaped 쌍따옴표(`\"`)로 둘러싸여 있음
2. 백슬래시가 이중으로 사용됨 (`\\\\d`가 `\\d`여야 함)
3. 패턴 끝에 줄바꿈 문자가 포함됨


다음과 같이 수정하세요:

```diff
-    private static final Pattern PASSWORD_PATTERN = Pattern.compile("\"^(?=.*[a-zA-Z])(?=.*\\\\d)(?=.*[!@#$%^])[a-zA-Z\\\\d!@#$%^]{8,20}$\"\n");
+    private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^])[a-zA-Z\\d!@#$%^]{8,20}$");
```

또한, 유효성 검사 로직이 null/빈 문자열을 유효하다고 간주합니다(항상 true 반환). 이는 `@NotBlank`와 같은 다른 제약 조건과 함께 사용한다고 가정하는 것 같습니다만, 명시적으로 null/빈 값을 거부하는 것이 더 명확할 수 있습니다.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T12:53:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,85 @@
+package moadong.fixture;
+
+import moadong.club.entity.Club;
+import moadong.club.entity.ClubRecruitmentInformation;
+import moadong.club.enums.ClubRecruitmentStatus;
+import moadong.club.enums.ClubState;
+import moadong.club.payload.request.ClubInfoRequest;
+import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+
+public class ClubFixture {
+    public static final ClubState DEFAULT_STATE = ClubState.UNAVAILABLE;
+    public static Club createClub() {
+        return Club.builder()
+                .name("테스트 동아리")
+                .category("학술")
+                .division("프로그래밍")
+                .clubRecruitmentInformation(createRecruitmentInfo())
+                .build();
+    }
+
+    public static ClubRecruitmentInformation createRecruitmentInfo() {
+        return ClubRecruitmentInformation.builder()
+                .id("recruit_789")
+                .logo("logo.png")
+                .introduction("우수한 개발 동아리")
+                .description("개발 문화를 선도하는 동아리입니다.")
+                .presidentName("홍길동")
+                .presidentTelephoneNumber("010-1234-5678")
+                .recruitmentStart(LocalDateTime.now().plusDays(1))
+                .recruitmentEnd(LocalDateTime.now().plusDays(7))
+                .recruitmentTarget("1~2학년")
+                .feedImages(List.of("image1.jpg", "image2.jpg"))
+                .tags(List.of("개발", "프로그래밍", "스터디"))
+                .clubRecruitmentStatus(ClubRecruitmentStatus.OPEN)
+                .recruitmentForm("https://forms.example.com")
+                .build();
+    }
path: backend/src/test/java/moadong/fixture/ClubFixture.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**ClubRecruitmentInformation 생성자에 recruitmentForm 오류가 있습니다.**

파이프라인 오류를 확인해보면, `ClubRecruitmentInformationBuilder`에 `recruitmentForm` 메서드가 없다고 표시되었습니다.


`ClubRecruitmentInformation` 클래스를 확인하여 다음 사항을 확인하세요:
1. `recruitmentForm` 필드가 존재하는지
2. 필드 타입이 String인지 혹은 다른 타입인지
3. 빌더가 해당 필드를 지원하는지

이 오류를 해결하기 위해 필드 이름이 변경되었거나 존재하지 않는 경우 해당 라인을 제거하거나 올바른 필드 이름으로 수정하세요.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: PR Test</summary>

[error] 40-40: Cannot find symbol: method recruitmentForm(String) in class ClubRecruitmentInformationBuilder

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit b357485
created_at: 2025-05-08T12:53:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,85 @@
+package moadong.fixture;
+
+import moadong.club.entity.Club;
+import moadong.club.entity.ClubRecruitmentInformation;
+import moadong.club.enums.ClubRecruitmentStatus;
+import moadong.club.enums.ClubState;
+import moadong.club.payload.request.ClubInfoRequest;
+import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+
+public class ClubFixture {
+    public static final ClubState DEFAULT_STATE = ClubState.UNAVAILABLE;
+    public static Club createClub() {
+        return Club.builder()
+                .name("테스트 동아리")
+                .category("학술")
+                .division("프로그래밍")
+                .clubRecruitmentInformation(createRecruitmentInfo())
+                .build();
+    }
+
+    public static ClubRecruitmentInformation createRecruitmentInfo() {
+        return ClubRecruitmentInformation.builder()
+                .id("recruit_789")
+                .logo("logo.png")
+                .introduction("우수한 개발 동아리")
+                .description("개발 문화를 선도하는 동아리입니다.")
+                .presidentName("홍길동")
+                .presidentTelephoneNumber("010-1234-5678")
+                .recruitmentStart(LocalDateTime.now().plusDays(1))
+                .recruitmentEnd(LocalDateTime.now().plusDays(7))
+                .recruitmentTarget("1~2학년")
+                .feedImages(List.of("image1.jpg", "image2.jpg"))
+                .tags(List.of("개발", "프로그래밍", "스터디"))
+                .clubRecruitmentStatus(ClubRecruitmentStatus.OPEN)
+                .recruitmentForm("https://forms.example.com")
+                .build();
+    }
+
+    public static ClubInfoRequest createValidClubInfoRequest() {
+        return new ClubInfoRequest(
+                "club_123",
+                "테스트동아리",
+                "학술",
+                "프로그래밍",
+                List.of("개발", "스터디"),
+                "동아리 소개입니다.",
+                "홍길동",
+                "010-1234-5678",
+                "https://forms.gle/abcd"
+        );
+
path: backend/src/test/java/moadong/fixture/ClubFixture.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**ClubInfoRequest의 마지막 파라미터 타입이 맞지 않습니다.**

파이프라인 오류에 따르면 54번 라인의 `"https://forms.gle/abcd"` 값이 `String`에서 `Map<String, String>`으로 변환될 수 없습니다.


`ClubInfoRequest` 생성자의 마지막 파라미터는 `Map<String, String>` 타입을 기대하는 것 같습니다. 다음과 같이 수정하세요:

```diff
-                "https://forms.gle/abcd"
+                Map.of("url", "https://forms.gle/abcd")
```

수정 전에 `ClubInfoRequest` 클래스의 생성자 파라미터 타입을 확인하고 필요한 Map 구조를 제공하세요.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    public static ClubInfoRequest createValidClubInfoRequest() {
        return new ClubInfoRequest(
                "club_123",
                "테스트동아리",
                "학술",
                "프로그래밍",
                List.of("개발", "스터디"),
                "동아리 소개입니다.",
                "홍길동",
                "010-1234-5678",
                Map.of("url", "https://forms.gle/abcd")
        );
    }
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: PR Test</summary>

[error] 54-54: Incompatible types: String cannot be converted to Map<String,String>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit b357485
created_at: 2025-05-08T12:53:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,6 +51,9 @@ public class User implements UserDetails {
     @Field("refreshToken")
     private RefreshToken refreshToken;
 
+    @Field("userInformation")
+    private UserInformation userInformation;
+
path: backend/src/main/java/moadong/user/entity/User.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**UserInformation 필드 추가는 좋지만 프로필 업데이트 메서드 수정이 필요합니다.**

`userInformation` 필드 추가는 `user`와 `userinfo` 컬렉션을 통합하는 리팩토링의 일부로 보입니다. 이 접근 방식은 데이터 일관성을 개선할 수 있습니다.


`updateUserProfile` 메서드가 새로 추가된 `userInformation` 필드를 업데이트하지 않는 문제가 있습니다. 아래와 같이 메서드를 확장하는 것이 좋겠습니다:

```diff
public void updateUserProfile(UserUpdateRequest userUpdateRequest) {
    this.userId = userUpdateRequest.userId();
    this.password = userUpdateRequest.password();
+   if (userUpdateRequest.userInformation() != null) {
+       this.userInformation = userUpdateRequest.userInformation();
+   }
}
```

또한 `UserInformation` 필드만 업데이트하는 별도의 메서드를 추가하는 것도 고려하세요:

```java
public void updateUserInformation(UserInformation userInformation) {
    this.userInformation = userInformation;
}
```

물론 이는 `UserUpdateRequest` 클래스가 `userInformation` 필드를 포함하도록 업데이트되어야 함을 의미합니다.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T12:53:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,17 +17,19 @@
 @Builder
 @AllArgsConstructor
 @NoArgsConstructor
-@Document("user_informations")
 public class UserInformation {
 
     @Id
     private String id;
path: backend/src/main/java/moadong/user/entity/UserInformation.java
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**ID 필드 필요성 검토**

이 클래스가 이제 `User` 엔티티에 임베드된다면, 독립적인 `@Id` 필드가 필요하지 않을 수 있습니다. 임베디드 객체는 일반적으로 소유 엔티티의 ID를 공유합니다.

---

🏁 Script executed:

```shell
#!/bin/bash
# User 엔티티에서 UserInformation이 어떻게 사용되는지 확인
rg -A 3 -B 3 "UserInformation" backend/src/main/java/moadong/user/entity/User.java
```

Length of output: 232

---

추가 정보 확인을 위해 `UserInformation.java`의 상단 어노테이션과 import 구문을 살펴보겠습니다.

---

🏁 Script executed:

```shell
#!/bin/bash
# UserInformation 클래스 상단 어노테이션 및 import 확인
sed -n '1,50p' backend/src/main/java/moadong/user/entity/UserInformation.java
```

Length of output: 907

---

MongoDB 매핑 여부 확인을 위해 `User.java` 클래스 상단 어노테이션을 확인하겠습니다.

---

🏁 Script executed:

```shell
#!/bin/bash
# User 엔티티의 클래스 레벨 어노테이션 확인
sed -n '1,50p' backend/src/main/java/moadong/user/entity/User.java
```

Length of output: 1322

---



</details>

**UserInformation: 불필요한 JPA 어노테이션 및 ID 필드 제거 검토**

`UserInformation`은 `User` 도큐먼트 내부에 중첩된 값 객체로 사용되고 있으므로, 별도 식별자나 JPA 엔티티로서의 매핑이 필요하지 않습니다. 아래를 검토해주세요.

- UserInformation.java  
  - 클래스 레벨의 `@Entity` 어노테이션 제거  
  - `jakarta.persistence.Id` import 및 `@Id private String id;` 필드 삭제  

이렇게 하면 `UserInformation`은 순수한 POJO로 유지되며, MongoDB 하위 문서로 올바르게 매핑됩니다.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T12:53:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,35 @@
+package moadong.fixture;
+
+import moadong.user.entity.User;
+import moadong.user.entity.UserInformation;
+import moadong.user.payload.CustomUserDetails;
+import moadong.user.payload.request.UserLoginRequest;
+import moadong.user.payload.request.UserRegisterRequest;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+public class UserFixture {
+    public static final String collectUserId = "test12345";
+    public static final String collectPassword = "test12345@";
+    public static final String collectName = "테스터";
+    public static final String collectPhoneNumber = "010-1234-5678";
+    public static UserRegisterRequest createUserRegisterRequest(String userId, String password, String name, String phoneNumber) {
+        return new UserRegisterRequest(userId, password, name, phoneNumber);
+    }
+    public static UserLoginRequest createUserLoginRequest(String userId, String password){
+        return new UserLoginRequest(userId,password);
+    }
+    public static User createUser(PasswordEncoder passwordEncoder,String userId, String password, String name, String phoneNumber){
+        return new UserRegisterRequest(userId, password, name, phoneNumber).toUserEntity(passwordEncoder);
+    }
+
+    public static CustomUserDetails createUserDetails(String userId) {
+        return new CustomUserDetails(
+                User.builder()
+                        .id(userId)
+                        .userInformation(new UserInformation("testuser","010-1234-5677"))
+                        .password("password")
+                        .build()
+        );
+    }
path: backend/src/test/java/moadong/fixture/UserFixture.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**CustomUserDetails 생성 메서드의 일관성이 부족합니다.**

`createUserDetails` 메서드에서 사용하는 사용자 이름과 전화번호가 다른 메서드에서 사용하는 상수와 일치하지 않습니다. 일관성을 위해 미리 정의된 상수를 활용하는 것이 좋습니다.


일관성을 위해 다음과 같이 수정하세요:

```diff
public static CustomUserDetails createUserDetails(String userId) {
    return new CustomUserDetails(
            User.builder()
                    .id(userId)
-                    .userInformation(new UserInformation("testuser","010-1234-5677"))
+                    .userInformation(new UserInformation(collectName, collectPhoneNumber))
                    .password("password")
                    .build()
    );
}
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 95bbbd6 to 1a267ae
created_at: 2025-05-08T12:53:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+package moadong.unit.user;
+
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import moadong.club.entity.Club;
+import moadong.club.repository.ClubRepository;
+import moadong.fixture.UserFixture;
+import moadong.global.exception.RestApiException;
+import moadong.global.util.JwtProvider;
+import moadong.user.entity.User;
+import moadong.user.payload.request.UserRegisterRequest;
+import moadong.user.repository.UserRepository;
+import moadong.user.service.UserCommandService;
+import moadong.user.util.CookieMaker;
+import moadong.util.annotations.UnitTest;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
+
+@UnitTest
+class UserRegisterTest {
+    private static Validator validator;
+    @Spy
+    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+    @Mock
+    private UserRepository userRepository;
+    @Mock
+    private ClubRepository clubRepository;
+    @Mock
+    private AuthenticationManager authenticationManager;
+    @Mock
+    private JwtProvider jwtProvider;
+    @Mock
+    private CookieMaker cookieMaker;
+    @InjectMocks
+    private UserCommandService userCommandService;
+
+    @BeforeAll
+    public static void setUp() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+
+    @Test
+    void 회원가입시_올바른_정보를_입력하면_성공한다() {
+        // given
+        String userId = UserFixture.collectUserId;
+        String password = UserFixture.collectPassword;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+
+        doAnswer(invocation -> invocation.getArgument(0))
+                .when(userRepository).save(any(User.class));
+        doReturn(new Club(userId)).when(clubRepository).save(any(Club.class));
+
+        UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+
+        // when
+        User userResponse = userCommandService.registerUser(request);
+
+        // then
+        assertThat(userResponse).isNotNull();
+        assertThat(userResponse.getUserId()).isEqualTo(userId);
+        assertThat(userResponse.getUserInformation().getName()).isEqualTo(name);
+        assertThat(userResponse.getUserInformation().getPhoneNumber()).isEqualTo(phoneNumber);
+        assertThat(passwordEncoder.matches(password, userResponse.getPassword())).isTrue();
+
+        verify(userRepository, times(1)).save(any(User.class));
+        verify(clubRepository, times(1)).save(any(Club.class));
+    }
+
+    /*
+    아이디 규칙
+    • 5자 ~ 20자
+    • 적어도 하나의 소문자, 하나의 숫자가 포함
+    • 소문자, 대문자, 숫자, 특수문자(!@#$~) 만 사용
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "",                         // 빈 문자열
+            "    ",                     // 공백만
+            "abcde",                    // 숫자 없음
+            "ABCDE",                    // 숫자, 소문자 없음
+            "12345",                    // 소문자 없음
+            "ab!@",                     // 5자 미만 + 숫자 없음
+            "abc123%^",                // 허용되지 않은 특수문자 (`^`)
+            "abc1234567890123456789",  // 21자 (초과)
+            "ab12",                     // 4자 (부족)
+            "ABC123",                   // 소문자 없음
+            "abcd@",                    // 숫자 없음
+            "1234@",                    // 소문자 없음
+            "abc 123",                  // 공백 포함
+            "abC!@#",                   // 숫자 없음
+            "abc123*",                  // `*`은 허용되지 않음
+            "a1@",                      // 길이 부족
+            UserFixture.collectPassword // 비밀번호와 동일 (검증에 따라 실패할 수도 있음)
+    })
+    void 회원가입시_유저_아이디가_조건에_맞지_않으면_실패한다(String userId) {
+        // given
+        String password = UserFixture.collectPassword;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+        //  when
+        try {
+            UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+            Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
+            // then
+            if (violations.isEmpty()) {
+                fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
+            }
+        } catch (RestApiException e) {
+
+        }
path: backend/src/test/java/moadong/unit/user/UserRegisterTest.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**예외 처리 로직을 개선하세요.**

현재 코드는 `RestApiException`을 catch하지만 예외에 대한 추가 검증이나 처리가 없습니다. 또한 예외가 발생했을 때와 검증 오류(violations)가 발생했을 때를 구분하여 처리하는 것이 좋습니다.

```diff
  try {
      UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
      Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
      // then
      if (violations.isEmpty()) {
          fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
      }
-  } catch (RestApiException e) {
-
+  } catch (RestApiException e) {
+      // 예외 메시지나 코드를 검증합니다
+      assertThat(e.getErrorCode()).isNotNull();
  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
        try {
            UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
            Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
            // then
            if (violations.isEmpty()) {
                fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
            }
        } catch (RestApiException e) {
            // 예외 메시지나 코드를 검증합니다
            assertThat(e.getErrorCode()).isNotNull();
        }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T12:53:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+package moadong.unit.user;
+
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import moadong.club.entity.Club;
+import moadong.club.repository.ClubRepository;
+import moadong.fixture.UserFixture;
+import moadong.global.exception.RestApiException;
+import moadong.global.util.JwtProvider;
+import moadong.user.entity.User;
+import moadong.user.payload.request.UserRegisterRequest;
+import moadong.user.repository.UserRepository;
+import moadong.user.service.UserCommandService;
+import moadong.user.util.CookieMaker;
+import moadong.util.annotations.UnitTest;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
+
+@UnitTest
+class UserRegisterTest {
+    private static Validator validator;
+    @Spy
+    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+    @Mock
+    private UserRepository userRepository;
+    @Mock
+    private ClubRepository clubRepository;
+    @Mock
+    private AuthenticationManager authenticationManager;
+    @Mock
+    private JwtProvider jwtProvider;
+    @Mock
+    private CookieMaker cookieMaker;
+    @InjectMocks
+    private UserCommandService userCommandService;
+
+    @BeforeAll
+    public static void setUp() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+
+    @Test
+    void 회원가입시_올바른_정보를_입력하면_성공한다() {
+        // given
+        String userId = UserFixture.collectUserId;
+        String password = UserFixture.collectPassword;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+
+        doAnswer(invocation -> invocation.getArgument(0))
+                .when(userRepository).save(any(User.class));
+        doReturn(new Club(userId)).when(clubRepository).save(any(Club.class));
+
+        UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+
+        // when
+        User userResponse = userCommandService.registerUser(request);
+
+        // then
+        assertThat(userResponse).isNotNull();
+        assertThat(userResponse.getUserId()).isEqualTo(userId);
+        assertThat(userResponse.getUserInformation().getName()).isEqualTo(name);
+        assertThat(userResponse.getUserInformation().getPhoneNumber()).isEqualTo(phoneNumber);
+        assertThat(passwordEncoder.matches(password, userResponse.getPassword())).isTrue();
+
+        verify(userRepository, times(1)).save(any(User.class));
+        verify(clubRepository, times(1)).save(any(Club.class));
+    }
+
+    /*
+    아이디 규칙
+    • 5자 ~ 20자
+    • 적어도 하나의 소문자, 하나의 숫자가 포함
+    • 소문자, 대문자, 숫자, 특수문자(!@#$~) 만 사용
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "",                         // 빈 문자열
+            "    ",                     // 공백만
+            "abcde",                    // 숫자 없음
+            "ABCDE",                    // 숫자, 소문자 없음
+            "12345",                    // 소문자 없음
+            "ab!@",                     // 5자 미만 + 숫자 없음
+            "abc123%^",                // 허용되지 않은 특수문자 (`^`)
+            "abc1234567890123456789",  // 21자 (초과)
+            "ab12",                     // 4자 (부족)
+            "ABC123",                   // 소문자 없음
+            "abcd@",                    // 숫자 없음
+            "1234@",                    // 소문자 없음
+            "abc 123",                  // 공백 포함
+            "abC!@#",                   // 숫자 없음
+            "abc123*",                  // `*`은 허용되지 않음
+            "a1@",                      // 길이 부족
+            UserFixture.collectPassword // 비밀번호와 동일 (검증에 따라 실패할 수도 있음)
+    })
+    void 회원가입시_유저_아이디가_조건에_맞지_않으면_실패한다(String userId) {
+        // given
+        String password = UserFixture.collectPassword;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+        //  when
+        try {
+            UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+            Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
+            // then
+            if (violations.isEmpty()) {
+                fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
+            }
+        } catch (RestApiException e) {
+
+        }
+    }
+
+    /*
+    비밀번호 규칙
+    • 8자 ~ 20자
+    • 숫자랑 영어 대소문자 반드시 하나이상 포함
+    • 특수문자는 반드시 하나가 필요하고 !@#$%^ 만 허용
+    • 공백 포함 불가
+    • 아이디와 동일한 비밀번호 불가
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "short1!",              // 7자 (길이 부족)
+            "longpassword1234567890!", // 21자 (길이 초과)
+            "abcdefgh",             // 영문 소문자만 (숫자 없음)
+            "ABCDEFGH",             // 영문 대문자만 (숫자 없음)
+            "12345678",             // 숫자만 (영문 없음)
+            "Abcdefgh",             // 영문만 (숫자 없음)
+            "abcd1234*",            // 허용되지 않은 특수문자 `*`
+            "abc def123!",          // 공백 포함
+            UserFixture.collectUserId,      // 아이디와 동일하거나 포함
+            "passWord!",            // 특수문자 있음, 숫자 없음
+            "1234!@#$",             // 숫자 + 특수문자, 문자 없음
+            "Abcdef12()",           // 괄호 포함 (허용되지 않은 특수문자)
+            "ABCD1234~",            // `~` 특수문자 허용 안됨
+            "abc1234^",             // `^`는 허용되지만 문자 형식에 따라 위험성 있음
+    })
+    void 회원가입시_유저_비밀번호가_조건에_맞지_않으면_실패한다(String password) {
+        String userId = UserFixture.collectUserId;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+
+        //  when
+        try {
+            UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+            Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
+            // then
+            if (violations.isEmpty()) {
+                fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
+            }
+        } catch (RestApiException e) {
+
path: backend/src/test/java/moadong/unit/user/UserRegisterTest.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**비밀번호 검증 테스트의 예외 처리 구조를 개선하세요.**

회원가입시_유저_비밀번호가_조건에_맞지_않으면_실패한다 테스트에서도 예외 처리 구조를 개선하는 것이 좋습니다. 현재는 예외를 잡지만 추가적인 검증이 없습니다.

```diff
  try {
      UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
      Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
      // then
      if (violations.isEmpty()) {
          fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 password: " + password);
      }
-  } catch (RestApiException e) {
-
+  } catch (RestApiException e) {
+      // 예외 메시지나 코드를 검증합니다
+      assertThat(e.getErrorCode()).isNotNull();
  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
        //  when
        try {
            UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
            Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
            // then
            if (violations.isEmpty()) {
                fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
            }
        } catch (RestApiException e) {
            // 예외 메시지나 코드를 검증합니다
            assertThat(e.getErrorCode()).isNotNull();
        }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-08T12:53:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+package moadong.unit.user;
+
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import moadong.club.entity.Club;
+import moadong.club.repository.ClubRepository;
+import moadong.fixture.UserFixture;
+import moadong.global.exception.RestApiException;
+import moadong.global.util.JwtProvider;
+import moadong.user.entity.User;
+import moadong.user.payload.request.UserRegisterRequest;
+import moadong.user.repository.UserRepository;
+import moadong.user.service.UserCommandService;
+import moadong.user.util.CookieMaker;
+import moadong.util.annotations.UnitTest;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
+
+@UnitTest
+class UserRegisterTest {
+    private static Validator validator;
+    @Spy
+    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+    @Mock
+    private UserRepository userRepository;
+    @Mock
+    private ClubRepository clubRepository;
+    @Mock
+    private AuthenticationManager authenticationManager;
+    @Mock
+    private JwtProvider jwtProvider;
+    @Mock
+    private CookieMaker cookieMaker;
+    @InjectMocks
+    private UserCommandService userCommandService;
+
+    @BeforeAll
+    public static void setUp() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+
+    @Test
+    void 회원가입시_올바른_정보를_입력하면_성공한다() {
+        // given
+        String userId = UserFixture.collectUserId;
+        String password = UserFixture.collectPassword;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+
+        doAnswer(invocation -> invocation.getArgument(0))
+                .when(userRepository).save(any(User.class));
+        doReturn(new Club(userId)).when(clubRepository).save(any(Club.class));
+
+        UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+
+        // when
+        User userResponse = userCommandService.registerUser(request);
+
+        // then
+        assertThat(userResponse).isNotNull();
+        assertThat(userResponse.getUserId()).isEqualTo(userId);
+        assertThat(userResponse.getUserInformation().getName()).isEqualTo(name);
+        assertThat(userResponse.getUserInformation().getPhoneNumber()).isEqualTo(phoneNumber);
+        assertThat(passwordEncoder.matches(password, userResponse.getPassword())).isTrue();
+
+        verify(userRepository, times(1)).save(any(User.class));
+        verify(clubRepository, times(1)).save(any(Club.class));
+    }
+
+    /*
+    아이디 규칙
+    • 5자 ~ 20자
+    • 적어도 하나의 소문자, 하나의 숫자가 포함
+    • 소문자, 대문자, 숫자, 특수문자(!@#$~) 만 사용
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "",                         // 빈 문자열
+            "    ",                     // 공백만
+            "abcde",                    // 숫자 없음
+            "ABCDE",                    // 숫자, 소문자 없음
+            "12345",                    // 소문자 없음
+            "ab!@",                     // 5자 미만 + 숫자 없음
+            "abc123%^",                // 허용되지 않은 특수문자 (`^`)
+            "abc1234567890123456789",  // 21자 (초과)
+            "ab12",                     // 4자 (부족)
+            "ABC123",                   // 소문자 없음
+            "abcd@",                    // 숫자 없음
+            "1234@",                    // 소문자 없음
+            "abc 123",                  // 공백 포함
+            "abC!@#",                   // 숫자 없음
+            "abc123*",                  // `*`은 허용되지 않음
+            "a1@",                      // 길이 부족
+            UserFixture.collectPassword // 비밀번호와 동일 (검증에 따라 실패할 수도 있음)
+    })
+    void 회원가입시_유저_아이디가_조건에_맞지_않으면_실패한다(String userId) {
+        // given
+        String password = UserFixture.collectPassword;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+        //  when
+        try {
+            UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+            Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
+            // then
+            if (violations.isEmpty()) {
+                fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
+            }
+        } catch (RestApiException e) {
+
+        }
+    }
+
+    /*
+    비밀번호 규칙
+    • 8자 ~ 20자
+    • 숫자랑 영어 대소문자 반드시 하나이상 포함
+    • 특수문자는 반드시 하나가 필요하고 !@#$%^ 만 허용
+    • 공백 포함 불가
+    • 아이디와 동일한 비밀번호 불가
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "short1!",              // 7자 (길이 부족)
+            "longpassword1234567890!", // 21자 (길이 초과)
+            "abcdefgh",             // 영문 소문자만 (숫자 없음)
+            "ABCDEFGH",             // 영문 대문자만 (숫자 없음)
+            "12345678",             // 숫자만 (영문 없음)
+            "Abcdefgh",             // 영문만 (숫자 없음)
+            "abcd1234*",            // 허용되지 않은 특수문자 `*`
+            "abc def123!",          // 공백 포함
+            UserFixture.collectUserId,      // 아이디와 동일하거나 포함
+            "passWord!",            // 특수문자 있음, 숫자 없음
+            "1234!@#$",             // 숫자 + 특수문자, 문자 없음
+            "Abcdef12()",           // 괄호 포함 (허용되지 않은 특수문자)
+            "ABCD1234~",            // `~` 특수문자 허용 안됨
+            "abc1234^",             // `^`는 허용되지만 문자 형식에 따라 위험성 있음
+    })
+    void 회원가입시_유저_비밀번호가_조건에_맞지_않으면_실패한다(String password) {
+        String userId = UserFixture.collectUserId;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+
+        //  when
+        try {
+            UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+            Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
+            // then
+            if (violations.isEmpty()) {
+                fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
+            }
+        } catch (RestApiException e) {
+
path: backend/src/test/java/moadong/unit/user/UserRegisterTest.java
user: login: lepitaaar
body: 궁금한데 왜 테스트코드의 예외처리에 아무것도 없을까요? @Zepelown 
created_at: 2025-05-10T02:24:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+package moadong.unit.user;
+
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import moadong.club.entity.Club;
+import moadong.club.repository.ClubRepository;
+import moadong.fixture.UserFixture;
+import moadong.global.exception.RestApiException;
+import moadong.global.util.JwtProvider;
+import moadong.user.entity.User;
+import moadong.user.payload.request.UserRegisterRequest;
+import moadong.user.repository.UserRepository;
+import moadong.user.service.UserCommandService;
+import moadong.user.util.CookieMaker;
+import moadong.util.annotations.UnitTest;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
+
+@UnitTest
+class UserRegisterTest {
+    private static Validator validator;
+    @Spy
+    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+    @Mock
+    private UserRepository userRepository;
+    @Mock
+    private ClubRepository clubRepository;
+    @Mock
+    private AuthenticationManager authenticationManager;
+    @Mock
+    private JwtProvider jwtProvider;
+    @Mock
+    private CookieMaker cookieMaker;
+    @InjectMocks
+    private UserCommandService userCommandService;
+
+    @BeforeAll
+    public static void setUp() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+
+    @Test
+    void 회원가입시_올바른_정보를_입력하면_성공한다() {
+        // given
+        String userId = UserFixture.collectUserId;
+        String password = UserFixture.collectPassword;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+
+        doAnswer(invocation -> invocation.getArgument(0))
+                .when(userRepository).save(any(User.class));
+        doReturn(new Club(userId)).when(clubRepository).save(any(Club.class));
+
+        UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+
+        // when
+        User userResponse = userCommandService.registerUser(request);
+
+        // then
+        assertThat(userResponse).isNotNull();
+        assertThat(userResponse.getUserId()).isEqualTo(userId);
+        assertThat(userResponse.getUserInformation().getName()).isEqualTo(name);
+        assertThat(userResponse.getUserInformation().getPhoneNumber()).isEqualTo(phoneNumber);
+        assertThat(passwordEncoder.matches(password, userResponse.getPassword())).isTrue();
+
+        verify(userRepository, times(1)).save(any(User.class));
+        verify(clubRepository, times(1)).save(any(Club.class));
+    }
+
+    /*
+    아이디 규칙
+    • 5자 ~ 20자
+    • 적어도 하나의 소문자, 하나의 숫자가 포함
+    • 소문자, 대문자, 숫자, 특수문자(!@#$~) 만 사용
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "",                         // 빈 문자열
+            "    ",                     // 공백만
+            "abcde",                    // 숫자 없음
+            "ABCDE",                    // 숫자, 소문자 없음
+            "12345",                    // 소문자 없음
+            "ab!@",                     // 5자 미만 + 숫자 없음
+            "abc123%^",                // 허용되지 않은 특수문자 (`^`)
+            "abc1234567890123456789",  // 21자 (초과)
+            "ab12",                     // 4자 (부족)
+            "ABC123",                   // 소문자 없음
+            "abcd@",                    // 숫자 없음
+            "1234@",                    // 소문자 없음
+            "abc 123",                  // 공백 포함
+            "abC!@#",                   // 숫자 없음
+            "abc123*",                  // `*`은 허용되지 않음
+            "a1@",                      // 길이 부족
+            UserFixture.collectPassword // 비밀번호와 동일 (검증에 따라 실패할 수도 있음)
+    })
+    void 회원가입시_유저_아이디가_조건에_맞지_않으면_실패한다(String userId) {
+        // given
+        String password = UserFixture.collectPassword;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+        //  when
+        try {
+            UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+            Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
+            // then
+            if (violations.isEmpty()) {
+                fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
+            }
+        } catch (RestApiException e) {
+
+        }
+    }
+
+    /*
+    비밀번호 규칙
+    • 8자 ~ 20자
+    • 숫자랑 영어 대소문자 반드시 하나이상 포함
+    • 특수문자는 반드시 하나가 필요하고 !@#$%^ 만 허용
+    • 공백 포함 불가
+    • 아이디와 동일한 비밀번호 불가
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "short1!",              // 7자 (길이 부족)
+            "longpassword1234567890!", // 21자 (길이 초과)
+            "abcdefgh",             // 영문 소문자만 (숫자 없음)
+            "ABCDEFGH",             // 영문 대문자만 (숫자 없음)
+            "12345678",             // 숫자만 (영문 없음)
+            "Abcdefgh",             // 영문만 (숫자 없음)
+            "abcd1234*",            // 허용되지 않은 특수문자 `*`
+            "abc def123!",          // 공백 포함
+            UserFixture.collectUserId,      // 아이디와 동일하거나 포함
+            "passWord!",            // 특수문자 있음, 숫자 없음
+            "1234!@#$",             // 숫자 + 특수문자, 문자 없음
+            "Abcdef12()",           // 괄호 포함 (허용되지 않은 특수문자)
+            "ABCD1234~",            // `~` 특수문자 허용 안됨
+            "abc1234^",             // `^`는 허용되지만 문자 형식에 따라 위험성 있음
+    })
+    void 회원가입시_유저_비밀번호가_조건에_맞지_않으면_실패한다(String password) {
+        String userId = UserFixture.collectUserId;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+
+        //  when
+        try {
+            UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+            Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request);
+            // then
+            if (violations.isEmpty()) {
+                fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId);
+            }
+        } catch (RestApiException e) {
+
path: backend/src/test/java/moadong/unit/user/UserRegisterTest.java
user: login: coderabbitai[bot]
body: > Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

<!-- This is an auto-generated mute comment by CodeRabbit -->
created_at: 2025-05-10T02:24:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,10 @@
+package moadong.global.util;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class TestTypeConstants {
path: backend/src/main/java/moadong/global/util/TestTypeConstants.java
user: login: Due-IT
body: 해당 어노테이션을 프로덕션 패키지내에 정의한 이유가 있나요?
created_at: 2025-05-10T05:40:06Z
author_association: COLLABORATOR
diff_hunk: @@ -7,7 +7,7 @@
 import java.util.regex.Pattern;
 
 public class PasswordValidator implements ConstraintValidator<Password, String> {
-    private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^]).{8,20}$");
+    private static final Pattern PASSWORD_PATTERN = Pattern.compile("\"^(?=.*[a-zA-Z])(?=.*\\\\d)(?=.*[!@#$%^])[a-zA-Z\\\\d!@#$%^]{8,20}$\"\n");
path: backend/src/main/java/moadong/global/validator/PasswordValidator.java
user: login: Due-IT
body: 변경전과 후가 어떻게 다른걸까요?
created_at: 2025-05-10T05:41:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,35 @@
+package moadong.fixture;
+
+import moadong.user.entity.User;
+import moadong.user.entity.UserInformation;
+import moadong.user.payload.CustomUserDetails;
+import moadong.user.payload.request.UserLoginRequest;
+import moadong.user.payload.request.UserRegisterRequest;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+public class UserFixture {
+    public static final String collectUserId = "test12345";
+    public static final String collectPassword = "test12345@";
+    public static final String collectName = "테스터";
+    public static final String collectPhoneNumber = "010-1234-5678";
+    public static UserRegisterRequest createUserRegisterRequest(String userId, String password, String name, String phoneNumber) {
+        return new UserRegisterRequest(userId, password, name, phoneNumber);
+    }
+    public static UserLoginRequest createUserLoginRequest(String userId, String password){
+        return new UserLoginRequest(userId,password);
+    }
+    public static User createUser(PasswordEncoder passwordEncoder,String userId, String password, String name, String phoneNumber){
+        return new UserRegisterRequest(userId, password, name, phoneNumber).toUserEntity(passwordEncoder);
+    }
path: backend/src/test/java/moadong/fixture/UserFixture.java
user: login: Due-IT
body: 테스트 코드에서 요청용 DTO를 명시적으로 생성하는 것이, 테스트를 명세서처럼 활용하는 데 도움이 된다고 생각합니다. 만약 이와 같이 사용한다 하더라도 테스트를 위한 User모델을 만드는 UserFixture에 위치하는건조금 이상하다고 생각되네요
created_at: 2025-05-10T05:48:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,83 @@
+package moadong.fixture;
+
+import moadong.club.entity.Club;
+import moadong.club.entity.ClubRecruitmentInformation;
+import moadong.club.enums.ClubRecruitmentStatus;
+import moadong.club.enums.ClubState;
+import moadong.club.payload.request.ClubInfoRequest;
+import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Map;
+
+public class ClubFixture {
+    public static final ClubState DEFAULT_STATE = ClubState.UNAVAILABLE;
+    public static Club createClub() {
+        return Club.builder()
+                .name("테스트 동아리")
+                .category("학술")
+                .division("프로그래밍")
+                .clubRecruitmentInformation(createRecruitmentInfo())
+                .build();
+    }
path: backend/src/test/java/moadong/fixture/ClubFixture.java
user: login: Due-IT
body: 제안과 같이 mock을 활용하면 객체의 수정할 수 없는 부분도 테스트 가능하고, 테스트를 위해 프로덕션 코드가 수정될 가능성이 많이 줄어듭니다!
```suggestion
    public static Club createClub(String clubId, String name) {
        Club club = mock(Club.class);
        when(club.getId()).thenReturn(clubId);
        when(club.getName()).thenReturn(name);
        return club;
    }
```
created_at: 2025-05-10T05:52:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,121 @@
+package moadong.unit.club;
+
+import moadong.club.entity.Club;
+import moadong.club.payload.request.ClubCreateRequest;
+import moadong.club.payload.request.ClubInfoRequest;
+import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest;
+import moadong.club.repository.ClubRepository;
+import moadong.club.service.ClubProfileService;
+import moadong.club.service.RecruitmentScheduler;
+import moadong.fixture.ClubFixture;
+import moadong.global.exception.RestApiException;
+import moadong.user.payload.CustomUserDetails;
+import moadong.util.annotations.UnitTest;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.Optional;
+
+import static moadong.fixture.UserFixture.createUserDetails;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@UnitTest
+public class ClubProfileServiceTest {
+    private final String clubId = "club_123";
+    private final String userId = "user_456";
+    @Mock
+    private ClubRepository clubRepository;
+    @Mock
+    private RecruitmentScheduler recruitmentScheduler;
+    @InjectMocks
+    private ClubProfileService clubProfileService;
+
+    @Test
+    public void 정상적으로_클럽을_생성한다() {
+        // Given
+        ClubCreateRequest request = new ClubCreateRequest("테스트", "카테고리", "분과");
+        when(clubRepository.save(any())).thenReturn(mock(Club.class));
+
+        // When
+        clubProfileService.createClub(request);
+
+        // Then
+        verify(clubRepository, times(1)).save(any(Club.class));
+    }
+    @Test
+    void 정상적으로_클럽_약력을_업데이트한다() {
+        // Given
+        ClubInfoRequest request = ClubFixture.createValidClubInfoRequest();
+        CustomUserDetails user = createUserDetails(userId);
+        Club mockClub = mock(Club.class);
+
+        when(clubRepository.findById(clubId)).thenReturn(Optional.of(mockClub));
+        when(mockClub.getUserId()).thenReturn(userId);
+
+        // When
+        clubProfileService.updateClubInfo(request, user);
+
+        // Then
+        verify(mockClub).update(request);
+        verify(clubRepository).save(mockClub);
+    }
+
+    @Test
+    void 클럽이_없을_땐_클럽_약력_업데이트가_실패한다() {
+        when(clubRepository.findById(any())).thenReturn(Optional.empty());
+        assertThrows(RestApiException.class,
+                () -> clubProfileService.updateClubInfo(ClubFixture.createValidClubInfoRequest(), createUserDetails(userId)));
+    }
+
+    @Test
+    void 권한이_없는_클럽은_클럽_약력_업데이트_할_수_없다() {
+        Club mockClub = mock(Club.class);
+        when(clubRepository.findById(clubId)).thenReturn(Optional.of(mockClub));
+        when(mockClub.getUserId()).thenReturn("different_user");
+
+        assertThrows(RestApiException.class,
+                () -> clubProfileService.updateClubInfo(ClubFixture.createValidClubInfoRequest(), createUserDetails(userId)));
+    }
+
+
+//    ToDo: 시간 계산법을 LocalDateTime에서 Instant로 변경 후에 활성화할 것
path: backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java
user: login: Due-IT
body: 아래 주석처리된 코드부분에서 날짜/시간 생성시에만 ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime(); 을 통해 생성후 사용하시면 될것 같습니다! Instant를 사용하는건 글로벌 서비스로 확장될 가능성이 있을때 변경하여도 될것 같네요
created_at: 2025-05-10T05:56:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,174 @@
+package moadong.unit.user;
+
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import moadong.club.entity.Club;
+import moadong.club.repository.ClubRepository;
+import moadong.fixture.UserFixture;
+import moadong.global.exception.RestApiException;
+import moadong.global.util.JwtProvider;
+import moadong.user.entity.User;
+import moadong.user.payload.request.UserRegisterRequest;
+import moadong.user.repository.UserRepository;
+import moadong.user.service.UserCommandService;
+import moadong.user.util.CookieMaker;
+import moadong.util.annotations.UnitTest;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
+
+@UnitTest
+class UserRegisterTest {
+    private static Validator validator;
+    @Spy
+    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+    @Mock
+    private UserRepository userRepository;
+    @Mock
+    private ClubRepository clubRepository;
+    @Mock
+    private AuthenticationManager authenticationManager;
+    @Mock
+    private JwtProvider jwtProvider;
+    @Mock
+    private CookieMaker cookieMaker;
+    @InjectMocks
+    private UserCommandService userCommandService;
+
+    @BeforeAll
+    public static void setUp() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+
+    @Test
+    void 회원가입시_올바른_정보를_입력하면_성공한다() {
+        // given
+        String userId = UserFixture.collectUserId;
+        String password = UserFixture.collectPassword;
+        String name = UserFixture.collectName;
+        String phoneNumber = UserFixture.collectPhoneNumber;
+
+        doAnswer(invocation -> invocation.getArgument(0))
+                .when(userRepository).save(any(User.class));
+        doReturn(new Club(userId)).when(clubRepository).save(any(Club.class));
+
+        UserRegisterRequest request = UserFixture.createUserRegisterRequest(userId, password, name, phoneNumber);
+
+        // when
+        User userResponse = userCommandService.registerUser(request);
+
+        // then
+        assertThat(userResponse).isNotNull();
+        assertThat(userResponse.getUserId()).isEqualTo(userId);
+        assertThat(userResponse.getUserInformation().getName()).isEqualTo(name);
+        assertThat(userResponse.getUserInformation().getPhoneNumber()).isEqualTo(phoneNumber);
+        assertThat(passwordEncoder.matches(password, userResponse.getPassword())).isTrue();
+
+        verify(userRepository, times(1)).save(any(User.class));
+        verify(clubRepository, times(1)).save(any(Club.class));
+    }
+
+    /*
+    아이디 규칙
+    • 5자 ~ 20자
+    • 적어도 하나의 소문자, 하나의 숫자가 포함
+    • 소문자, 대문자, 숫자, 특수문자(!@#$~) 만 사용
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {
path: backend/src/test/java/moadong/unit/user/UserRegisterTest.java
user: login: Due-IT
body: 다양한 패턴을 미리 검증하니 너무 든든하네요
created_at: 2025-05-10T05:57:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,35 @@
+package moadong.fixture;
+
+import moadong.user.entity.User;
+import moadong.user.entity.UserInformation;
+import moadong.user.payload.CustomUserDetails;
+import moadong.user.payload.request.UserLoginRequest;
+import moadong.user.payload.request.UserRegisterRequest;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+public class UserFixture {
+    public static final String collectUserId = "test12345";
+    public static final String collectPassword = "test12345@";
+    public static final String collectName = "테스터";
+    public static final String collectPhoneNumber = "010-1234-5678";
+    public static UserRegisterRequest createUserRegisterRequest(String userId, String password, String name, String phoneNumber) {
+        return new UserRegisterRequest(userId, password, name, phoneNumber);
+    }
+    public static UserLoginRequest createUserLoginRequest(String userId, String password){
+        return new UserLoginRequest(userId,password);
+    }
+    public static User createUser(PasswordEncoder passwordEncoder,String userId, String password, String name, String phoneNumber){
+        return new UserRegisterRequest(userId, password, name, phoneNumber).toUserEntity(passwordEncoder);
+    }
path: backend/src/test/java/moadong/fixture/UserFixture.java
user: login: PororoAndFriends
body: <img width="517" alt="image" src="https://github.com/user-attachments/assets/2f5ee33b-dd19-485d-b228-df4751e7ac6d" />

이전에 사용했던 테스트코드인데 변수를 받아서 설정하는것까지 사용하고싶다면 기본값을 넣고 builder를 리턴하여 테스트코드 내에서 수정할 수 있도록 만드는 건 어떨까요? DataBuilder패턴이라고 하더라고요
created_at: 2025-05-10T06:14:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,10 @@
+package moadong.global.util;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class TestTypeConstants {
path: backend/src/main/java/moadong/global/util/TestTypeConstants.java
user: login: Zepelown
body: import 문제가 있어서 프로덕션에 넣어뒀는데 다시 리팩토링하였습니다.
created_at: 2025-05-10T06:34:51Z
author_association: MEMBER
diff_hunk: @@ -7,7 +7,7 @@
 import java.util.regex.Pattern;
 
 public class PasswordValidator implements ConstraintValidator<Password, String> {
-    private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^]).{8,20}$");
+    private static final Pattern PASSWORD_PATTERN = Pattern.compile("\"^(?=.*[a-zA-Z])(?=.*\\\\d)(?=.*[!@#$%^])[a-zA-Z\\\\d!@#$%^]{8,20}$\"\n");
path: backend/src/main/java/moadong/global/validator/PasswordValidator.java
user: login: Zepelown
body: 기존엔 123456a@ 과 같이 끝에 특수문자가 들어간 경우를 검증을 못했는데 이를 변경했습니다.
created_at: 2025-05-10T06:35:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,35 @@
+package moadong.fixture;
+
+import moadong.user.entity.User;
+import moadong.user.entity.UserInformation;
+import moadong.user.payload.CustomUserDetails;
+import moadong.user.payload.request.UserLoginRequest;
+import moadong.user.payload.request.UserRegisterRequest;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+public class UserFixture {
+    public static final String collectUserId = "test12345";
+    public static final String collectPassword = "test12345@";
+    public static final String collectName = "테스터";
+    public static final String collectPhoneNumber = "010-1234-5678";
+    public static UserRegisterRequest createUserRegisterRequest(String userId, String password, String name, String phoneNumber) {
+        return new UserRegisterRequest(userId, password, name, phoneNumber);
+    }
+    public static UserLoginRequest createUserLoginRequest(String userId, String password){
+        return new UserLoginRequest(userId,password);
+    }
+    public static User createUser(PasswordEncoder passwordEncoder,String userId, String password, String name, String phoneNumber){
+        return new UserRegisterRequest(userId, password, name, phoneNumber).toUserEntity(passwordEncoder);
+    }
path: backend/src/test/java/moadong/fixture/UserFixture.java
user: login: Zepelown
body: UserFixture에 분리해서 따로 두도록 하겠습니다.
record class로 구현을 했어서 builder를 추가로 쓰는 건 과한 것 같습니다.
회의에서 record class를 아예 다 builder로 바꿀지 말지를 토론해봐야겠네요.
created_at: 2025-05-10T07:05:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,121 @@
+package moadong.unit.club;
+
+import moadong.club.entity.Club;
+import moadong.club.payload.request.ClubCreateRequest;
+import moadong.club.payload.request.ClubInfoRequest;
+import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest;
+import moadong.club.repository.ClubRepository;
+import moadong.club.service.ClubProfileService;
+import moadong.club.service.RecruitmentScheduler;
+import moadong.fixture.ClubFixture;
+import moadong.global.exception.RestApiException;
+import moadong.user.payload.CustomUserDetails;
+import moadong.util.annotations.UnitTest;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.Optional;
+
+import static moadong.fixture.UserFixture.createUserDetails;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@UnitTest
+public class ClubProfileServiceTest {
+    private final String clubId = "club_123";
+    private final String userId = "user_456";
+    @Mock
+    private ClubRepository clubRepository;
+    @Mock
+    private RecruitmentScheduler recruitmentScheduler;
+    @InjectMocks
+    private ClubProfileService clubProfileService;
+
+    @Test
+    public void 정상적으로_클럽을_생성한다() {
+        // Given
+        ClubCreateRequest request = new ClubCreateRequest("테스트", "카테고리", "분과");
+        when(clubRepository.save(any())).thenReturn(mock(Club.class));
+
+        // When
+        clubProfileService.createClub(request);
+
+        // Then
+        verify(clubRepository, times(1)).save(any(Club.class));
+    }
+    @Test
+    void 정상적으로_클럽_약력을_업데이트한다() {
+        // Given
+        ClubInfoRequest request = ClubFixture.createValidClubInfoRequest();
+        CustomUserDetails user = createUserDetails(userId);
+        Club mockClub = mock(Club.class);
+
+        when(clubRepository.findById(clubId)).thenReturn(Optional.of(mockClub));
+        when(mockClub.getUserId()).thenReturn(userId);
+
+        // When
+        clubProfileService.updateClubInfo(request, user);
+
+        // Then
+        verify(mockClub).update(request);
+        verify(clubRepository).save(mockClub);
+    }
+
+    @Test
+    void 클럽이_없을_땐_클럽_약력_업데이트가_실패한다() {
+        when(clubRepository.findById(any())).thenReturn(Optional.empty());
+        assertThrows(RestApiException.class,
+                () -> clubProfileService.updateClubInfo(ClubFixture.createValidClubInfoRequest(), createUserDetails(userId)));
+    }
+
+    @Test
+    void 권한이_없는_클럽은_클럽_약력_업데이트_할_수_없다() {
+        Club mockClub = mock(Club.class);
+        when(clubRepository.findById(clubId)).thenReturn(Optional.of(mockClub));
+        when(mockClub.getUserId()).thenReturn("different_user");
+
+        assertThrows(RestApiException.class,
+                () -> clubProfileService.updateClubInfo(ClubFixture.createValidClubInfoRequest(), createUserDetails(userId)));
+    }
+
+
+//    ToDo: 시간 계산법을 LocalDateTime에서 Instant로 변경 후에 활성화할 것
path: backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java
user: login: Zepelown
body: 테스트에서 문제가 발생해서 Instant로 수정하는 거 아니었나요?
created_at: 2025-05-10T07:09:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+package moadong.fixture;
+
+import moadong.club.entity.Club;
+import moadong.club.entity.ClubRecruitmentInformation;
+import moadong.club.enums.ClubRecruitmentStatus;
+import moadong.club.enums.ClubState;
+import moadong.club.payload.request.ClubInfoRequest;
+import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Map;
+
+public class ClubFixture {
+    public static final ClubState DEFAULT_STATE = ClubState.UNAVAILABLE;
+    public static Club createClub() {
+        return Club.builder()
+                .name("테스트 동아리")
+                .category("학술")
+                .division("프로그래밍")
+                .clubRecruitmentInformation(createRecruitmentInfo())
+                .build();
+    }
path: backend/src/test/java/moadong/fixture/ClubFixture.java
user: login: Zepelown
body: 제안대로 수정해보겠습니다
created_at: 2025-05-10T07:09:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,29 @@
+package moadong.fixture;
+
+import moadong.user.entity.User;
+import moadong.user.entity.UserInformation;
+import moadong.user.payload.CustomUserDetails;
+import moadong.user.payload.request.UserRegisterRequest;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+public class UserFixture {
+    public static final String collectUserId = "test12345";
+    public static final String collectPassword = "test12345@";
+    public static final String collectName = "테스터";
+    public static final String collectPhoneNumber = "010-1234-5678";
+
+    public static User createUser(PasswordEncoder passwordEncoder, String userId, String password, String name, String phoneNumber) {
+        return new UserRegisterRequest(userId, password, name, phoneNumber).toUserEntity(passwordEncoder);
+    }
+
+    public static CustomUserDetails createUserDetails(String userId) {
+        return new CustomUserDetails(
+                User.builder()
+                        .id(userId)
+                        .userInformation(new UserInformation(collectUserId, collectPhoneNumber))
+                        .password(collectPassword)
+                        .build()
+        );
+    }
path: backend/src/test/java/moadong/fixture/UserFixture.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**매개변수 활용 오류 수정 필요**

`createUserDetails` 메서드에서 `userId` 매개변수를 받고 있지만, 실제로는 이 값을 제대로 활용하지 않고 있습니다. 23번 라인에서 `userInformation`을 생성할 때 `userId` 대신 고정된 상수 `collectUserId`를 사용하고 있습니다.

```diff
public static CustomUserDetails createUserDetails(String userId) {
    return new CustomUserDetails(
            User.builder()
                    .id(userId)
-                    .userInformation(new UserInformation(collectUserId, collectPhoneNumber))
+                    .userInformation(new UserInformation(collectName, collectPhoneNumber))
                    .password(collectPassword)
                    .build()
    );
}
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-10T07:29:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,7 +7,7 @@
 import java.util.regex.Pattern;
 
 public class PasswordValidator implements ConstraintValidator<Password, String> {
-    private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^]).{8,20}$");
+    private static final Pattern PASSWORD_PATTERN = Pattern.compile("\"^(?=.*[a-zA-Z])(?=.*\\\\d)(?=.*[!@#$%^])[a-zA-Z\\\\d!@#$%^]{8,20}$\"\n");
path: backend/src/main/java/moadong/global/validator/PasswordValidator.java
user: login: tgyuuAn
body: 이런 정규 표현식은 주석을 달아주는게 좋아요. 주석 + 테스트까지
created_at: 2025-05-10T12:04:07Z
author_association: NONE
diff_hunk: @@ -7,7 +7,7 @@
 import java.util.regex.Pattern;
 
 public class PasswordValidator implements ConstraintValidator<Password, String> {
-    private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^]).{8,20}$");
+    private static final Pattern PASSWORD_PATTERN = Pattern.compile("\"^(?=.*[a-zA-Z])(?=.*\\\\d)(?=.*[!@#$%^])[a-zA-Z\\\\d!@#$%^]{8,20}$\"\n");
path: backend/src/main/java/moadong/global/validator/PasswordValidator.java
user: login: Zepelown
body: > 이런 정규 표현식은 주석을 달아주는게 좋아요. 주석 + 테스트까지

어쩌다가 이런 누추한 곳에 안대장님이.. 
감사합니다 반영해보겠습니다


created_at: 2025-05-10T12:23:22Z
author_association: MEMBER
diff_hunk: @@ -7,7 +7,7 @@
 import java.util.regex.Pattern;
 
 public class PasswordValidator implements ConstraintValidator<Password, String> {
-    private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^]).{8,20}$");
+    private static final Pattern PASSWORD_PATTERN = Pattern.compile("\"^(?=.*[a-zA-Z])(?=.*\\\\d)(?=.*[!@#$%^])[a-zA-Z\\\\d!@#$%^]{8,20}$\"\n");
path: backend/src/main/java/moadong/global/validator/PasswordValidator.java
user: login: tgyuuAn
body: 두영이 시키 뭐하고 사나 구경하다가 왔습니다 하하하
created_at: 2025-05-10T12:25:03Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-08T12:48:28Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

```markdown
## Walkthrough

이번 변경에서는 테스트 태스크 분리(유닛/통합), 사용자 및 클럽 관련 엔티티 구조 개편, 회원가입 및 로그인 로직 개선, 테스트 픽스처 및 커스텀 테스트 어노테이션 도입, 그리고 기존 통합 테스트 삭제 및 유닛 테스트 추가가 이루어졌습니다. 비즈니스 로직과 테스트 코드 모두에서 구조적 변화가 포함되어 있습니다.

## Changes

| 파일/그룹 | 변경 요약 |
|---|---|
| `backend/build.gradle` | JUnit 태그 기반의 `unitTest`, `integrationTest` 태스크 추가 및 테스트용 Lombok 의존성 추가. 기존 `test` 태스크 명시적 선언. |
| `.../PasswordValidator.java` | 비밀번호 정규식 패턴 변경(문자 허용 범위 명확화, 패턴 문자열 오류 포함). |
| `.../user/entity/User.java` | `UserInformation` 필드 추가 및 MongoDB 필드 매핑 어노테이션 적용. |
| `.../user/entity/UserInformation.java` | `@Document` 어노테이션 및 `userId` 필드 제거, `name`과 `phoneNumber`만 받는 생성자 추가. |
| `.../user/payload/request/UserRegisterRequest.java` | 필드 검증 어노테이션을 `@NotBlank`로 변경, 유저ID와 비밀번호 동일성 검증 compact 생성자 추가, `PasswordEncoder`를 활용한 엔티티 변환 방식으로 변경. |
| `.../user/repository/UserInformationRepository.java` | `UserInformationRepository` 인터페이스 삭제. |
| `.../user/service/UserCommandService.java` | `UserInformationRepository` 의존성 및 관련 로직 제거, 회원가입 반환 타입을 `void`에서 `User`로 변경, 패스워드 인코딩 및 유저 생성 방식 변경. |
| `backend/src/test/java/moadong/fixture/ClubFixture.java`<br>`backend/src/test/java/moadong/fixture/UserFixture.java` | 클럽 및 유저 관련 테스트 픽스처 클래스 신규 추가, 테스트 데이터 생성 메서드 제공. |
| `.../unit/club/ClubProfileServiceTest.java` | 클럽 프로필 서비스 유닛 테스트 신규 추가(생성, 업데이트, 권한/존재 예외 등). |
| `.../unit/user/UserLoginTest.java` | 유저 로그인 유닛 테스트 신규 추가(성공/실패 케이스). |
| `.../unit/user/UserRegisterTest.java` | 유저 회원가입 유닛 테스트 신규 추가(정상/유효성 실패 케이스, 파라미터라이즈드 테스트 포함). |
| `.../util/annotations/IntegrationTest.java`<br>`.../util/annotations/UnitTest.java`<br>`.../util/annotations/TestTypeConstants.java` | 커스텀 테스트 어노테이션(`@UnitTest`, `@IntegrationTest`) 및 테스트 타입 상수 클래스 추가. |
| `.../fixture/ClubRequestFixture.java`<br>`.../fixture/UserRequestFixture.java` | 클럽 및 유저 요청 관련 테스트 픽스처 클래스 신규 추가, 요청 객체 생성 메서드 제공. |
| `.../user/service/UserCommandServiceTest.java` | 기존 통합 테스트 클래스 삭제. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User as 사용자
    participant Controller as UserController
    participant Service as UserCommandService
    participant Repo as UserRepository
    participant Encoder as PasswordEncoder
    participant ClubRepo as ClubRepository

    User->>Controller: 회원가입 요청(UserRegisterRequest)
    Controller->>Service: registerUser(request)
    Service->>Encoder: 비밀번호 인코딩
    Service->>Repo: 유저 저장
    Service->>ClubRepo: createClub(유저 ID)
    Service-->>Controller: User 반환
    Controller-->>User: 회원가입 응답

    User->>Controller: 로그인 요청(UserLoginRequest)
    Controller->>Service: loginUser(request, response)
    Service->>Repo: 유저 조회
    Service->>Encoder: 패스워드 검증
    Service->>ClubRepo: 유저의 클럽 조회
    Service-->>Controller: 토큰/클럽ID 반환
    Controller-->>User: 로그인 응답
```

## Suggested reviewers

- Due-IT
- PororoAndFriends
```

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Moadong/moadong&utm_content=385):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-08T12:50:17Z
body: ## Test Results
64 tests   64 ✅  3s ⏱️
13 suites   0 💤
13 files     0 ❌

Results for commit 65e64b55.

[test-results]:data:application/gzip;base64,H4sIAPBIH2gC/12MSQ7CMBAEvxL5zCEkXvkMMs6MNCKJkZcT4u/YAbJw66qW6smQRojs0pz7U8NiprTRkINN5OfCFcuT6if5D64xO/dn7vQopl0FWhoPAkLw4WtCntdi3YfgR2y9hXe5hfc156eJUgEmBUh+EwJAo1Oi5yikNtIOHQeuOo3SKIO2Za83AbeTKAEBAAA=

:recycle: This comment has been updated with latest results.
author_association: NONE