Skip to content

Commit 67669c4

Browse files
authored
Merge pull request #301 from checkmo2025/refactor/300/nickname-korean
refactor: 한글 닉네임 및 대소문자 무시 정책 적용
2 parents 0b610ed + f371f62 commit 67669c4

22 files changed

Lines changed: 735 additions & 50 deletions

File tree

src/main/java/checkmo/authentication/internal/entity/AuthUser.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package checkmo.authentication.internal.entity;
22

33
import checkmo.common.BaseEntity;
4+
import checkmo.common.nickname.NicknamePolicy;
45
import jakarta.persistence.Column;
56
import jakarta.persistence.Entity;
67
import jakarta.persistence.EnumType;
78
import jakarta.persistence.Enumerated;
89
import jakarta.persistence.GeneratedValue;
910
import jakarta.persistence.GenerationType;
1011
import jakarta.persistence.Id;
12+
import jakarta.persistence.PrePersist;
13+
import jakarta.persistence.PreUpdate;
14+
import jakarta.persistence.Table;
15+
import jakarta.persistence.UniqueConstraint;
1116
import java.time.LocalDateTime;
1217
import lombok.AccessLevel;
1318
import lombok.AllArgsConstructor;
@@ -20,6 +25,11 @@
2025
@AllArgsConstructor
2126
@NoArgsConstructor(access = AccessLevel.PROTECTED)
2227
@Entity
28+
@Table(
29+
uniqueConstraints = {
30+
@UniqueConstraint(name = "UK_auth_user_nickname_key", columnNames = "nick_name_key")
31+
}
32+
)
2333
public class AuthUser extends BaseEntity {
2434

2535
@Id
@@ -51,6 +61,9 @@ public class AuthUser extends BaseEntity {
5161
@Column(length = 20)
5262
private String nickName;
5363

64+
@Column(name = "nick_name_key", length = 20)
65+
private String nickNameKey;
66+
5467
private LocalDateTime deactivatedAt;
5568

5669
public void deactivate() {
@@ -82,7 +95,14 @@ public void updateEmail(String newEmail) {
8295
}
8396

8497
public void updateNickname(String nickName) {
85-
this.nickName = nickName;
98+
this.nickName = NicknamePolicy.normalizeForStorage(nickName);
99+
this.nickNameKey = NicknamePolicy.comparisonKey(this.nickName);
100+
}
101+
102+
@PrePersist
103+
@PreUpdate
104+
private void synchronizeNicknameIdentity() {
105+
updateNickname(nickName);
86106
}
87107

88108
public boolean isAdmin() {

src/main/java/checkmo/authentication/internal/repository/AuthRepository.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@
33
import checkmo.authentication.internal.entity.AuthUser;
44
import java.util.Optional;
55
import org.springframework.data.jpa.repository.JpaRepository;
6-
import org.springframework.data.jpa.repository.Query;
7-
import org.springframework.data.repository.query.Param;
86

97
public interface AuthRepository extends JpaRepository<AuthUser, Long> {
108

119
Optional<AuthUser> findByEmail(String email);
10+
Optional<AuthUser> findByEmailIgnoreCase(String email);
11+
Optional<AuthUser> findByNickNameKey(String nickNameKey);
1212
Optional<AuthUser> findByIdAndDeactivatedAtIsNotNull(Long id);
1313
Optional<AuthUser> findByProviderAndProviderUserId(String provider, String providerUserId);
1414

1515
boolean existsByEmail(String email);
1616

17-
@Query("SELECT a FROM AuthUser a WHERE a.email = :identifier OR a.nickName = :identifier")
18-
Optional<AuthUser> findByIdentifier(@Param("identifier") String identifier);
1917
}

src/main/java/checkmo/authentication/internal/security/auth/CustomUserDetailsService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import checkmo.authentication.internal.exception.AuthErrorStatus;
55
import checkmo.authentication.internal.exception.AuthException;
66
import checkmo.authentication.internal.repository.AuthRepository;
7+
import checkmo.common.nickname.NicknamePolicy;
78
import lombok.RequiredArgsConstructor;
89
import org.springframework.security.core.userdetails.UserDetails;
910
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -23,7 +24,8 @@ public class CustomUserDetailsService implements UserDetailsService {
2324

2425
@Override
2526
public UserDetails loadUserByUsername(String identifier) throws UsernameNotFoundException {
26-
AuthUser user = authRepository.findByIdentifier(identifier)
27+
AuthUser user = authRepository.findByEmailIgnoreCase(identifier)
28+
.or(() -> authRepository.findByNickNameKey(NicknamePolicy.comparisonKey(identifier)))
2729
.orElseThrow(() -> new UsernameNotFoundException(
2830
"해당 이메일 또는 아이디를 가진 사용자를 찾을 수 없습니다: " + identifier));
2931

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package checkmo.common.nickname;
2+
3+
import java.text.Normalizer;
4+
import java.util.Locale;
5+
6+
public final class NicknamePolicy {
7+
8+
public static final int MAX_LENGTH = 20;
9+
public static final String ALLOWED_SPECIAL_CHARACTERS = "!@#$%^&*()_+-=[]{};':\"\\|,.<>/?";
10+
11+
private NicknamePolicy() {
12+
}
13+
14+
public static String normalize(String nickname) {
15+
if (nickname == null) {
16+
return null;
17+
}
18+
return Normalizer.normalize(nickname, Normalizer.Form.NFC);
19+
}
20+
21+
public static String comparisonKey(String nickname) {
22+
String normalized = normalize(nickname);
23+
if (normalized == null || normalized.isEmpty()) {
24+
return null;
25+
}
26+
return normalized.toLowerCase(Locale.ROOT);
27+
}
28+
29+
public static String normalizeForStorage(String nickname) {
30+
ValidationError validationError = validate(nickname, false);
31+
if (validationError != ValidationError.NONE) {
32+
throw new IllegalArgumentException(validationError.getMessage());
33+
}
34+
35+
String normalized = normalize(nickname);
36+
return normalized == null || normalized.isEmpty() ? null : normalized;
37+
}
38+
39+
public static boolean isSameIdentity(String first, String second) {
40+
String firstKey = comparisonKey(first);
41+
return firstKey != null && firstKey.equals(comparisonKey(second));
42+
}
43+
44+
public static ValidationError validate(String nickname, boolean required) {
45+
if (nickname == null || nickname.isEmpty()) {
46+
return required ? ValidationError.REQUIRED : ValidationError.NONE;
47+
}
48+
49+
String normalized = normalize(nickname);
50+
if (normalized.codePoints().anyMatch(NicknamePolicy::isWhitespace)) {
51+
return ValidationError.WHITESPACE;
52+
}
53+
if (normalized.codePointCount(0, normalized.length()) > MAX_LENGTH) {
54+
return ValidationError.TOO_LONG;
55+
}
56+
if (!normalized.codePoints().allMatch(NicknamePolicy::isAllowed)) {
57+
return ValidationError.INVALID_CHARACTER;
58+
}
59+
return ValidationError.NONE;
60+
}
61+
62+
private static boolean isWhitespace(int codePoint) {
63+
return Character.isWhitespace(codePoint) || Character.isSpaceChar(codePoint);
64+
}
65+
66+
private static boolean isAllowed(int codePoint) {
67+
return isHangulSyllable(codePoint)
68+
|| isHangulCompatibilityJamo(codePoint)
69+
|| isAsciiLetter(codePoint)
70+
|| isAsciiDigit(codePoint)
71+
|| ALLOWED_SPECIAL_CHARACTERS.indexOf(codePoint) >= 0;
72+
}
73+
74+
private static boolean isHangulSyllable(int codePoint) {
75+
return codePoint >= '\uAC00' && codePoint <= '\uD7A3';
76+
}
77+
78+
private static boolean isHangulCompatibilityJamo(int codePoint) {
79+
return (codePoint >= '\u3131' && codePoint <= '\u314E')
80+
|| (codePoint >= '\u314F' && codePoint <= '\u3163');
81+
}
82+
83+
private static boolean isAsciiLetter(int codePoint) {
84+
return (codePoint >= 'A' && codePoint <= 'Z')
85+
|| (codePoint >= 'a' && codePoint <= 'z');
86+
}
87+
88+
private static boolean isAsciiDigit(int codePoint) {
89+
return codePoint >= '0' && codePoint <= '9';
90+
}
91+
92+
public enum ValidationError {
93+
NONE(""),
94+
REQUIRED("닉네임은 필수입니다."),
95+
WHITESPACE("닉네임에는 공백을 사용할 수 없습니다."),
96+
TOO_LONG("닉네임은 최대 20자까지 가능합니다."),
97+
INVALID_CHARACTER("닉네임은 한글, 영문, 숫자, 허용된 특수문자만 사용할 수 있습니다.");
98+
99+
private final String message;
100+
101+
ValidationError(String message) {
102+
this.message = message;
103+
}
104+
105+
public String getMessage() {
106+
return message;
107+
}
108+
}
109+
}

src/main/java/checkmo/member/internal/entity/Member.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package checkmo.member.internal.entity;
22

33
import checkmo.common.BaseEntity;
4+
import checkmo.common.nickname.NicknamePolicy;
45
import checkmo.member.internal.exception.MemberErrorStatus;
56
import checkmo.member.internal.exception.MemberException;
67
import jakarta.persistence.CascadeType;
@@ -15,7 +16,10 @@
1516
import jakarta.persistence.Index;
1617
import jakarta.persistence.JoinColumn;
1718
import jakarta.persistence.OneToMany;
19+
import jakarta.persistence.PrePersist;
20+
import jakarta.persistence.PreUpdate;
1821
import jakarta.persistence.Table;
22+
import jakarta.persistence.UniqueConstraint;
1923
import java.util.ArrayList;
2024
import java.util.HashSet;
2125
import java.time.LocalDateTime;
@@ -35,6 +39,9 @@
3539
@Table(
3640
indexes = {
3741
@Index(name = "idx_member_deactivated_at", columnList = "deactivated_at")
42+
},
43+
uniqueConstraints = {
44+
@UniqueConstraint(name = "UK_member_nickname_key", columnNames = "nick_name_key")
3845
}
3946
)
4047
public class Member extends BaseEntity {
@@ -58,6 +65,9 @@ public class Member extends BaseEntity {
5865
@Column(length = 20)
5966
private String nickName;
6067

68+
@Column(name = "nick_name_key", length = 20)
69+
private String nickNameKey;
70+
6171
@Column(length = 40)
6272
private String description;
6373

@@ -88,14 +98,25 @@ public class Member extends BaseEntity {
8898
private Set<MemberInterestCategory> interestCategories = new HashSet<>();
8999

90100
public void updateAdditionalInfo(String nickName, String name, String phoneNumber, String description) {
91-
this.nickName = nickName;
101+
updateNickname(nickName);
92102
this.name = name != null ? name : "";
93103
this.phoneNumber = phoneNumber != null ? phoneNumber : "";
94104
this.description = description;
95105
}
96106

97107
public void updateNickname(String nickName) {
98-
this.nickName = nickName;
108+
this.nickName = NicknamePolicy.normalizeForStorage(nickName);
109+
this.nickNameKey = NicknamePolicy.comparisonKey(this.nickName);
110+
}
111+
112+
public boolean hasSameNicknameIdentity(String nickName) {
113+
return NicknamePolicy.isSameIdentity(this.nickName, nickName);
114+
}
115+
116+
@PrePersist
117+
@PreUpdate
118+
private void synchronizeNicknameIdentity() {
119+
updateNickname(nickName);
99120
}
100121

101122
public void updateProfile(String description, String imgUrl, String phoneNumber) {

src/main/java/checkmo/member/internal/repository/MemberRepository.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616

1717
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
1818

19-
boolean existsByNickNameAndDeactivatedAtIsNull(String nickName);
19+
boolean existsByNickNameKey(String nickNameKey);
2020
Optional<Member> findByIdAndDeactivatedAtIsNull(Long id);
2121
List<Member> findAllByIdInAndDeactivatedAtIsNull(List<Long> ids);
2222

23-
@Query("select m from Member m where m.nickName = :nickName and m.deactivatedAt is null")
24-
Optional<Member> findByNickName(@Param("nickName") String nickName);
23+
@Query("select m from Member m where m.nickNameKey = :nickNameKey and m.deactivatedAt is null")
24+
Optional<Member> findByNickNameKey(@Param("nickNameKey") String nickNameKey);
2525

26-
@Query("select m.id from Member m where m.nickName = :nickName and m.deactivatedAt is null")
27-
Optional<Long> findIdByNickName(@Param("nickName") String nickName);
26+
@Query("select m.id from Member m where m.nickNameKey = :nickNameKey and m.deactivatedAt is null")
27+
Optional<Long> findIdByNickNameKey(@Param("nickNameKey") String nickNameKey);
2828

2929
@Query("select m.nickName from Member m where m.id = :memberId and m.deactivatedAt is null")
3030
Optional<String> findActiveNicknameById(@Param("memberId") Long memberId);

src/main/java/checkmo/member/internal/service/command/MemberBlockCommandService.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package checkmo.member.internal.service.command;
22

3+
import checkmo.common.nickname.NicknamePolicy;
34
import checkmo.member.internal.entity.Member;
45
import checkmo.member.internal.entity.MemberBlock;
56
import checkmo.member.internal.exception.MemberErrorStatus;
@@ -25,7 +26,7 @@ public class MemberBlockCommandService {
2526
public void block(Long blockerId, String blockedNickname) {
2627
Member blocker = memberRepository.findByIdAndDeactivatedAtIsNull(blockerId)
2728
.orElseThrow(() -> new MemberException(MemberErrorStatus.MEMBER_NOT_FOUND));
28-
Member blocked = memberRepository.findByNickName(blockedNickname)
29+
Member blocked = memberRepository.findByNickNameKey(NicknamePolicy.comparisonKey(blockedNickname))
2930
.orElseThrow(() -> new MemberException(MemberErrorStatus.MEMBER_NOT_FOUND));
3031

3132
if (Member.isSameMember(blocker.getId(), blocked.getId())) {
@@ -50,7 +51,7 @@ public void block(Long blockerId, String blockedNickname) {
5051
}
5152

5253
public void unblock(Long blockerId, String blockedNickname) {
53-
Long blockedId = memberRepository.findIdByNickName(blockedNickname)
54+
Long blockedId = memberRepository.findIdByNickNameKey(NicknamePolicy.comparisonKey(blockedNickname))
5455
.orElseThrow(() -> new MemberException(MemberErrorStatus.MEMBER_NOT_FOUND));
5556

5657
MemberBlock memberBlock = memberBlockRepository.findByBlocker_IdAndBlocked_Id(blockerId, blockedId)

0 commit comments

Comments
 (0)