Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 3 additions & 23 deletions src/main/java/com/example/RealMatch/oauth/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.example.RealMatch.oauth.code.OAuthErrorCode;
import com.example.RealMatch.oauth.dto.OAuthTokenResponse;
import com.example.RealMatch.oauth.dto.request.SignupCompleteRequest;
import com.example.RealMatch.user.application.util.NicknameValidator;
import com.example.RealMatch.user.domain.entity.ContentCategory;
import com.example.RealMatch.user.domain.entity.SignupPurpose;
import com.example.RealMatch.user.domain.entity.Term;
Expand Down Expand Up @@ -41,6 +42,7 @@ public class AuthService {
private final ContentCategoryRepository contentCategoryRepository;
private final UserContentCategoryRepository userContentCategoryRepository;
private final JwtProvider jwtProvider;
private final NicknameValidator nicknameValidator;

public OAuthTokenResponse completeSignup(Long userId, String providerId, SignupCompleteRequest request) {
// 유저 조회
Expand All @@ -53,7 +55,7 @@ public OAuthTokenResponse completeSignup(Long userId, String providerId, SignupC
}

// ⭐ 닉네임 중복 체크 추가
validateNickname(request.nickname());
nicknameValidator.validate(request.nickname());

// 유저 정보 업데이트
user.completeSignup(
Expand Down Expand Up @@ -113,28 +115,6 @@ private String extractToken(String header) {
return header;
}

private void validateNickname(String nickname) {
// 1. null/빈 문자열 체크
if (nickname == null || nickname.trim().isEmpty()) {
throw new CustomException(OAuthErrorCode.INVALID_NICKNAME);
}

// 2. 길이 체크 (2~10자)
if (nickname.length() < 2 || nickname.length() > 10) {
throw new CustomException(OAuthErrorCode.INVALID_NICKNAME_LENGTH);
}

// 3. 형식 체크 (한글, 영문, 숫자만)
if (!nickname.matches("^[가-힣a-zA-Z0-9]+$")) {
throw new CustomException(OAuthErrorCode.INVALID_NICKNAME_FORMAT);
}

// 4. 중복 체크
if (userRepository.existsByNickname(nickname)) {
throw new CustomException(OAuthErrorCode.DUPLICATE_NICKNAME);
}
}

private void saveTermAgreements(User user, List<SignupCompleteRequest.TermAgreementDto> terms) {
// 요청 데이터 존재 여부 확인
if (terms == null || terms.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import com.example.RealMatch.global.exception.CustomException;
import com.example.RealMatch.match.domain.repository.MatchCampaignHistoryRepository;
import com.example.RealMatch.user.application.util.NicknameValidator;
import com.example.RealMatch.user.domain.entity.AuthenticationMethod;
import com.example.RealMatch.user.domain.entity.User;
import com.example.RealMatch.user.domain.entity.UserContentCategory;
Expand Down Expand Up @@ -39,6 +40,7 @@ public class UserService {
private final AuthenticationMethodRepository authenticationMethodRepository;
private final UserMatchingDetailRepository userMatchingDetailRepository;
private final UserContentCategoryRepository userContentCategoryRepository;
private final NicknameValidator nicknameValidator;

public MyPageResponseDto getMyPage(Long userId) {
// 유저 조회 (존재하지 않거나 삭제된 유저 예외 처리)
Expand Down Expand Up @@ -166,4 +168,9 @@ public MyLoginResponseDto getSocialLoginInfo(Long userId) {
// DTO 변환 및 반환
return MyLoginResponseDto.from(linkedProviders);
}

@Transactional(readOnly = true)
public boolean isNicknameAvailable(String nickname) {
return nicknameValidator.isAvailable(nickname);
}
Comment on lines +172 to +175
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Swagger 문서(@ApiResponses)에 따르면 닉네임의 형식이나 길이가 유효하지 않을 경우 400 Bad Request를 반환해야 합니다. 하지만 현재 isNicknameAvailable 메소드는 유효성 검사에 실패할 경우 false를 반환하여, 컨트롤러에서 항상 200 OK{ "available": false } 응답을 보내게 됩니다. 이는 API 명세와 구현 간의 불일치입니다.

유효성 검사에 실패했을 때 false를 반환하는 대신 CustomException을 발생시켜 전역 예외 처리기에서 적절한 400번대 에러 응답을 생성하도록 수정하는 것을 권장합니다. 이렇게 하면 API 명세를 준수하고 클라이언트에게 더 명확한 에러 원인을 전달할 수 있습니다.

더불어, 이 닉네임 유효성 검사 로직은 updateMyInfo 메소드에도 중복으로 존재합니다. 이번 기회에 중복되는 로직을 별도의 private 메소드로 추출하여 재사용성을 높이는 리팩토링을 고려해보시는 것도 좋겠습니다.

    @Transactional(readOnly = true)
    public boolean isNicknameAvailable(String nickname) {

        // 1. null / 공백 체크
        if (nickname == null || nickname.trim().isEmpty()) {
            throw new CustomException(UserErrorCode.INVALID_NICKNAME_FORMAT);
        }
        nickname = nickname.trim();

        // 2. 길이 체크 (사람 기준 2~10자)
        int length = nickname.codePointCount(0, nickname.length());
        if (length < 2 || length > 10) {
            throw new CustomException(UserErrorCode.INVALID_NICKNAME_LENGTH);
        }

        // 3. 형식 체크 (한글, 영문, 숫자만)
        if (!nickname.matches("^[가-힣a-zA-Z0-9]+$")) {
            throw new CustomException(UserErrorCode.INVALID_NICKNAME_FORMAT);
        }

        // 4. 중복 체크
        return !userRepository.existsByNickname(nickname);
    }
References
  1. Extract duplicate validation logic into a separate component (e.g., a validator class) to reduce code duplication and improve maintainability.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.example.RealMatch.user.application.util;

import org.springframework.stereotype.Component;

import com.example.RealMatch.global.exception.CustomException;
import com.example.RealMatch.user.domain.repository.UserRepository;
import com.example.RealMatch.user.presentation.code.UserErrorCode;

import lombok.RequiredArgsConstructor;
/**
* 닉네임 검증을 위한 공통 유틸리티 클래스
* - 형식, 길이, 중복 검증을 한 곳에서 처리
*/
@Component
@RequiredArgsConstructor
public class NicknameValidator {

private final UserRepository userRepository;

private static final int MIN_LENGTH = 2;
private static final int MAX_LENGTH = 10;
private static final String NICKNAME_PATTERN = "^[가-힣a-zA-Z0-9]+$";

/**
* 닉네임 사용 가능 여부 확인 (중복 체크만)
* - 형식/길이 검증 후 중복 여부 반환
* - 검증 실패 시 예외 발생
*
* @param nickname 검증할 닉네임
* @return 사용 가능하면 true, 중복이면 false
* @throws CustomException 형식/길이가 잘못된 경우
*/
public boolean isAvailable(String nickname) {
validateFormat(nickname);
return !userRepository.existsByNickname(nickname.trim());
}

/**
* 닉네임 검증 (형식, 길이, 중복 모두 체크)
* - 검증 실패 시 예외 발생
*
* @param nickname 검증할 닉네임
* @throws CustomException 검증 실패 시
*/
public void validate(String nickname) {
validateFormat(nickname);
validateDuplicate(nickname);
}

/**
* 닉네임 변경 시 검증 (기존 닉네임과 비교)
* - 기존 닉네임과 같으면 검증 통과
* - 다르면 형식, 길이, 중복 체크
*
* @param newNickname 새 닉네임
* @param currentNickname 현재 닉네임
* @throws CustomException 검증 실패 시
*/
public void validateForUpdate(String newNickname, String currentNickname) {
// 기존 닉네임과 동일하면 검증 통과
if (newNickname.equals(currentNickname)) {
return;
}

// 형식 검증
validateFormat(newNickname);

// 중복 검증
validateDuplicate(newNickname);
}

/**
* 닉네임 형식 및 길이 검증
*/
private void validateFormat(String nickname) {
// null 체크
if (nickname == null || nickname.trim().isEmpty()) {
throw new CustomException(UserErrorCode.INVALID_NICKNAME_FORMAT);
}

String trimmedNickname = nickname.trim();

// 길이 체크 (2~10자)
int length = trimmedNickname.codePointCount(0, trimmedNickname.length());
if (length < MIN_LENGTH || length > MAX_LENGTH) {
throw new CustomException(UserErrorCode.INVALID_NICKNAME_LENGTH);
}

// 형식 체크 (한글, 영문, 숫자만)
if (!trimmedNickname.matches(NICKNAME_PATTERN)) {
throw new CustomException(UserErrorCode.INVALID_NICKNAME_FORMAT);
}
}

/**
* 닉네임 중복 검증
*/
private void validateDuplicate(String nickname) {
if (userRepository.existsByNickname(nickname.trim())) {
throw new CustomException(UserErrorCode.DUPLICATE_NICKNAME);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.example.RealMatch.user.presentation.dto.response.MyPageResponseDto;
import com.example.RealMatch.user.presentation.dto.response.MyProfileCardResponseDto;
import com.example.RealMatch.user.presentation.dto.response.MyScrapResponseDto;
import com.example.RealMatch.user.presentation.dto.response.NicknameAvailableResponseDto;
import com.example.RealMatch.user.presentation.swagger.UserSwagger;

import io.swagger.v3.oas.annotations.tags.Tag;
Expand Down Expand Up @@ -107,4 +108,13 @@ public CustomResponse<Void> updateMyFeature(
userFeatureService.updateMyFeatures(userDetails.getUserId(), request);
return CustomResponse.ok(null);
}

@Override
@GetMapping("/nickname/available")
public CustomResponse<NicknameAvailableResponseDto> checkNicknameAvailable(
@RequestParam String nickname
) {
boolean available = userService.isNicknameAvailable(nickname);
return CustomResponse.ok(new NicknameAvailableResponseDto(available));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.example.RealMatch.user.presentation.dto.response;

public record NicknameAvailableResponseDto(boolean available) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.example.RealMatch.user.presentation.dto.response.MyPageResponseDto;
import com.example.RealMatch.user.presentation.dto.response.MyProfileCardResponseDto;
import com.example.RealMatch.user.presentation.dto.response.MyScrapResponseDto;
import com.example.RealMatch.user.presentation.dto.response.NicknameAvailableResponseDto;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand Down Expand Up @@ -115,4 +116,30 @@ CustomResponse<Void> updateMyFeature(
@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
@RequestBody MatchRequestDto request
);

@Operation(
summary = "닉네임 중복 체크 API",
description = """
회원가입 및 닉네임 변경 시 사용할 닉네임 중복 체크 API입니다.

user 테이블의 nickname 컬럼에서 닉네임이 있으면 "available": false 반환
없으면 "available": true 반환

닉네임 형식 및 길이 조건
- 형식: 한글, 영문, 숫자만 허용 (특수문자 및 공백 불가)
- 길이: 2자 이상 10자 이하 허용
"""
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "중복 체크 성공"),
@ApiResponse(responseCode = "400", description = "닉네임 형식 또는 길이 오류")
})
CustomResponse<NicknameAvailableResponseDto> checkNicknameAvailable(
@Parameter(
description = "중복 여부를 확인할 닉네임",
required = true,
example = "비비"
)
@RequestParam String nickname
);
}
Loading