diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java index 7ba25497..62e69672 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java @@ -196,8 +196,8 @@ public void changeNickname(String nickname, LocalDateTime now) { this.nicknameUpdatedAt = now; } - public boolean canChangeNickname() { + public boolean canChangeNickname(int restrictionMinutes, LocalDateTime now) { return nicknameUpdatedAt == null - || ChronoUnit.HOURS.between(nicknameUpdatedAt, LocalDateTime.now()) >= 24; + || ChronoUnit.MINUTES.between(nicknameUpdatedAt, now) >= restrictionMinutes; } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/policy/NicknameChangePolicy.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/policy/NicknameChangePolicy.java new file mode 100644 index 00000000..059e0cb5 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/policy/NicknameChangePolicy.java @@ -0,0 +1,14 @@ +package com.dreamypatisiel.devdevdev.domain.policy; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class NicknameChangePolicy { + @Value("${nickname.change.interval.minutes:1440}") + private int nicknameChangeIntervalMinutes; + + public int getNicknameChangeIntervalMinutes() { + return nicknameChangeIntervalMinutes; + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberService.java index 622c5439..8d17f0aa 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberService.java @@ -2,6 +2,7 @@ import com.dreamypatisiel.devdevdev.domain.entity.*; import com.dreamypatisiel.devdevdev.domain.entity.embedded.CustomSurveyAnswer; +import com.dreamypatisiel.devdevdev.domain.policy.NicknameChangePolicy; import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; import com.dreamypatisiel.devdevdev.domain.repository.comment.CommentRepository; import com.dreamypatisiel.devdevdev.domain.repository.comment.MyWrittenCommentDto; @@ -47,6 +48,8 @@ import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.MEMBER_INCOMPLETE_SURVEY_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.NicknameExceptionMessage.NICKNAME_CHANGE_RATE_LIMIT_MESSAGE; +import org.springframework.beans.factory.annotation.Value; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -63,6 +66,7 @@ public class MemberService { private final SurveyAnswerJdbcTemplateRepository surveyAnswerJdbcTemplateRepository; private final CommentRepository commentRepository; private final CompanyRepository companyRepository; + private final NicknameChangePolicy nicknameChangePolicy; /** * 회원 탈퇴 회원의 북마크와 회원 정보를 삭제합니다. @@ -290,7 +294,7 @@ public SliceCustom findMySubscribedCompanies(Pageable } /** - * @Note: 유저의 닉네임을 변경합니다. 최근 24시간 이내에 변경한 이력이 있다면 닉네임 변경이 불가능합니다. + * @Note: 유저의 닉네임을 변경합니다. 설정된 제한 시간 이내에 변경한 이력이 있다면 닉네임 변경이 불가능합니다. * @Author: 유소영 * @Since: 2025.07.03 */ @@ -298,7 +302,7 @@ public SliceCustom findMySubscribedCompanies(Pageable public String changeNickname(String nickname, Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); - if (!member.canChangeNickname()) { + if (!member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalMinutes(), timeProvider.getLocalDateTimeNow())) { throw new NicknameException(NICKNAME_CHANGE_RATE_LIMIT_MESSAGE); } @@ -313,6 +317,6 @@ public String changeNickname(String nickname, Authentication authentication) { */ public boolean canChangeNickname(Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); - return member.canChangeNickname(); + return member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalMinutes(), timeProvider.getLocalDateTimeNow()); } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java index 84f77efe..76124f35 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java @@ -9,23 +9,48 @@ class MemberTest { + @ParameterizedTest + @CsvSource({ + ", true", // 변경 이력 없음(null) + "60, false", // 24시간 이내 + "1439, false", // 24시간 이내 + "1440, true", // 24시간 경과(경계) + "1550, true", // 24시간 초과 + }) + @DisplayName("닉네임 변경 가능 여부 파라미터 테스트") + void canChangeNickname(Long minutesAgo, boolean expected) { + // given + LocalDateTime now = LocalDateTime.now(); + Member member = new Member(); + if (minutesAgo != null) { + member.changeNickname("닉네임", now.minusMinutes(minutesAgo)); + } + int restrictionMinutes = 1440; // 24시간 + // when + boolean result = member.canChangeNickname(restrictionMinutes, now); + // then + assertThat(result).isEqualTo(expected); + } + @ParameterizedTest @CsvSource({ ", true", // 변경 이력 없음(null) "0, false", // 24시간 이내 - "1, false", // 24시간 이내 - "24, true", // 24시간 경과(경계) - "25, true", // 24시간 초과 + "1, true", // 24시간 이내 + "60, true", // 24시간 경과(경계) + "1440, true", // 24시간 초과 }) @DisplayName("닉네임 변경 가능 여부 파라미터 테스트") - void canChangeNickname(Long hoursAgo, boolean expected) { + void canChangeNicknameWhenDev(Long minutesAgo, boolean expected) { // given + LocalDateTime now = LocalDateTime.now(); Member member = new Member(); - if (hoursAgo != null) { - member.changeNickname("닉네임", LocalDateTime.now().minusHours(hoursAgo)); + if (minutesAgo != null) { + member.changeNickname("닉네임", now.minusMinutes(minutesAgo)); } + int restrictionMinutes = 1; // 1분 // when - boolean result = member.canChangeNickname(); + boolean result = member.canChangeNickname(restrictionMinutes, now); // then assertThat(result).isEqualTo(expected); } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java index 47e17764..1df770b8 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java @@ -37,6 +37,7 @@ import com.dreamypatisiel.devdevdev.exception.NicknameException; import com.dreamypatisiel.devdevdev.exception.SurveyException; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; +import com.dreamypatisiel.devdevdev.global.common.TimeProvider; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; @@ -62,6 +63,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.auditing.AuditingHandler; import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.domain.PageRequest; @@ -119,6 +121,8 @@ class MemberServiceTest extends ElasticsearchSupportTest { PickCommentRepository pickCommentRepository; @Autowired SubscriptionRepository subscriptionRepository; + @MockBean + TimeProvider timeProvider; @Test @DisplayName("회원이 회원탈퇴 설문조사를 완료하지 않으면 탈퇴가 불가능하다.") @@ -458,6 +462,8 @@ void getBookmarkedTechArticlesNotFoundMemberException() { @DisplayName("회원탈퇴 서베이 이력을 기록한다.") void recordMemberExitSurveyAnswer() { // given + when(timeProvider.getLocalDateTimeNow()).thenReturn(LocalDateTime.of(2024, 1, 1, 0, 0, 0, 0)); + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); Member member = Member.createMemberBy(socialMemberDto); memberRepository.save(member); @@ -1205,24 +1211,27 @@ void changeNickname() { assertThat(changedNickname).isEqualTo(newNickname); } - @DisplayName("회원이 24시간 이내에 닉네임을 변경한 적이 있다면 예외가 발생한다.") + @DisplayName("회원이 1440분(24시간) 이내에 닉네임을 변경한 적이 있다면 예외가 발생한다.") @ParameterizedTest @CsvSource({ "0, true", - "1, true", - "23, true", - "24, false", // 변경 허용 - "25, false" // 변경 허용 + "60, true", // 1시간 + "1439, true", // 23.9시간 + "1440, false", // 24시간, 변경 허용 + "1500, false" // 25시간, 변경 허용 }) - void changeNicknameThrowsExceptionWhenChangedWithin24Hours(long hoursAgo, boolean shouldThrowException) { + void changeNicknameThrowsExceptionWhenChangedWithin24Hours(long minutesAgo, boolean shouldThrowException) { // given + LocalDateTime fixedNow = LocalDateTime.of(2024, 1, 1, 12, 0, 0); + when(timeProvider.getLocalDateTimeNow()).thenReturn(fixedNow); + String oldNickname = "이전 닉네임"; String newNickname = "새 닉네임"; SocialMemberDto socialMemberDto = createSocialDto(userId, name, oldNickname, password, email, socialType, role); Member member = Member.createMemberBy(socialMemberDto); - member.changeNickname(oldNickname, LocalDateTime.now().minusHours(hoursAgo)); + member.changeNickname(oldNickname, fixedNow.minusMinutes(minutesAgo)); memberRepository.save(member); UserPrincipal userPrincipal = UserPrincipal.createByMember(member); @@ -1247,20 +1256,23 @@ void changeNicknameThrowsExceptionWhenChangedWithin24Hours(long hoursAgo, boolea @ParameterizedTest @CsvSource({ "0, false", - "1, false", - "23, false", - "24, true", - "25, true" + "60, false", // 1시간 + "1439, false", // 23.9시간 + "1440, true", // 24시간 + "1500, true" // 25시간 }) - void canChangeNickname(long hoursAgo, boolean expected) { + void canChangeNickname(long minutesAgo, boolean expected) { // given + LocalDateTime fixedNow = LocalDateTime.of(2024, 1, 1, 12, 0, 0); + when(timeProvider.getLocalDateTimeNow()).thenReturn(fixedNow); + String oldNickname = "이전 닉네임"; String newNickname = "새 닉네임"; SocialMemberDto socialMemberDto = createSocialDto(userId, name, oldNickname, password, email, socialType, role); Member member = Member.createMemberBy(socialMemberDto); - member.changeNickname(newNickname, LocalDateTime.now().minusHours(hoursAgo)); + member.changeNickname(newNickname, fixedNow.minusMinutes(minutesAgo)); memberRepository.save(member); UserPrincipal userPrincipal = UserPrincipal.createByMember(member);