diff --git a/src/main/java/com/munecting/api/domain/oidc/dto/OidcUserInfo.java b/src/main/java/com/munecting/api/domain/oidc/dto/OidcUserInfo.java index ff2430b..b58f726 100644 --- a/src/main/java/com/munecting/api/domain/oidc/dto/OidcUserInfo.java +++ b/src/main/java/com/munecting/api/domain/oidc/dto/OidcUserInfo.java @@ -4,14 +4,12 @@ @Builder public record OidcUserInfo( - String sub, - String email + String sub ) { - public static OidcUserInfo of(String sub, String email) { + public static OidcUserInfo of(String sub) { return OidcUserInfo.builder() .sub(sub) - .email(email) .build(); } } diff --git a/src/main/java/com/munecting/api/domain/oidc/strategy/impl/AppleOidcStrategy.java b/src/main/java/com/munecting/api/domain/oidc/strategy/impl/AppleOidcStrategy.java index 508937a..d463e00 100644 --- a/src/main/java/com/munecting/api/domain/oidc/strategy/impl/AppleOidcStrategy.java +++ b/src/main/java/com/munecting/api/domain/oidc/strategy/impl/AppleOidcStrategy.java @@ -30,6 +30,6 @@ public OidcUserInfo authenticate(String idToken) { Map header = getHeader(idToken); Claims claims = getClaimsWithVerifySign(idToken, header); - return OidcUserInfo.of(claims.getSubject(), claims.get("email").toString()); + return OidcUserInfo.of(claims.getSubject()); } } diff --git a/src/main/java/com/munecting/api/domain/oidc/strategy/impl/GoogleOidcStrategy.java b/src/main/java/com/munecting/api/domain/oidc/strategy/impl/GoogleOidcStrategy.java index b0f1b0e..15be12a 100644 --- a/src/main/java/com/munecting/api/domain/oidc/strategy/impl/GoogleOidcStrategy.java +++ b/src/main/java/com/munecting/api/domain/oidc/strategy/impl/GoogleOidcStrategy.java @@ -26,8 +26,7 @@ public OidcUserInfo authenticate(String idToken) { GoogleIdToken googleIdToken = verifyIdToken(idToken); String subject = googleIdToken.getPayload().getSubject(); - String email = googleIdToken.getPayload().getEmail(); - return OidcUserInfo.of(subject, email); + return OidcUserInfo.of(subject); } private GoogleIdToken verifyIdToken(final String idToken) { diff --git a/src/main/java/com/munecting/api/domain/oidc/strategy/impl/KakaoOidcStrategy.java b/src/main/java/com/munecting/api/domain/oidc/strategy/impl/KakaoOidcStrategy.java index 90fa3f3..8c84af2 100644 --- a/src/main/java/com/munecting/api/domain/oidc/strategy/impl/KakaoOidcStrategy.java +++ b/src/main/java/com/munecting/api/domain/oidc/strategy/impl/KakaoOidcStrategy.java @@ -31,6 +31,6 @@ public OidcUserInfo authenticate(String idToken) { Map header = getHeader(idToken); Claims claims = getClaimsWithVerifySign(idToken, header); - return OidcUserInfo.of(claims.getSubject(), claims.get("email").toString()); + return OidcUserInfo.of(claims.getSubject()); } } diff --git a/src/main/java/com/munecting/api/domain/user/entity/User.java b/src/main/java/com/munecting/api/domain/user/entity/User.java index 5f0417b..880511a 100644 --- a/src/main/java/com/munecting/api/domain/user/entity/User.java +++ b/src/main/java/com/munecting/api/domain/user/entity/User.java @@ -25,6 +25,7 @@ public class User extends BaseEntity { private String socialId; @NotBlank + @Column(unique = true) private String nickname; @Column(nullable = true) @@ -38,6 +39,20 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private SocialType socialType; + public static User toEntity(String socialId, String nickname, Role role, SocialType socialType) { + return toEntity(socialId, nickname, null, role, socialType); + } + + public static User toEntity(String socialId, String nickname, String profileImageUrl, Role role, SocialType socialType) { + return User.builder() + .socialId(socialId) + .nickname(nickname) + .profileImageUrl(profileImageUrl) + .role(role) + .socialType(socialType) + .build(); + } + public String updateNickname(String nickname) { this.nickname = nickname; diff --git a/src/main/java/com/munecting/api/domain/user/service/AuthService.java b/src/main/java/com/munecting/api/domain/user/service/AuthService.java index a330168..22de6da 100644 --- a/src/main/java/com/munecting/api/domain/user/service/AuthService.java +++ b/src/main/java/com/munecting/api/domain/user/service/AuthService.java @@ -32,6 +32,7 @@ public class AuthService { private final RedisTemplate redisTemplate; private final OidcService oidcService; private final UserRepository userRepository; + private final UserNicknameService nicknameService; @Value("${jwt.refresh.expiration}") private long refreshTokenExpiration; @@ -77,25 +78,23 @@ private String getRedisKey(Long userId) { @Transactional public UserTokenResponseDto getOrCreateUser(LoginRequestDto dto) { - OidcUserInfo oidcUserInfo = oidcService.getOidcUserInfo(dto.socialType(), dto.idToken()); - String socialId = dto.socialType().toString() + "_" + oidcUserInfo.sub(); + String socialId = generateSocialId(dto); User user = userRepository.findBySocialId(socialId) - .orElseGet(() -> - createUser(socialId, oidcUserInfo.email(), dto.socialType()) - ); + .orElseGet(() -> createUser(socialId, dto.socialType())); return issueTokensForUser(user); } - //TODO: 이메일 제거, 닉네임 자체 생성 - private User createUser(String socialId, String email, SocialType socialType) { - User newUser = User.builder() - .socialId(socialId) - .nickname(email.split("@")[0]) - .role(Role.USER) - .socialType(socialType) - .build(); + private String generateSocialId(LoginRequestDto dto) { + OidcUserInfo oidcUserInfo = oidcService.getOidcUserInfo(dto.socialType(), dto.idToken()); + + return dto.socialType().toString() + "_" + oidcUserInfo.sub(); + } + + private User createUser(String socialId, SocialType socialType) { + User newUser = User.toEntity( + socialId, nicknameService.generateUniqueNickname(), Role.USER, socialType); return userRepository.save(newUser); } diff --git a/src/main/java/com/munecting/api/domain/user/service/UserNicknameService.java b/src/main/java/com/munecting/api/domain/user/service/UserNicknameService.java new file mode 100644 index 0000000..9c0467d --- /dev/null +++ b/src/main/java/com/munecting/api/domain/user/service/UserNicknameService.java @@ -0,0 +1,102 @@ +package com.munecting.api.domain.user.service; + +import com.munecting.api.domain.user.dao.UserRepository; +import com.munecting.api.domain.user.entity.User; +import com.munecting.api.global.error.exception.InternalServerException; +import com.munecting.api.global.error.exception.InvalidValueException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.Random; + +import static com.munecting.api.global.common.dto.response.Status.*; + +@Service +@Slf4j +@RequiredArgsConstructor +public class UserNicknameService { + + private final UserRepository userRepository; + + private static final int MIN_LENGTH = 2; + private static final int MAX_LENGTH = 15; + + private static final String NAME_VALUE_RULES = "^(?=.*[a-zA-Z0-9가-힣])[a-zA-Z가-힣][a-zA-Z0-9가-힣_]*$"; + + private static final String DEFAULT_NICKNAME = "뮤넥터"; + private static final String DELIMITER = "_"; + private static final int MAX_UNIQUE_STRING_LENGTH = 10; + private static final int MAX_NICKNAME_RETRY_COUNT = 100; + private static final String CHARACTERS_POOL = "0123456789abcdefghijklmnopqrstuvwxyz"; + private final Random random = new Random(); + + public String updateNickname(User user, String nickname) { + if (!StringUtils.hasText(nickname)) { + return user.getNickname(); + } + + validateNickname(user, nickname); + return user.updateNickname(nickname); + } + + private void validateNickname(User existingUser, String nickname) { + if (isInvalidNicknameLength(nickname)) { + throw new InvalidValueException(INVALID_NICKNAME_LENGTH); + } + + if (isInvalidNamingRule(nickname)) { + throw new InvalidValueException(INVALID_NICKNAME_VALUE); + } + + if (isDuplicatedNickname(existingUser, nickname)) { + throw new InvalidValueException(DUPLICATED_NICKNAME); + } + } + + private boolean isInvalidNicknameLength(String nickname) { + return nickname.length() < MIN_LENGTH || nickname.length() > MAX_LENGTH; + } + + private boolean isInvalidNamingRule(String nickname) { + return !nickname.matches(NAME_VALUE_RULES); + } + + private boolean isDuplicatedNickname(User existingUser, String nickname) { + // 기존 닉네임과 일치하는 경우 중복 체크 제외 + if (existingUser.getNickname().equals(nickname)) { + return false; + } + + return userRepository.existsByNickname(nickname); + } + + public String generateUniqueNickname() { + String nickname; + int attemptCount = 0; + + do { + if (attemptCount >= MAX_NICKNAME_RETRY_COUNT) { + throw new InternalServerException("닉네임 생성에 실패하였습니다."); + } + nickname = DEFAULT_NICKNAME + DELIMITER + generateUniqueString(); + attemptCount++; + + } while (userRepository.existsByNickname(nickname)); + + return nickname; + } + + private String generateUniqueString() { + int uniqueStringLength = random.nextInt(MAX_UNIQUE_STRING_LENGTH); + log.info("uniqueStringLength : {}", uniqueStringLength); + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < uniqueStringLength; i++) { + result.append(CHARACTERS_POOL.charAt(random.nextInt(CHARACTERS_POOL.length()))); + } + + return result.toString(); + } +} diff --git a/src/main/java/com/munecting/api/domain/user/service/UserService.java b/src/main/java/com/munecting/api/domain/user/service/UserService.java index f3e2dc9..28ee843 100644 --- a/src/main/java/com/munecting/api/domain/user/service/UserService.java +++ b/src/main/java/com/munecting/api/domain/user/service/UserService.java @@ -8,14 +8,11 @@ import com.munecting.api.domain.user.dto.response.GetProfileResponseDto; import com.munecting.api.domain.user.dto.response.UpdateProfileResponseDto; import com.munecting.api.domain.user.entity.User; -import com.munecting.api.global.common.dto.response.Status; import com.munecting.api.global.error.exception.EntityNotFoundException; -import com.munecting.api.global.error.exception.InvalidValueException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import static com.munecting.api.global.common.dto.response.Status.USER_NOT_FOUND; @@ -29,17 +26,8 @@ public class UserService { private final CommentRepository commentRepository; private final LikeRepository likeRepository; private final UploadedMusicRepository uploadedMusicRepository; - private final UserProfileImageService userProfileImageService; - - private static final int MIN_LENGTH = 2; - private static final int MAX_LENGTH = 15; - - private static final String NAME_VALUE_RULES = "^[a-zA-Z0-9가-힣]+$"; - - private static final String NICKNAME_LENGTH_ERROR_MESSAGE = "닉네임은 2-15자 사이여야 합니다."; - private static final String NICKNAME_WRONG_VALUE_ERROR_MESSAGE = "닉네임은 한글, 영문, 숫자만 포함할 수 있습니다."; - private static final String NICKNAME_DUPLICATED_ERROR_MESSAGE = "이미 사용 중인 닉네임입니다."; - + private final UserProfileImageService profileImageService; + private final UserNicknameService nicknameService; @Transactional public void deleteUser(Long userId) { @@ -75,32 +63,11 @@ public User findUserByIdOrThrow (Long userId) { } private String updateNickname(User user, String nickname) { - if (!StringUtils.hasText(nickname)) { - return user.getNickname(); - } - - validateNickname(user, nickname); - return user.updateNickname(nickname); - } - - private void validateNickname(User existingUser, String nickname) { - if (nickname.length() < MIN_LENGTH || nickname.length() > MAX_LENGTH) { - throw new InvalidValueException(Status.BAD_REQUEST, NICKNAME_LENGTH_ERROR_MESSAGE); - } - - if (!nickname.matches(NAME_VALUE_RULES)) { - throw new InvalidValueException(Status.BAD_REQUEST, NICKNAME_WRONG_VALUE_ERROR_MESSAGE); - } - - // 중복 검사 - if (!existingUser.getNickname().equals(nickname) - && userRepository.existsByNickname(nickname)) { - throw new InvalidValueException(Status.CONFLICT, NICKNAME_DUPLICATED_ERROR_MESSAGE); - } + return nicknameService.updateNickname(user, nickname); } private String updateProfileImage(User user, MultipartFile imgFile) { - return userProfileImageService.updateImage(user, imgFile); + return profileImageService.updateImage(user, imgFile); } @Transactional(readOnly = true) diff --git a/src/main/java/com/munecting/api/global/common/dto/response/Status.java b/src/main/java/com/munecting/api/global/common/dto/response/Status.java index dee66d7..47fdace 100644 --- a/src/main/java/com/munecting/api/global/common/dto/response/Status.java +++ b/src/main/java/com/munecting/api/global/common/dto/response/Status.java @@ -44,6 +44,9 @@ public enum Status { // User 오류 응답 USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", "존재하지 않는 회원입니다."), + INVALID_NICKNAME_LENGTH(HttpStatus.BAD_REQUEST, "USER_NICKNAME400", "닉네임은 2-15자 사이여야 합니다."), + INVALID_NICKNAME_VALUE(HttpStatus.BAD_REQUEST, "USER_NICKNAME400", "닉네임은 한글, 영문, 숫자, 언더바(_)만 사용 가능하며, 첫 글자는 언더바 또는 숫자일 수 없습니다."), + DUPLICATED_NICKNAME(HttpStatus.CONFLICT, "USER_NICKNAME409","이미 사용 중인 닉네임입니다."), // 분산락 오류 응답 DISTRIBUTED_LOCK_ACQUISITION_FAILURE(HttpStatus.CONFLICT, "LOCK409", "잠시 후에 시도해주세요."),