diff --git a/src/main/java/com/dduru/gildongmu/auth/service/OauthAuthService.java b/src/main/java/com/dduru/gildongmu/auth/service/OauthAuthService.java index fdeb3217..5aa82fd4 100644 --- a/src/main/java/com/dduru/gildongmu/auth/service/OauthAuthService.java +++ b/src/main/java/com/dduru/gildongmu/auth/service/OauthAuthService.java @@ -11,6 +11,8 @@ import com.dduru.gildongmu.common.exception.BusinessException; import com.dduru.gildongmu.common.exception.ErrorCode; import com.dduru.gildongmu.common.jwt.JwtTokenProvider; +import com.dduru.gildongmu.profile.domain.Profile; +import com.dduru.gildongmu.profile.repository.ProfileRepository; import com.dduru.gildongmu.user.domain.User; import com.dduru.gildongmu.user.enums.OauthType; import com.dduru.gildongmu.user.enums.Role; @@ -30,6 +32,7 @@ public class OauthAuthService { private final OauthFactory oauthFactory; private final UserRepository userRepository; + private final ProfileRepository profileRepository; private final JwtTokenProvider jwtTokenProvider; private final RefreshTokenService refreshTokenService; @@ -145,22 +148,27 @@ private record UserCreationResult(User user, boolean isNewUser) { } private User createNewUser(OauthUserInfo oauthUserInfo) { - // 추가 정보는 회원가입 이후 별도 입력으로 변경 - // 닉네임은 온보딩에서 설정하므로 null로 초기화 - User newUser = User.builder() .email(oauthUserInfo.email()) .name(oauthUserInfo.name()) - .nickname(null) - .profileImage(oauthUserInfo.profileImage()) .oauthId(oauthUserInfo.oauthId()) .oauthType(oauthUserInfo.loginType()) .role(Role.USER) + .build(); + + User savedUser = userRepository.save(newUser); + + Profile profile = Profile.builder() + .user(savedUser) + .profileImage(null) + .nickname(null) .gender(null) .phoneNumber(null) .birthday(null) .build(); - return userRepository.save(newUser); + profileRepository.save(profile); + + return savedUser; } } diff --git a/src/main/java/com/dduru/gildongmu/comment/dto/CommentResponse.java b/src/main/java/com/dduru/gildongmu/comment/dto/CommentResponse.java index 20486c6f..1ec2383c 100644 --- a/src/main/java/com/dduru/gildongmu/comment/dto/CommentResponse.java +++ b/src/main/java/com/dduru/gildongmu/comment/dto/CommentResponse.java @@ -34,8 +34,8 @@ public static CommentResponse from(Comment comment) { authorProfileImage = null; } else { content = comment.getContent(); - author = comment.getUser().getNickname(); - authorProfileImage = comment.getUser().getProfileImage(); + author = comment.getUser().getProfile().getNickname(); + authorProfileImage = comment.getUser().getProfile().getProfileImage(); } return new CommentResponse( diff --git a/src/main/java/com/dduru/gildongmu/common/exception/ErrorCode.java b/src/main/java/com/dduru/gildongmu/common/exception/ErrorCode.java index b267e5bb..c9941d38 100644 --- a/src/main/java/com/dduru/gildongmu/common/exception/ErrorCode.java +++ b/src/main/java/com/dduru/gildongmu/common/exception/ErrorCode.java @@ -14,7 +14,7 @@ public enum ErrorCode { UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증되지 않은 사용자입니다."), DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 다른 소셜 계정으로 가입된 이메일입니다."), - // 닉네임 (NICKNAME) - 이전 '온보딩 (ONBD)' + // 닉네임 (NICKNAME) NICKNAME_NOT_BLANK(HttpStatus.BAD_REQUEST, "닉네임은 공백일 수 없습니다."), NICKNAME_INVALID_LENGTH(HttpStatus.BAD_REQUEST, "닉네임은 2자 이상 14자 이하로 입력해주세요."), NICKNAME_INVALID_CHARACTERS(HttpStatus.BAD_REQUEST, "닉네임은 한글, 영어, 숫자만 사용 가능합니다."), @@ -25,6 +25,7 @@ public enum ErrorCode { // 사용자 (USER) USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), + PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저의 프로필을 찾을 수 없습니다."), // 게시글 (POST) POST_NOT_FOUND(HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다."), diff --git a/src/main/java/com/dduru/gildongmu/common/jwt/JwtTokenProvider.java b/src/main/java/com/dduru/gildongmu/common/jwt/JwtTokenProvider.java index 9263e0cd..9971b6cd 100644 --- a/src/main/java/com/dduru/gildongmu/common/jwt/JwtTokenProvider.java +++ b/src/main/java/com/dduru/gildongmu/common/jwt/JwtTokenProvider.java @@ -122,6 +122,50 @@ public String createVerificationToken(Long userId, String phoneNumber) { .compact(); } + /** + * Verification Token 검증 및 전화번호 추출 + * + * @param token verification token + * @param expectedPhoneNumber 검증할 전화번호 + * @return 검증 성공 여부 + */ + public boolean validateVerificationToken(String token, String expectedPhoneNumber) { + try { + Claims claims = getClaims(token); + String tokenType = claims.get("type", String.class); + if (!"verification".equals(tokenType)) { + log.warn("Verification Token이 아닙니다 - {}", tokenType); + return false; + } + + String phoneNumber = claims.get("phone_number", String.class); + if (phoneNumber == null || !phoneNumber.equals(expectedPhoneNumber)) { + log.warn("전화번호가 일치하지 않습니다. 토큰: {}, 요청: {}", phoneNumber, expectedPhoneNumber); + return false; + } + + return true; + } catch (ExpiredJwtException e) { + log.warn("Verification Token이 만료되었습니다"); + return false; + } catch (UnsupportedJwtException e) { + log.warn("JWT 형식이 틀렸습니다"); + return false; + } catch (MalformedJwtException e) { + log.warn("JWT 구조가 잘못되었습니다"); + return false; + } catch (SignatureException e) { + log.warn("JWT 서명 검증 실패하였습니다"); + return false; + } catch (IllegalArgumentException e) { + log.warn("부적절한 값이 들어왔습니다"); + return false; + } catch (JwtException e) { + log.warn("Verification Token이 유효하지 않습니다: {}", e.getMessage()); + return false; + } + } + private Claims getClaims(String token) { return Jwts.parser() .setSigningKey(jwtSecret) diff --git a/src/main/java/com/dduru/gildongmu/post/domain/Post.java b/src/main/java/com/dduru/gildongmu/post/domain/Post.java index d1285131..8db2c7e1 100644 --- a/src/main/java/com/dduru/gildongmu/post/domain/Post.java +++ b/src/main/java/com/dduru/gildongmu/post/domain/Post.java @@ -4,10 +4,15 @@ import com.dduru.gildongmu.destination.domain.Destination; import com.dduru.gildongmu.participation.domain.Participation; import com.dduru.gildongmu.post.enums.PostStatus; -import com.dduru.gildongmu.post.exception.*; +import com.dduru.gildongmu.post.exception.InvalidPostStatusException; +import com.dduru.gildongmu.post.exception.InvalidRecruitCapacityException; +import com.dduru.gildongmu.post.exception.RecruitCountBelowZeroException; +import com.dduru.gildongmu.post.exception.RecruitCountExceedCapacityException; +import com.dduru.gildongmu.post.exception.TravelAlreadyEndedException; +import com.dduru.gildongmu.post.exception.TravelAlreadyStartedException; +import com.dduru.gildongmu.profile.domain.enums.AgeRange; +import com.dduru.gildongmu.profile.domain.enums.Gender; import com.dduru.gildongmu.user.domain.User; -import com.dduru.gildongmu.user.enums.AgeRange; -import com.dduru.gildongmu.user.enums.Gender; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/com/dduru/gildongmu/post/repository/PostRepositoryImpl.java b/src/main/java/com/dduru/gildongmu/post/repository/PostRepositoryImpl.java index a36d6cc8..744b57c1 100644 --- a/src/main/java/com/dduru/gildongmu/post/repository/PostRepositoryImpl.java +++ b/src/main/java/com/dduru/gildongmu/post/repository/PostRepositoryImpl.java @@ -1,11 +1,11 @@ package com.dduru.gildongmu.post.repository; -import com.dduru.gildongmu.user.enums.AgeRange; -import com.dduru.gildongmu.user.enums.Gender; import com.dduru.gildongmu.post.domain.Post; import com.dduru.gildongmu.post.dto.PostListRequest; import com.dduru.gildongmu.post.enums.PostStatus; -import com.dduru.gildongmu.user.exception.InvalidGenderException; +import com.dduru.gildongmu.profile.domain.enums.AgeRange; +import com.dduru.gildongmu.profile.domain.enums.Gender; +import com.dduru.gildongmu.profile.exception.InvalidGenderException; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/dduru/gildongmu/post/service/PostService.java b/src/main/java/com/dduru/gildongmu/post/service/PostService.java index d20bcdbb..9cd2bc81 100644 --- a/src/main/java/com/dduru/gildongmu/post/service/PostService.java +++ b/src/main/java/com/dduru/gildongmu/post/service/PostService.java @@ -14,9 +14,9 @@ import com.dduru.gildongmu.post.exception.InvalidPostDateException; import com.dduru.gildongmu.post.exception.PostAccessDeniedException; import com.dduru.gildongmu.post.repository.PostRepository; +import com.dduru.gildongmu.profile.domain.enums.AgeRange; +import com.dduru.gildongmu.profile.domain.enums.Gender; import com.dduru.gildongmu.user.domain.User; -import com.dduru.gildongmu.user.enums.AgeRange; -import com.dduru.gildongmu.user.enums.Gender; import com.dduru.gildongmu.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/dduru/gildongmu/profile/controller/ProfileApiDocs.java b/src/main/java/com/dduru/gildongmu/profile/controller/ProfileApiDocs.java new file mode 100644 index 00000000..cef11f55 --- /dev/null +++ b/src/main/java/com/dduru/gildongmu/profile/controller/ProfileApiDocs.java @@ -0,0 +1,347 @@ +package com.dduru.gildongmu.profile.controller; + +import com.dduru.gildongmu.common.dto.ApiResult; +import com.dduru.gildongmu.common.exception.ErrorResponse; +import com.dduru.gildongmu.profile.dto.NicknameRandomResponse; +import com.dduru.gildongmu.profile.dto.NicknameUpdateRequest; +import com.dduru.gildongmu.profile.dto.NicknameValidateResponse; +import com.dduru.gildongmu.profile.dto.ProfileSetupRequest; +import com.dduru.gildongmu.profile.validator.ValidNickname; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; + +@Tag(name = "Profiles", description = "사용자 프로필 관리 API") +public interface ProfileApiDocs { + + @Operation(summary = "닉네임 수정", description = "사용자의 닉네임을 수정합니다.") + @ApiResponses({ + @ApiResponse( + responseCode = "204", + description = "닉네임 수정 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ApiResult.class)) + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 요청 - 닉네임 형식이 올바르지 않거나 검증 규칙을 위반함", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "InvalidNickname", + value = """ + { + "status": 400, + "data": { + "errorCode": "NICKNAME_INVALID_LENGTH", + "field": "nickname", + "message": "닉네임은 2자 이상 14자 이하로 입력해주세요." + } + } + """ + ) + ) + ), + @ApiResponse( + responseCode = "401", + description = "인증 필요 - 유효한 인증 토큰이 필요함", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "Unauthorized", + value = """ + { + "status": 401, + "data": { + "errorCode": "UNAUTHORIZED", + "field": null, + "message": "인증되지 않은 사용자입니다." + } + } + """ + ) + ) + ), + @ApiResponse( + responseCode = "404", + description = "프로필을 찾을 수 없음", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "ProfileNotFound", + value = """ + { + "status": 404, + "data": { + "errorCode": "PROFILE_NOT_FOUND", + "field": null, + "message": "해당 유저의 프로필을 찾을 수 없습니다." + } + } + """ + ) + ) + ), + @ApiResponse( + responseCode = "409", + description = "닉네임 중복 - 이미 사용 중인 닉네임임", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "NicknameAlreadyTaken", + value = """ + { + "status": 409, + "data": { + "errorCode": "NICKNAME_ALREADY_TAKEN", + "field": null, + "message": "이미 사용 중인 닉네임입니다." + } + } + """ + ) + ) + ) + }) + ResponseEntity> updateNickname( + @Parameter(hidden = true) Long id, + @Valid NicknameUpdateRequest request + ); + + @Operation(summary = "닉네임 유효성 확인", description = "닉네임의 사용 가능 여부를 확인합니다. 닉네임 형식 검증 및 중복 여부를 체크합니다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "닉네임 유효성 확인 성공 - 사용 가능한 닉네임임", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = NicknameValidateResponse.class)) + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 요청 - 닉네임 형식이 올바르지 않거나 검증 규칙을 위반함 (예: 길이, 특수문자, 금지 단어 등)", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "InvalidNickname", + value = """ + { + "status": 400, + "data": { + "errorCode": "NICKNAME_INVALID_LENGTH", + "field": "nickname", + "message": "닉네임은 2자 이상 14자 이하로 입력해주세요." + } + } + """ + ) + ) + ), + @ApiResponse( + responseCode = "409", + description = "닉네임 중복 - 이미 사용 중인 닉네임임", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "NicknameAlreadyTaken", + value = """ + { + "status": 409, + "data": { + "errorCode": "NICKNAME_ALREADY_TAKEN", + "field": null, + "message": "이미 사용 중인 닉네임입니다." + } + } + """ + ) + ) + ) + }) + ResponseEntity> checkNickname( + @Parameter( + description = "유효성 체크할 닉네임", + example = "길동무" + ) + @ValidNickname String nickname + ); + + @Operation(summary = "랜덤 닉네임 생성", description = "랜덤으로 사용 가능한 닉네임을 생성합니다. 형용사와 명사 조합에 랜덤 숫자를 추가하여 고유성을 보장합니다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "랜덤 닉네임 생성 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = NicknameRandomResponse.class)) + ), + @ApiResponse( + responseCode = "401", + description = "인증 필요 - 유효한 인증 토큰이 필요함", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "Unauthorized", + value = """ + { + "status": 401, + "data": { + "errorCode": "UNAUTHORIZED", + "field": null, + "message": "인증되지 않은 사용자입니다." + } + } + """ + ) + ) + ) + }) + ResponseEntity> generateRandomNickname(); + + @Operation( + summary = "프로필 초기 설정", + description = "온보딩 과정에서 사용자의 프로필 정보를 초기 설정합니다. 닉네임, 성별, 전화번호, 생년월일을 저장하며, 비관적 잠금을 사용하여 닉네임 중복을 방지합니다." + ) + @ApiResponses({ + @ApiResponse( + responseCode = "204", + description = "프로필 초기 설정 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ApiResult.class)) + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 요청 - 입력 값 형식이 올바르지 않음 (예: 닉네임 형식 오류, 생년월일 형식 오류, 성별 형식 오류 등)", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "InvalidInput", + value = """ + { + "status": 400, + "data": { + "errorCode": "INVALID_INPUT_VALUE", + "field": "birthday", + "message": "생년월일 형식이 올바르지 않습니다. (yyyy-MM-dd 형식)" + } + } + """ + ) + ) + ), + @ApiResponse( + responseCode = "401", + description = "인증 필요 또는 인증 토큰 오류 - 유효한 인증 토큰이 필요하거나 인증 토큰이 유효하지 않음/만료됨", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "Unauthorized", + value = """ + { + "status": 401, + "data": { + "errorCode": "UNAUTHORIZED", + "field": null, + "message": "인증되지 않은 사용자입니다." + } + } + """ + ), + @ExampleObject( + name = "InvalidToken", + value = """ + { + "status": 401, + "data": { + "errorCode": "INVALID_TOKEN", + "field": null, + "message": "유효하지 않거나 만료된 인증 토큰입니다." + } + } + """ + ) + } + ) + ), + @ApiResponse( + responseCode = "404", + description = "프로필을 찾을 수 없음 - 해당 사용자의 프로필이 존재하지 않음", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "ProfileNotFound", + value = """ + { + "status": 404, + "data": { + "errorCode": "PROFILE_NOT_FOUND", + "field": null, + "message": "해당 유저의 프로필을 찾을 수 없습니다." + } + } + """ + ) + ) + ), + @ApiResponse( + responseCode = "409", + description = "충돌 발생 - 닉네임 중복(NICKNAME_ALREADY_TAKEN) 또는 전화번호 중복(DUPLICATE_PHONE_NUMBER)", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "NicknameAlreadyTaken", + value = """ + { + "status": 409, + "data": { + "errorCode": "NICKNAME_ALREADY_TAKEN", + "field": null, + "message": "이미 사용 중인 닉네임입니다." + } + } + """ + ), + @ExampleObject( + name = "DuplicatePhoneNumber", + value = """ + { + "status": 409, + "data": { + "errorCode": "DUPLICATE_PHONE_NUMBER", + "field": null, + "message": "이미 가입된 전화번호입니다." + } + } + """ + ) + } + ) + ) + }) + ResponseEntity> setupInitialProfile( + @Parameter(hidden = true) Long userId, + @Parameter( + description = "프로필 초기 설정 요청 정보", + required = true + ) + @Valid ProfileSetupRequest request + ); +} diff --git a/src/main/java/com/dduru/gildongmu/user/controller/UserController.java b/src/main/java/com/dduru/gildongmu/profile/controller/ProfileController.java similarity index 53% rename from src/main/java/com/dduru/gildongmu/user/controller/UserController.java rename to src/main/java/com/dduru/gildongmu/profile/controller/ProfileController.java index 1ffe97a5..2bada98e 100644 --- a/src/main/java/com/dduru/gildongmu/user/controller/UserController.java +++ b/src/main/java/com/dduru/gildongmu/profile/controller/ProfileController.java @@ -1,12 +1,13 @@ -package com.dduru.gildongmu.user.controller; +package com.dduru.gildongmu.profile.controller; import com.dduru.gildongmu.common.annotation.CurrentUser; import com.dduru.gildongmu.common.dto.ApiResult; -import com.dduru.gildongmu.user.dto.NicknameRandomResponse; -import com.dduru.gildongmu.user.dto.UserCheckNicknameResponse; -import com.dduru.gildongmu.user.dto.UserUpdateNicknameRequest; -import com.dduru.gildongmu.user.service.UserService; -import com.dduru.gildongmu.user.validator.ValidNickname; +import com.dduru.gildongmu.profile.dto.NicknameRandomResponse; +import com.dduru.gildongmu.profile.dto.NicknameValidateResponse; +import com.dduru.gildongmu.profile.dto.NicknameUpdateRequest; +import com.dduru.gildongmu.profile.dto.ProfileSetupRequest; +import com.dduru.gildongmu.profile.service.ProfileService; +import com.dduru.gildongmu.profile.validator.ValidNickname; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -23,33 +24,43 @@ @RequestMapping("/api/v1") @RequiredArgsConstructor @RestController -public class UserController implements UserApiDocs { +public class ProfileController implements ProfileApiDocs { - private final UserService userService; + private final ProfileService profileService; @Override @PutMapping("/users/nickname") public ResponseEntity> updateNickname( @CurrentUser Long id, - @Valid @RequestBody UserUpdateNicknameRequest request + @Valid @RequestBody NicknameUpdateRequest request ) { - userService.updateNickname(id, request); + profileService.updateNickname(id, request); return ResponseEntity.status(HttpStatus.NO_CONTENT).body(ApiResult.noContent()); } @Override @GetMapping("/nicknames/{nickname}/availability") - public ResponseEntity> checkNickname( + public ResponseEntity> checkNickname( @PathVariable @ValidNickname String nickname ) { - UserCheckNicknameResponse response = userService.checkNickname(nickname); + NicknameValidateResponse response = profileService.checkNickname(nickname); return ResponseEntity.ok(ApiResult.ok(response)); } @Override @GetMapping("/nicknames/random") public ResponseEntity> generateRandomNickname() { - NicknameRandomResponse response = userService.generateRandomNickname(); + NicknameRandomResponse response = profileService.generateRandomNickname(); return ResponseEntity.ok(ApiResult.ok(response)); } + + @Override + @PutMapping("/me/profile") + public ResponseEntity> setupInitialProfile( + @CurrentUser Long userId, + @RequestBody @Valid ProfileSetupRequest request + ) { + profileService.setupInitialProfile(userId, request); + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(ApiResult.noContent()); + } } diff --git a/src/main/java/com/dduru/gildongmu/profile/domain/Profile.java b/src/main/java/com/dduru/gildongmu/profile/domain/Profile.java new file mode 100644 index 00000000..99c571d6 --- /dev/null +++ b/src/main/java/com/dduru/gildongmu/profile/domain/Profile.java @@ -0,0 +1,75 @@ +package com.dduru.gildongmu.profile.domain; + +import com.dduru.gildongmu.common.entity.BaseTimeEntity; +import com.dduru.gildongmu.profile.domain.enums.Gender; +import com.dduru.gildongmu.user.domain.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Entity +@Table(name = "profiles") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Profile extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false, unique = true) + private User user; + + @Column(length = 14, unique = true) + private String nickname; + + @Enumerated(EnumType.STRING) + private Gender gender; + + @Column(name = "phone_number", length = 20) + private String phoneNumber; + + @Column(name = "birthday") + private LocalDate birthday; + + @Column(name = "profile_image", length = 500) + private String profileImage; + + @Column(name = "self_introduction", length = 60) + private String selfIntroduction; + + @Builder + public Profile(User user, String nickname, Gender gender, String phoneNumber, LocalDate birthday, String profileImage, String selfIntroduction) { + this.user = user; + this.nickname = nickname; + this.gender = gender; + this.phoneNumber = phoneNumber; + this.birthday = birthday; + this.profileImage = profileImage; + this.selfIntroduction = selfIntroduction; + } + + public void updateNickname(String nickname) { + this.nickname = nickname; + } + + public void setupInitialProfile(String nickname, Gender gender, String phoneNumber, LocalDate birthday) { + if (nickname != null) { + this.nickname = nickname; + } + if (gender != null) { + this.gender = gender; + } + if (phoneNumber != null) { + this.phoneNumber = phoneNumber; + } + if (birthday != null) { + this.birthday = birthday; + } + } +} diff --git a/src/main/java/com/dduru/gildongmu/user/enums/AgeRange.java b/src/main/java/com/dduru/gildongmu/profile/domain/enums/AgeRange.java similarity index 93% rename from src/main/java/com/dduru/gildongmu/user/enums/AgeRange.java rename to src/main/java/com/dduru/gildongmu/profile/domain/enums/AgeRange.java index 761267e8..950497e0 100644 --- a/src/main/java/com/dduru/gildongmu/user/enums/AgeRange.java +++ b/src/main/java/com/dduru/gildongmu/profile/domain/enums/AgeRange.java @@ -1,4 +1,4 @@ -package com.dduru.gildongmu.user.enums; +package com.dduru.gildongmu.profile.domain.enums; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/dduru/gildongmu/user/enums/Gender.java b/src/main/java/com/dduru/gildongmu/profile/domain/enums/Gender.java similarity index 83% rename from src/main/java/com/dduru/gildongmu/user/enums/Gender.java rename to src/main/java/com/dduru/gildongmu/profile/domain/enums/Gender.java index 7872ec88..9cb536bc 100644 --- a/src/main/java/com/dduru/gildongmu/user/enums/Gender.java +++ b/src/main/java/com/dduru/gildongmu/profile/domain/enums/Gender.java @@ -1,6 +1,6 @@ -package com.dduru.gildongmu.user.enums; +package com.dduru.gildongmu.profile.domain.enums; -import com.dduru.gildongmu.user.exception.InvalidGenderException; +import com.dduru.gildongmu.profile.exception.InvalidGenderException; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/dduru/gildongmu/user/dto/NicknameRandomResponse.java b/src/main/java/com/dduru/gildongmu/profile/dto/NicknameRandomResponse.java similarity index 87% rename from src/main/java/com/dduru/gildongmu/user/dto/NicknameRandomResponse.java rename to src/main/java/com/dduru/gildongmu/profile/dto/NicknameRandomResponse.java index 7701e88f..4891b8fa 100644 --- a/src/main/java/com/dduru/gildongmu/user/dto/NicknameRandomResponse.java +++ b/src/main/java/com/dduru/gildongmu/profile/dto/NicknameRandomResponse.java @@ -1,4 +1,4 @@ -package com.dduru.gildongmu.user.dto; +package com.dduru.gildongmu.profile.dto; import lombok.Builder; diff --git a/src/main/java/com/dduru/gildongmu/user/dto/UserUpdateNicknameRequest.java b/src/main/java/com/dduru/gildongmu/profile/dto/NicknameUpdateRequest.java similarity index 66% rename from src/main/java/com/dduru/gildongmu/user/dto/UserUpdateNicknameRequest.java rename to src/main/java/com/dduru/gildongmu/profile/dto/NicknameUpdateRequest.java index da212368..21025f08 100644 --- a/src/main/java/com/dduru/gildongmu/user/dto/UserUpdateNicknameRequest.java +++ b/src/main/java/com/dduru/gildongmu/profile/dto/NicknameUpdateRequest.java @@ -1,9 +1,9 @@ -package com.dduru.gildongmu.user.dto; +package com.dduru.gildongmu.profile.dto; -import com.dduru.gildongmu.user.validator.ValidNickname; +import com.dduru.gildongmu.profile.validator.ValidNickname; import io.swagger.v3.oas.annotations.media.Schema; -public record UserUpdateNicknameRequest( +public record NicknameUpdateRequest( @Schema( description = "닉네임", example = "길동무", diff --git a/src/main/java/com/dduru/gildongmu/profile/dto/NicknameValidateResponse.java b/src/main/java/com/dduru/gildongmu/profile/dto/NicknameValidateResponse.java new file mode 100644 index 00000000..a9e63b11 --- /dev/null +++ b/src/main/java/com/dduru/gildongmu/profile/dto/NicknameValidateResponse.java @@ -0,0 +1,9 @@ +package com.dduru.gildongmu.profile.dto; + +import lombok.Builder; + +@Builder +public record NicknameValidateResponse( + String sanitizedNickname +) { +} diff --git a/src/main/java/com/dduru/gildongmu/profile/dto/ProfileSetupRequest.java b/src/main/java/com/dduru/gildongmu/profile/dto/ProfileSetupRequest.java new file mode 100644 index 00000000..b3c5a26f --- /dev/null +++ b/src/main/java/com/dduru/gildongmu/profile/dto/ProfileSetupRequest.java @@ -0,0 +1,34 @@ +package com.dduru.gildongmu.profile.dto; + +import com.dduru.gildongmu.profile.validator.ValidNickname; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + + +public record ProfileSetupRequest( + @Schema(description = "닉네임", example = "용감한여행자1234") + @ValidNickname + @NotBlank + String nickname, + + @Schema(description = "성별", example = "M", allowableValues = {"M", "F"}) + @NotBlank + @Pattern(regexp = "^(M|F)$", message = "성별 형식이 올바르지 않습니다.") + String gender, + + @Schema(description = "전화번호", example = "01012345678") + @NotBlank + @Pattern(regexp = "^010\\d{8}$", message = "전화번호는 010으로 시작하는 11자리 숫자여야 합니다.") + String phoneNumber, + + @Schema(description = "생년월일", example = "2000-01-01") + @NotBlank + @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "생년월일 형식이 올바르지 않습니다. (예: 2000-01-01)") + String birthday, + + @Schema(description = "전화번호 인증 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + @NotBlank + String verificationToken +) { +} diff --git a/src/main/java/com/dduru/gildongmu/user/exception/InvalidGenderException.java b/src/main/java/com/dduru/gildongmu/profile/exception/InvalidGenderException.java similarity index 92% rename from src/main/java/com/dduru/gildongmu/user/exception/InvalidGenderException.java rename to src/main/java/com/dduru/gildongmu/profile/exception/InvalidGenderException.java index 0258cc0c..1e73b86a 100644 --- a/src/main/java/com/dduru/gildongmu/user/exception/InvalidGenderException.java +++ b/src/main/java/com/dduru/gildongmu/profile/exception/InvalidGenderException.java @@ -1,4 +1,4 @@ -package com.dduru.gildongmu.user.exception; +package com.dduru.gildongmu.profile.exception; import com.dduru.gildongmu.common.exception.BusinessException; import com.dduru.gildongmu.common.exception.ErrorCode; diff --git a/src/main/java/com/dduru/gildongmu/profile/repository/ProfileRepository.java b/src/main/java/com/dduru/gildongmu/profile/repository/ProfileRepository.java new file mode 100644 index 00000000..3938ca10 --- /dev/null +++ b/src/main/java/com/dduru/gildongmu/profile/repository/ProfileRepository.java @@ -0,0 +1,28 @@ +package com.dduru.gildongmu.profile.repository; + +import com.dduru.gildongmu.profile.domain.Profile; +import com.dduru.gildongmu.user.domain.User; +import jakarta.persistence.LockModeType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ProfileRepository extends JpaRepository { + Optional findByUser(User user); + + Optional findByUser_Id(Long userId); + + boolean existsByNickname(String nickname); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT COUNT(p) > 0 FROM Profile p WHERE p.nickname = :nickname") + boolean existsByNicknameWithLock(@Param("nickname") String nickname); + + boolean existsByPhoneNumber(String phoneNumber); +} + diff --git a/src/main/java/com/dduru/gildongmu/profile/service/ProfileService.java b/src/main/java/com/dduru/gildongmu/profile/service/ProfileService.java new file mode 100644 index 00000000..d71f83fe --- /dev/null +++ b/src/main/java/com/dduru/gildongmu/profile/service/ProfileService.java @@ -0,0 +1,152 @@ +package com.dduru.gildongmu.profile.service; + +import com.dduru.gildongmu.common.exception.BusinessException; +import com.dduru.gildongmu.common.exception.ErrorCode; +import com.dduru.gildongmu.common.jwt.JwtTokenProvider; +import com.dduru.gildongmu.profile.domain.Profile; +import com.dduru.gildongmu.profile.domain.enums.Gender; +import com.dduru.gildongmu.profile.dto.NicknameRandomResponse; +import com.dduru.gildongmu.profile.dto.NicknameUpdateRequest; +import com.dduru.gildongmu.profile.dto.NicknameValidateResponse; +import com.dduru.gildongmu.profile.dto.ProfileSetupRequest; +import com.dduru.gildongmu.profile.repository.ProfileRepository; +import com.dduru.gildongmu.profile.utils.NicknameGenerator; +import com.dduru.gildongmu.user.domain.User; +import com.dduru.gildongmu.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +@Slf4j +@RequiredArgsConstructor +@Service +public class ProfileService { + + private static final int NICKNAME_MAX_RETRY_ATTEMPTS = 10; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final NicknameGenerator nicknameGenerator; + private final JwtTokenProvider jwtTokenProvider; + + private final ProfileRepository profileRepository; + private final UserRepository userRepository; + + @Transactional + public void updateNickname(Long userId, NicknameUpdateRequest request) { + User user = userRepository.getByIdOrThrow(userId); + Profile profile = getProfileByUserId(user); + checkDuplicateNicknameWithLock(request.nickname()); + + profile.updateNickname(request.nickname()); + profileRepository.save(profile); + } + + @Transactional(readOnly = true) + public NicknameValidateResponse checkNickname(String nickname) { + checkDuplicateNickname(nickname); + + return NicknameValidateResponse.builder() + .sanitizedNickname(nickname) + .build(); + } + + /** + * 중복되지 않는 유니크한 닉네임을 생성합니다. + * 형용사 + 명사 조합에 랜덤 숫자를 추가하여 고유성을 보장합니다. + * DB에 중복이 있으면 새로운 랜덤 숫자로 재시도합니다. + * + * @return 유니크한 닉네임 (예: "용감한여행자1234") + */ + @Transactional(readOnly = true) + public NicknameRandomResponse generateRandomNickname() { + + for (int attempt = 0; attempt < NICKNAME_MAX_RETRY_ATTEMPTS; attempt++) { + String baseNickname = nicknameGenerator.generateBaseNickname(); + int randomNumber = nicknameGenerator.generateRandomNumber(); + String nickname = baseNickname + randomNumber; + + if (!profileRepository.existsByNickname(nickname)) { + log.info("랜덤 닉네임 생성: {}", nickname); + return NicknameRandomResponse.of(nickname); + } + + log.debug("해당 닉네임이 이미 존재합니다: {}, 새로운 숫자를 부여하겠습니다.", nickname); + } + + String fallbackNickname = "뚜비" + (System.currentTimeMillis() % 10000); + log.warn("유니크한 닉네임 생성에 {}회 실패하여 대체 닉네임 사용: {}", NICKNAME_MAX_RETRY_ATTEMPTS, fallbackNickname); + return NicknameRandomResponse.of(fallbackNickname); + } + + @Transactional + public void setupInitialProfile(Long userId, ProfileSetupRequest request) { + User user = userRepository.getByIdOrThrow(userId); + Profile profile = getProfileByUserId(user); + + // 비관적 잠금을 사용하여 닉네임 중복 체크 (race condition 방지) + checkDuplicateNicknameWithLock(request.nickname()); + + validateVerificationToken(request.verificationToken(), request.phoneNumber()); + + if (profileRepository.existsByPhoneNumber(request.phoneNumber())) { + throw new BusinessException(ErrorCode.DUPLICATE_PHONE_NUMBER); + } + + LocalDate birthday = parseBirthDate(request.birthday()); + + profile.setupInitialProfile( + request.nickname(), + Gender.valueOf(request.gender()), + request.phoneNumber(), + birthday + ); + + profileRepository.save(profile); + log.info("프로필 초기 설정 완료: userId={}, nickname={}", userId, request.nickname()); + } + + private LocalDate parseBirthDate(String birthDateString) { + if (birthDateString == null || birthDateString.trim().isEmpty()) { + return null; + } + + try { + return LocalDate.parse(birthDateString.trim(), DATE_FORMATTER); + } catch (DateTimeParseException e) { + log.warn("생년월일 파싱 실패: {}", birthDateString, e); + throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "생년월일 형식이 올바르지 않습니다. (yyyy-MM-dd 형식)"); + } + } + + private void validateVerificationToken(String verificationToken, String phoneNumber) { + if (verificationToken == null || verificationToken.trim().isEmpty()) { + throw new BusinessException(ErrorCode.INVALID_TOKEN, "인증 토큰이 필요합니다."); + } + + if (!jwtTokenProvider.validateVerificationToken(verificationToken, phoneNumber)) { + throw new BusinessException(ErrorCode.INVALID_TOKEN, "유효하지 않거나 만료된 인증 토큰입니다."); + } + } + + private Profile getProfileByUserId(User user) { + return profileRepository.findByUser(user) + .orElseThrow(() -> new BusinessException(ErrorCode.PROFILE_NOT_FOUND)); + } + + private void checkDuplicateNickname(String nickname) { + if (profileRepository.existsByNickname(nickname)) { + throw new BusinessException(ErrorCode.NICKNAME_ALREADY_TAKEN); + } + } + + private void checkDuplicateNicknameWithLock(String nickname) { + if (profileRepository.existsByNicknameWithLock(nickname)) { + throw new BusinessException(ErrorCode.NICKNAME_ALREADY_TAKEN); + } + } +} diff --git a/src/main/java/com/dduru/gildongmu/user/utils/NicknameAdjectiveProvider.java b/src/main/java/com/dduru/gildongmu/profile/utils/NicknameAdjectiveProvider.java similarity index 97% rename from src/main/java/com/dduru/gildongmu/user/utils/NicknameAdjectiveProvider.java rename to src/main/java/com/dduru/gildongmu/profile/utils/NicknameAdjectiveProvider.java index 151cf559..5862af7d 100644 --- a/src/main/java/com/dduru/gildongmu/user/utils/NicknameAdjectiveProvider.java +++ b/src/main/java/com/dduru/gildongmu/profile/utils/NicknameAdjectiveProvider.java @@ -1,4 +1,4 @@ -package com.dduru.gildongmu.user.utils; +package com.dduru.gildongmu.profile.utils; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/dduru/gildongmu/user/utils/NicknameGenerator.java b/src/main/java/com/dduru/gildongmu/profile/utils/NicknameGenerator.java similarity index 93% rename from src/main/java/com/dduru/gildongmu/user/utils/NicknameGenerator.java rename to src/main/java/com/dduru/gildongmu/profile/utils/NicknameGenerator.java index f2efdb6c..2b957388 100644 --- a/src/main/java/com/dduru/gildongmu/user/utils/NicknameGenerator.java +++ b/src/main/java/com/dduru/gildongmu/profile/utils/NicknameGenerator.java @@ -1,4 +1,4 @@ -package com.dduru.gildongmu.user.utils; +package com.dduru.gildongmu.profile.utils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,6 +36,6 @@ public int generateRandomNumber() { public String generateBaseNickname() { String adjective = nicknameAdjectiveProvider.getRandomAdjective(); String noun = nicknameNounProvider.getRandomNoun(); - return adjective + " " + noun; + return adjective + noun; } } diff --git a/src/main/java/com/dduru/gildongmu/user/utils/NicknameNounProvider.java b/src/main/java/com/dduru/gildongmu/profile/utils/NicknameNounProvider.java similarity index 96% rename from src/main/java/com/dduru/gildongmu/user/utils/NicknameNounProvider.java rename to src/main/java/com/dduru/gildongmu/profile/utils/NicknameNounProvider.java index 6b4ae10d..0146b815 100644 --- a/src/main/java/com/dduru/gildongmu/user/utils/NicknameNounProvider.java +++ b/src/main/java/com/dduru/gildongmu/profile/utils/NicknameNounProvider.java @@ -1,4 +1,4 @@ -package com.dduru.gildongmu.user.utils; +package com.dduru.gildongmu.profile.utils; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/dduru/gildongmu/user/validator/NicknameConstraintValidator.java b/src/main/java/com/dduru/gildongmu/profile/validator/NicknameConstraintValidator.java similarity index 96% rename from src/main/java/com/dduru/gildongmu/user/validator/NicknameConstraintValidator.java rename to src/main/java/com/dduru/gildongmu/profile/validator/NicknameConstraintValidator.java index 2cfe88dc..63a1d14d 100644 --- a/src/main/java/com/dduru/gildongmu/user/validator/NicknameConstraintValidator.java +++ b/src/main/java/com/dduru/gildongmu/profile/validator/NicknameConstraintValidator.java @@ -1,4 +1,4 @@ -package com.dduru.gildongmu.user.validator; +package com.dduru.gildongmu.profile.validator; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; diff --git a/src/main/java/com/dduru/gildongmu/user/validator/ValidNickname.java b/src/main/java/com/dduru/gildongmu/profile/validator/ValidNickname.java similarity index 96% rename from src/main/java/com/dduru/gildongmu/user/validator/ValidNickname.java rename to src/main/java/com/dduru/gildongmu/profile/validator/ValidNickname.java index 4931233c..b0e2ee50 100644 --- a/src/main/java/com/dduru/gildongmu/user/validator/ValidNickname.java +++ b/src/main/java/com/dduru/gildongmu/profile/validator/ValidNickname.java @@ -1,4 +1,4 @@ -package com.dduru.gildongmu.user.validator; +package com.dduru.gildongmu.profile.validator; import jakarta.validation.Constraint; import jakarta.validation.Payload; diff --git a/src/main/java/com/dduru/gildongmu/user/controller/UserApiDocs.java b/src/main/java/com/dduru/gildongmu/user/controller/UserApiDocs.java deleted file mode 100644 index 556aaa11..00000000 --- a/src/main/java/com/dduru/gildongmu/user/controller/UserApiDocs.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.dduru.gildongmu.user.controller; - -import com.dduru.gildongmu.common.dto.ApiResult; -import com.dduru.gildongmu.user.dto.NicknameRandomResponse; -import com.dduru.gildongmu.user.dto.UserCheckNicknameResponse; -import com.dduru.gildongmu.user.dto.UserUpdateNicknameRequest; -import com.dduru.gildongmu.user.validator.ValidNickname; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.ResponseEntity; - -@Tag(name = "Users", description = "사용자 관리 API") -public interface UserApiDocs { - - @Operation(summary = "닉네임 수정", description = "사용자의 닉네임을 수정합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "수정 성공"), - @ApiResponse(responseCode = "400", description = "잘못된 요청") - }) - ResponseEntity> updateNickname( - @Parameter(hidden = true) Long id, - @Valid UserUpdateNicknameRequest request - ); - - @Operation(summary = "닉네임 유효성 확인", description = "닉네임의 사용 가능 여부를 확인합니다.") - @ApiResponse(responseCode = "200", description = "유효성 확인 성공") - ResponseEntity> checkNickname( - @Parameter( - description = "유효성 체크할 닉네임", - example = "길동무" - ) - @ValidNickname String nickname - ); - - @Operation(summary = "랜덤 닉네임 생성", description = "랜덤으로 닉네임을 생성합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "랜덤 닉네임 생성 성공"), - @ApiResponse(responseCode = "401", description = "인증 필요") - }) - ResponseEntity> generateRandomNickname(); -} diff --git a/src/main/java/com/dduru/gildongmu/user/domain/User.java b/src/main/java/com/dduru/gildongmu/user/domain/User.java index 23fae491..04c5c643 100644 --- a/src/main/java/com/dduru/gildongmu/user/domain/User.java +++ b/src/main/java/com/dduru/gildongmu/user/domain/User.java @@ -1,17 +1,15 @@ package com.dduru.gildongmu.user.domain; -import com.dduru.gildongmu.user.enums.Gender; +import com.dduru.gildongmu.common.entity.BaseTimeEntity; +import com.dduru.gildongmu.profile.domain.Profile; import com.dduru.gildongmu.user.enums.OauthType; import com.dduru.gildongmu.user.enums.Role; -import com.dduru.gildongmu.common.entity.BaseTimeEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDate; - @Entity @Table(name = "users") @Getter @@ -27,12 +25,6 @@ public class User extends BaseTimeEntity { @Column(nullable = false, length = 50) private String name; - @Column(length = 14, unique = true) - private String nickname; - - @Column(name = "profile_image", nullable = false, length = 500) - private String profileImage; - @Column(name = "oauth_id", nullable = false, length = 100) private String oauthId; @@ -44,49 +36,15 @@ public class User extends BaseTimeEntity { @Column(nullable = false) private Role role = Role.USER; - @Enumerated(EnumType.STRING) - private Gender gender; - - // AgeRange는 사용하지 않으므로 주석 처리 - // @Enumerated(EnumType.STRING) - // @Column(name = "age_range", nullable = true) - // private AgeRange ageRange; - - @Column(name = "phone_number", length = 20) - private String phoneNumber; - - @Column(name = "birthday") - private LocalDate birthday; + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Profile profile; @Builder - public User(String email, String name, String nickname, String profileImage, String oauthId, - OauthType oauthType, Role role, Gender gender, /* AgeRange ageRange, */ String phoneNumber, LocalDate birthday) { + public User(String email, String name, String oauthId, OauthType oauthType, Role role) { this.email = email; this.name = name; - this.nickname = nickname; - this.profileImage = profileImage; this.oauthId = oauthId; this.oauthType = oauthType; this.role = role != null ? role : Role.USER; - this.gender = gender; - // this.ageRange = ageRange; - this.phoneNumber = phoneNumber; - this.birthday = birthday; } - - public void updateNickname(String nickname) { - this.nickname = nickname; - } - -// public void updateAdditionalInfo(Gender gender, LocalDate birthday, String phoneNumber) { -// if (gender != null) { -// this.gender = gender; -// } -// if (birthday != null) { -// this.birthday = birthday; -// } -// if (phoneNumber != null) { -// this.phoneNumber = phoneNumber; -// } -// } } diff --git a/src/main/java/com/dduru/gildongmu/user/dto/UserCheckNicknameResponse.java b/src/main/java/com/dduru/gildongmu/user/dto/UserCheckNicknameResponse.java deleted file mode 100644 index 19595c29..00000000 --- a/src/main/java/com/dduru/gildongmu/user/dto/UserCheckNicknameResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.dduru.gildongmu.user.dto; - -import lombok.Builder; - -@Builder -public record UserCheckNicknameResponse( - String sanitizedNickname -) { -} diff --git a/src/main/java/com/dduru/gildongmu/user/dto/UserInfo.java b/src/main/java/com/dduru/gildongmu/user/dto/UserInfo.java index d8c4af02..ca255e36 100644 --- a/src/main/java/com/dduru/gildongmu/user/dto/UserInfo.java +++ b/src/main/java/com/dduru/gildongmu/user/dto/UserInfo.java @@ -1,5 +1,6 @@ package com.dduru.gildongmu.user.dto; +import com.dduru.gildongmu.profile.domain.Profile; import com.dduru.gildongmu.user.domain.User; public record UserInfo( @@ -11,13 +12,14 @@ public record UserInfo( String nickname ) { public static UserInfo from(User user) { + Profile profile = user.getProfile(); return new UserInfo( user.getId(), user.getName(), - user.getProfileImage(), - user.getGender() != null ? user.getGender().name() : null, - user.getBirthday() != null ? user.getBirthday().toString() : null, - user.getNickname() + profile.getProfileImage(), + profile.getGender().name(), + profile.getBirthday().toString(), + profile.getNickname() ); } } diff --git a/src/main/java/com/dduru/gildongmu/user/repository/UserRepository.java b/src/main/java/com/dduru/gildongmu/user/repository/UserRepository.java index e37f5c4c..131c54fc 100644 --- a/src/main/java/com/dduru/gildongmu/user/repository/UserRepository.java +++ b/src/main/java/com/dduru/gildongmu/user/repository/UserRepository.java @@ -18,7 +18,4 @@ default User getByIdOrThrow(Long id) { return findById(id) .orElseThrow(() -> UserNotFoundException.of(id)); } - - boolean existsByNickname(String nickname); - boolean existsByPhoneNumber(String phoneNumber); } diff --git a/src/main/java/com/dduru/gildongmu/user/service/UserService.java b/src/main/java/com/dduru/gildongmu/user/service/UserService.java deleted file mode 100644 index 9442b39b..00000000 --- a/src/main/java/com/dduru/gildongmu/user/service/UserService.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.dduru.gildongmu.user.service; - -import com.dduru.gildongmu.common.exception.BusinessException; -import com.dduru.gildongmu.common.exception.ErrorCode; -import com.dduru.gildongmu.user.domain.User; -import com.dduru.gildongmu.user.dto.NicknameRandomResponse; -import com.dduru.gildongmu.user.dto.UserCheckNicknameResponse; -import com.dduru.gildongmu.user.dto.UserUpdateNicknameRequest; -import com.dduru.gildongmu.user.repository.UserRepository; -import com.dduru.gildongmu.user.utils.NicknameGenerator; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@RequiredArgsConstructor -@Service -public class UserService { - - private static final int NICKNAME_MAX_RETRY_ATTEMPTS = 10; - - private final NicknameGenerator nicknameGenerator; - private final UserRepository userRepository; - - @Transactional - public void updateNickname(Long userId, UserUpdateNicknameRequest request) { - User user = userRepository.getByIdOrThrow(userId); - checkDuplicateNickname(request.nickname()); - - user.updateNickname(request.nickname()); - } - - @Transactional(readOnly = true) - public UserCheckNicknameResponse checkNickname(String nickname) { - checkDuplicateNickname(nickname); - - return UserCheckNicknameResponse.builder() - .sanitizedNickname(nickname) - .build(); - } - - private void checkDuplicateNickname(String nickname) { - if (userRepository.existsByNickname(nickname)) { - throw new BusinessException(ErrorCode.NICKNAME_ALREADY_TAKEN); - } - } - - /** - * 중복되지 않는 유니크한 닉네임을 생성합니다. - * 형용사 + 명사 조합에 랜덤 숫자를 추가하여 고유성을 보장합니다. - * DB에 중복이 있으면 새로운 랜덤 숫자로 재시도합니다. - * - * @return 유니크한 닉네임 (예: "용감한 여행자1234") - */ - @Transactional(readOnly = true) - public NicknameRandomResponse generateRandomNickname() { - - for (int attempt = 0; attempt < NICKNAME_MAX_RETRY_ATTEMPTS; attempt++) { - String baseNickname = nicknameGenerator.generateBaseNickname(); - int randomNumber = nicknameGenerator.generateRandomNumber(); - String nickname = baseNickname + randomNumber; - - if (!userRepository.existsByNickname(nickname)) { - log.info("랜덤 닉네임 생성: {}", nickname); - return NicknameRandomResponse.of(nickname); - } - - log.debug("해당 닉네임이 이미 존재합니다: {}, 새로운 숫자를 부여하겠습니다.", nickname); - } - - String fallbackNickname = "뚜비" + (System.currentTimeMillis() % 10000); - log.warn("유니크한 닉네임 생성에 {}회 실패하여 대체 닉네임 사용: {}", NICKNAME_MAX_RETRY_ATTEMPTS, fallbackNickname); - return NicknameRandomResponse.of(fallbackNickname); - } -} diff --git a/src/main/java/com/dduru/gildongmu/verification/service/PhoneVerificationService.java b/src/main/java/com/dduru/gildongmu/verification/service/PhoneVerificationService.java index aea28cf6..22dd740b 100644 --- a/src/main/java/com/dduru/gildongmu/verification/service/PhoneVerificationService.java +++ b/src/main/java/com/dduru/gildongmu/verification/service/PhoneVerificationService.java @@ -1,7 +1,7 @@ package com.dduru.gildongmu.verification.service; import com.dduru.gildongmu.common.jwt.JwtTokenProvider; -import com.dduru.gildongmu.user.repository.UserRepository; +import com.dduru.gildongmu.profile.repository.ProfileRepository; import com.dduru.gildongmu.verification.dto.VerificationSendResponse; import com.dduru.gildongmu.verification.dto.VerificationVerifyResponse; import com.dduru.gildongmu.verification.exception.DuplicatePhoneNumberException; @@ -19,7 +19,7 @@ public class PhoneVerificationService { private final SmsService smsService; private final VerificationCodeService verificationCodeService; private final JwtTokenProvider jwtTokenProvider; - private final UserRepository userRepository; + private final ProfileRepository profileRepository; public VerificationSendResponse sendVerificationCode(String phoneNumber) { validatePhoneNumber(phoneNumber); @@ -48,7 +48,7 @@ public VerificationVerifyResponse verifyCode(String phoneNumber, String code) { } private void validatePhoneNumber(String phoneNumber) { - if (userRepository.existsByPhoneNumber(phoneNumber)) { + if (profileRepository.existsByPhoneNumber(phoneNumber)) { throw new DuplicatePhoneNumberException("이미 가입된 전화번호입니다. 로그인해주세요."); } } diff --git a/src/test/java/com/dduru/gildongmu/auth/repository/UserRepositoryTest.java b/src/test/java/com/dduru/gildongmu/auth/repository/UserRepositoryTest.java index 47389568..186190e7 100644 --- a/src/test/java/com/dduru/gildongmu/auth/repository/UserRepositoryTest.java +++ b/src/test/java/com/dduru/gildongmu/auth/repository/UserRepositoryTest.java @@ -1,8 +1,10 @@ package com.dduru.gildongmu.auth.repository; import com.dduru.gildongmu.config.QueryDslConfig; +import com.dduru.gildongmu.profile.domain.Profile; +import com.dduru.gildongmu.profile.domain.enums.Gender; +import com.dduru.gildongmu.profile.repository.ProfileRepository; import com.dduru.gildongmu.user.domain.User; -import com.dduru.gildongmu.user.enums.Gender; import com.dduru.gildongmu.user.enums.OauthType; import com.dduru.gildongmu.user.repository.UserRepository; import org.junit.jupiter.api.Test; @@ -21,21 +23,29 @@ class UserRepositoryTest { @Autowired private UserRepository userRepository; + @Autowired + private ProfileRepository profileRepository; + @Test void 사용자_저장_및_조회_테스트() { // given User user = User.builder() .email("test@example.com") .name("테스트사용자") - .profileImage("http://example.com/profile.jpg") .oauthId("12345") .oauthType(OauthType.KAKAO) - .gender(Gender.M) - .phoneNumber("010-1234-5678") .build(); // when User savedUser = userRepository.save(user); + + Profile profile = Profile.builder() + .user(savedUser) + .profileImage("http://example.com/profile.jpg") + .gender(Gender.M) + .phoneNumber("010-1234-5678") + .build(); + profileRepository.save(profile); // then assertThat(savedUser.getId()).isNotNull(); @@ -49,13 +59,18 @@ class UserRepositoryTest { User user = User.builder() .email("find@example.com") .name("찾을사용자") - .profileImage("http://example.com/profile.jpg") .oauthId("67890") .oauthType(OauthType.GOOGLE) - .gender(Gender.F) .build(); - userRepository.save(user); + User savedUser = userRepository.save(user); + + Profile profile = Profile.builder() + .user(savedUser) + .profileImage("http://example.com/profile.jpg") + .gender(Gender.F) + .build(); + profileRepository.save(profile); // when boolean exists = userRepository.existsByEmail("find@example.com"); @@ -70,13 +85,18 @@ class UserRepositoryTest { User user = User.builder() .email("oauth@example.com") .name("OAuth사용자") - .profileImage("http://example.com/profile.jpg") .oauthId("oauth123") .oauthType(OauthType.KAKAO) - .gender(Gender.M) .build(); - userRepository.save(user); + User savedUser = userRepository.save(user); + + Profile profile = Profile.builder() + .user(savedUser) + .profileImage("http://example.com/profile.jpg") + .gender(Gender.M) + .build(); + profileRepository.save(profile); // when boolean exists = userRepository.existsByOauthIdAndOauthType("oauth123", OauthType.KAKAO); diff --git a/src/test/java/com/dduru/gildongmu/profile/service/ProfileServiceTest.java b/src/test/java/com/dduru/gildongmu/profile/service/ProfileServiceTest.java new file mode 100644 index 00000000..d38eedcc --- /dev/null +++ b/src/test/java/com/dduru/gildongmu/profile/service/ProfileServiceTest.java @@ -0,0 +1,300 @@ +package com.dduru.gildongmu.profile.service; + +import com.dduru.gildongmu.common.exception.BusinessException; +import com.dduru.gildongmu.common.exception.ErrorCode; +import com.dduru.gildongmu.common.jwt.JwtTokenProvider; +import com.dduru.gildongmu.profile.domain.Profile; +import com.dduru.gildongmu.profile.dto.ProfileSetupRequest; +import com.dduru.gildongmu.profile.repository.ProfileRepository; +import com.dduru.gildongmu.profile.utils.NicknameGenerator; +import com.dduru.gildongmu.user.domain.User; +import com.dduru.gildongmu.user.enums.OauthType; +import com.dduru.gildongmu.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ProfileService 프로필 생성 테스트") +class ProfileServiceTest { + + @Mock + private ProfileRepository profileRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private JwtTokenProvider jwtTokenProvider; + + @Mock + private NicknameGenerator nicknameGenerator; + + @InjectMocks + private ProfileService profileService; + + @Test + @DisplayName("정상적인 프로필 생성 성공") + void 성공() { + // given + Long userId = 1L; + String nickname = "테스트닉네임"; + String phoneNumber = "01012345678"; + String verificationToken = "valid-token"; + ProfileSetupRequest request = new ProfileSetupRequest( + nickname, + "M", + phoneNumber, + "2000-01-01", + verificationToken + ); + + User user = User.builder() + .email("test@example.com") + .name("테스트사용자") + .oauthId("12345") + .oauthType(OauthType.KAKAO) + .build(); + + Profile profile = Profile.builder() + .user(user) + .build(); + + when(userRepository.getByIdOrThrow(userId)).thenReturn(user); + when(profileRepository.findByUser(user)).thenReturn(Optional.of(profile)); + when(profileRepository.existsByNicknameWithLock(nickname)).thenReturn(false); + when(jwtTokenProvider.validateVerificationToken(verificationToken, phoneNumber)).thenReturn(true); + when(profileRepository.existsByPhoneNumber(phoneNumber)).thenReturn(false); + when(profileRepository.save(any(Profile.class))).thenReturn(profile); + + // when + profileService.setupInitialProfile(userId, request); + + // then + verify(profileRepository).existsByNicknameWithLock(nickname); + verify(jwtTokenProvider).validateVerificationToken(verificationToken, phoneNumber); + verify(profileRepository).existsByPhoneNumber(phoneNumber); + verify(profileRepository).save(any(Profile.class)); + } + + @Test + @DisplayName("닉네임 중복 시 NICKNAME_ALREADY_TAKEN 예외 발생 (비관적 잠금 사용)") + void 닉네임중복_예외발생() { + // given + Long userId = 1L; + String duplicateNickname = "중복닉네임"; + ProfileSetupRequest request = new ProfileSetupRequest( + duplicateNickname, + "M", + "01012345678", + "2000-01-01", + "valid-token" + ); + + User user = User.builder() + .email("test@example.com") + .name("테스트사용자") + .oauthId("12345") + .oauthType(OauthType.KAKAO) + .build(); + + Profile profile = Profile.builder() + .user(user) + .build(); + + when(userRepository.getByIdOrThrow(userId)).thenReturn(user); + when(profileRepository.findByUser(user)).thenReturn(Optional.of(profile)); + when(profileRepository.existsByNicknameWithLock(duplicateNickname)).thenReturn(true); + + // when & then + assertThatThrownBy(() -> profileService.setupInitialProfile(userId, request)) + .isInstanceOf(BusinessException.class) + .satisfies(exception -> { + BusinessException businessException = (BusinessException) exception; + assertThat(businessException.getErrorCode()).isEqualTo(ErrorCode.NICKNAME_ALREADY_TAKEN); + }); + + verify(profileRepository).existsByNicknameWithLock(duplicateNickname); + verify(profileRepository, never()).save(any(Profile.class)); + verify(jwtTokenProvider, never()).validateVerificationToken(anyString(), anyString()); + } + + @Test + @DisplayName("전화번호 중복 시 DUPLICATE_PHONE_NUMBER 예외 발생") + void 전화번호중복_예외발생() { + // given + Long userId = 1L; + String duplicatePhoneNumber = "01012345678"; + ProfileSetupRequest request = new ProfileSetupRequest( + "테스트닉네임", + "M", + duplicatePhoneNumber, + "2000-01-01", + "valid-token" + ); + + User user = User.builder() + .email("test@example.com") + .name("테스트사용자") + .oauthId("12345") + .oauthType(OauthType.KAKAO) + .build(); + + Profile profile = Profile.builder() + .user(user) + .build(); + + when(userRepository.getByIdOrThrow(userId)).thenReturn(user); + when(profileRepository.findByUser(user)).thenReturn(Optional.of(profile)); + when(profileRepository.existsByNicknameWithLock("테스트닉네임")).thenReturn(false); + when(jwtTokenProvider.validateVerificationToken("valid-token", duplicatePhoneNumber)).thenReturn(true); + when(profileRepository.existsByPhoneNumber(duplicatePhoneNumber)).thenReturn(true); + + // when & then + assertThatThrownBy(() -> profileService.setupInitialProfile(userId, request)) + .isInstanceOf(BusinessException.class) + .satisfies(exception -> { + BusinessException businessException = (BusinessException) exception; + assertThat(businessException.getErrorCode()).isEqualTo(ErrorCode.DUPLICATE_PHONE_NUMBER); + }); + + verify(profileRepository).existsByNicknameWithLock("테스트닉네임"); + verify(profileRepository).existsByPhoneNumber(duplicatePhoneNumber); + verify(profileRepository, never()).save(any(Profile.class)); + } + + @Test + @DisplayName("인증 토큰 검증 실패 시 INVALID_TOKEN 예외 발생") + void 인증토큰검증실패_예외발생() { + // given + Long userId = 1L; + String invalidToken = "invalid-token"; + String phoneNumber = "01012345678"; + ProfileSetupRequest request = new ProfileSetupRequest( + "테스트닉네임", + "M", + phoneNumber, + "2000-01-01", + invalidToken + ); + + User user = User.builder() + .email("test@example.com") + .name("테스트사용자") + .oauthId("12345") + .oauthType(OauthType.KAKAO) + .build(); + + Profile profile = Profile.builder() + .user(user) + .build(); + + when(userRepository.getByIdOrThrow(userId)).thenReturn(user); + when(profileRepository.findByUser(user)).thenReturn(Optional.of(profile)); + when(profileRepository.existsByNicknameWithLock("테스트닉네임")).thenReturn(false); + when(jwtTokenProvider.validateVerificationToken(invalidToken, phoneNumber)).thenReturn(false); + + // when & then + assertThatThrownBy(() -> profileService.setupInitialProfile(userId, request)) + .isInstanceOf(BusinessException.class) + .satisfies(exception -> { + BusinessException businessException = (BusinessException) exception; + assertThat(businessException.getErrorCode()).isEqualTo(ErrorCode.INVALID_TOKEN); + }); + + verify(profileRepository).existsByNicknameWithLock("테스트닉네임"); + verify(jwtTokenProvider).validateVerificationToken(invalidToken, phoneNumber); + verify(profileRepository, never()).existsByPhoneNumber(anyString()); + verify(profileRepository, never()).save(any(Profile.class)); + } + + @Test + @DisplayName("프로필이 없는 경우 PROFILE_NOT_FOUND 예외 발생") + void 프로필없음_예외발생() { + // given + Long userId = 1L; + ProfileSetupRequest request = new ProfileSetupRequest( + "테스트닉네임", + "M", + "01012345678", + "2000-01-01", + "valid-token" + ); + + User user = User.builder() + .email("test@example.com") + .name("테스트사용자") + .oauthId("12345") + .oauthType(OauthType.KAKAO) + .build(); + + when(userRepository.getByIdOrThrow(userId)).thenReturn(user); + when(profileRepository.findByUser(user)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> profileService.setupInitialProfile(userId, request)) + .isInstanceOf(BusinessException.class) + .satisfies(exception -> { + BusinessException businessException = (BusinessException) exception; + assertThat(businessException.getErrorCode()).isEqualTo(ErrorCode.PROFILE_NOT_FOUND); + }); + + verify(profileRepository).findByUser(user); + verify(profileRepository, never()).existsByNicknameWithLock(anyString()); + verify(profileRepository, never()).save(any(Profile.class)); + } + + @Test + @DisplayName("비관적 잠금을 사용하여 닉네임 중복 체크 메서드 호출 확인") + void 비관적잠금사용확인() { + // given + Long userId = 1L; + String nickname = "테스트닉네임"; + ProfileSetupRequest request = new ProfileSetupRequest( + nickname, + "F", + "01087654321", + "1995-05-15", + "valid-token" + ); + + User user = User.builder() + .email("test2@example.com") + .name("테스트사용자2") + .oauthId("67890") + .oauthType(OauthType.GOOGLE) + .build(); + + Profile profile = Profile.builder() + .user(user) + .build(); + + when(userRepository.getByIdOrThrow(userId)).thenReturn(user); + when(profileRepository.findByUser(user)).thenReturn(Optional.of(profile)); + when(profileRepository.existsByNicknameWithLock(nickname)).thenReturn(false); + when(jwtTokenProvider.validateVerificationToken("valid-token", "01087654321")).thenReturn(true); + when(profileRepository.existsByPhoneNumber("01087654321")).thenReturn(false); + when(profileRepository.save(any(Profile.class))).thenReturn(profile); + + // when + profileService.setupInitialProfile(userId, request); + + // then + // existsByNickname이 아닌 existsByNicknameWithLock이 호출되었는지 확인 + verify(profileRepository).existsByNicknameWithLock(nickname); + verify(profileRepository, never()).existsByNickname(nickname); + } +} diff --git a/src/test/java/com/dduru/gildongmu/user/service/UserServiceTest.java b/src/test/java/com/dduru/gildongmu/user/service/UserServiceTest.java index 9635844c..48ce864c 100644 --- a/src/test/java/com/dduru/gildongmu/user/service/UserServiceTest.java +++ b/src/test/java/com/dduru/gildongmu/user/service/UserServiceTest.java @@ -1,8 +1,9 @@ package com.dduru.gildongmu.user.service; -import com.dduru.gildongmu.user.dto.NicknameRandomResponse; -import com.dduru.gildongmu.user.repository.UserRepository; -import com.dduru.gildongmu.user.utils.NicknameGenerator; +import com.dduru.gildongmu.profile.repository.ProfileRepository; +import com.dduru.gildongmu.profile.dto.NicknameRandomResponse; +import com.dduru.gildongmu.profile.service.ProfileService; +import com.dduru.gildongmu.profile.utils.NicknameGenerator; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,10 +24,10 @@ class UserServiceTest { private NicknameGenerator nicknameGenerator; @Mock - private UserRepository userRepository; + private ProfileRepository profileRepository; @InjectMocks - private UserService userService; + private ProfileService profileService; @DisplayName("중복이 없는 경우 첫 시도에 랜덤 닉네임을 반환한다") @Test @@ -38,16 +39,16 @@ void randomNickname_firstTrySuccess() { when(nicknameGenerator.generateBaseNickname()).thenReturn(baseNickname); when(nicknameGenerator.generateRandomNumber()).thenReturn(randomNumber); - when(userRepository.existsByNickname(expectedNickname)).thenReturn(false); + when(profileRepository.existsByNickname(expectedNickname)).thenReturn(false); // when - NicknameRandomResponse response = userService.generateRandomNickname(); + NicknameRandomResponse response = profileService.generateRandomNickname(); // then assertThat(response.nickname()).isEqualTo(expectedNickname); verify(nicknameGenerator, times(1)).generateBaseNickname(); verify(nicknameGenerator, times(1)).generateRandomNumber(); - verify(userRepository, times(1)).existsByNickname(eq(expectedNickname)); + verify(profileRepository, times(1)).existsByNickname(eq(expectedNickname)); } @DisplayName("중복이 발생하면 재시도하여 다음 닉네임을 반환한다") @@ -62,16 +63,16 @@ void randomNickname_retriesOnDuplicate() { when(nicknameGenerator.generateBaseNickname()).thenReturn(baseNickname); when(nicknameGenerator.generateRandomNumber()).thenReturn(firstNumber, secondNumber); - when(userRepository.existsByNickname(firstNickname)).thenReturn(true); - when(userRepository.existsByNickname(secondNickname)).thenReturn(false); + when(profileRepository.existsByNickname(firstNickname)).thenReturn(true); + when(profileRepository.existsByNickname(secondNickname)).thenReturn(false); // when - NicknameRandomResponse response = userService.generateRandomNickname(); + NicknameRandomResponse response = profileService.generateRandomNickname(); // then assertThat(response.nickname()).isEqualTo(secondNickname); - verify(userRepository, times(1)).existsByNickname(eq(firstNickname)); - verify(userRepository, times(1)).existsByNickname(eq(secondNickname)); + verify(profileRepository, times(1)).existsByNickname(eq(firstNickname)); + verify(profileRepository, times(1)).existsByNickname(eq(secondNickname)); verify(nicknameGenerator, times(2)).generateRandomNumber(); } @@ -85,16 +86,16 @@ void randomNickname_returnsFallbackAfterMaxAttempts() { when(nicknameGenerator.generateBaseNickname()).thenReturn(baseNickname); when(nicknameGenerator.generateRandomNumber()).thenReturn(duplicatedNumber); - when(userRepository.existsByNickname(duplicatedNickname)).thenReturn(true); + when(profileRepository.existsByNickname(duplicatedNickname)).thenReturn(true); // when - NicknameRandomResponse response = userService.generateRandomNickname(); + NicknameRandomResponse response = profileService.generateRandomNickname(); // then assertThat(response.nickname()).startsWith("뚜비"); verify(nicknameGenerator, times(10)).generateBaseNickname(); verify(nicknameGenerator, times(10)).generateRandomNumber(); - verify(userRepository, times(10)).existsByNickname(eq(duplicatedNickname)); + verify(profileRepository, times(10)).existsByNickname(eq(duplicatedNickname)); } } diff --git a/src/test/java/com/dduru/gildongmu/verification/service/PhoneVerificationServiceTest.java b/src/test/java/com/dduru/gildongmu/verification/service/PhoneVerificationServiceTest.java index c44395e5..d9ad427f 100644 --- a/src/test/java/com/dduru/gildongmu/verification/service/PhoneVerificationServiceTest.java +++ b/src/test/java/com/dduru/gildongmu/verification/service/PhoneVerificationServiceTest.java @@ -1,7 +1,7 @@ package com.dduru.gildongmu.verification.service; import com.dduru.gildongmu.common.jwt.JwtTokenProvider; -import com.dduru.gildongmu.user.repository.UserRepository; +import com.dduru.gildongmu.profile.repository.ProfileRepository; import com.dduru.gildongmu.verification.dto.VerificationVerifyResponse; import com.dduru.gildongmu.verification.exception.DuplicatePhoneNumberException; import com.dduru.gildongmu.verification.exception.SmsSendFailedException; @@ -25,7 +25,7 @@ class PhoneVerificationServiceTest { @Mock private JwtTokenProvider jwtTokenProvider; @Mock - private UserRepository userRepository; + private ProfileRepository profileRepository; @InjectMocks private PhoneVerificationService phoneVerificationService; @@ -33,7 +33,7 @@ class PhoneVerificationServiceTest { @Test void 인증번호발송_가입된번호면_예외발생() { // given - when(userRepository.existsByPhoneNumber("01012345678")).thenReturn(true); + when(profileRepository.existsByPhoneNumber("01012345678")).thenReturn(true); // when & then assertThatThrownBy(() -> phoneVerificationService.sendVerificationCode("01012345678")) @@ -45,7 +45,7 @@ class PhoneVerificationServiceTest { @Test void 인증번호발송_SMS실패시_롤백후_예외발생() { // given - when(userRepository.existsByPhoneNumber(anyString())).thenReturn(false); + when(profileRepository.existsByPhoneNumber(anyString())).thenReturn(false); VerificationCodeService.VerificationCreateResult result = new VerificationCodeService.VerificationCreateResult("999999", java.time.LocalDateTime.now().plusMinutes(3)); when(verificationCodeService.createVerification("01022223333")).thenReturn(result);