diff --git a/src/docs/asciidoc/user-api.adoc b/src/docs/asciidoc/user-api.adoc index baaa8d7..fab5deb 100644 --- a/src/docs/asciidoc/user-api.adoc +++ b/src/docs/asciidoc/user-api.adoc @@ -94,4 +94,28 @@ include::{snippetsDir}/generalUserSignUp/1/response-fields.adoc[] 실패1. include::{snippetsDir}/generalUserSignUp/2/http-response.adoc[] 실패 2 -include::{snippetsDir}/generalUserSignUp/3/http-response.adoc[] \ No newline at end of file +include::{snippetsDir}/generalUserSignUp/3/http-response.adoc[] + + +=== **6. 소셜 회원가입 api** + +소셜 회원가입 api입니다. + +==== Request +**소셜 로그인 후 발급된 session id를 cookie에 전달** +include::{snippetsDir}/socialUserSignUp/1/http-request.adoc[] + +==== Request Body Fields +include::{snippetsDir}/socialUserSignUp/1/request-fields.adoc[] + +==== 성공 Response +include::{snippetsDir}/socialUserSignUp/1/http-response.adoc[] + +==== Response Body Fields +include::{snippetsDir}/socialUserSignUp/1/response-fields.adoc[] + +==== 실패 Response +실패1. +include::{snippetsDir}/socialUserSignUp/2/http-response.adoc[] +실패 2 +include::{snippetsDir}/socialUserSignUp/3/http-response.adoc[] \ No newline at end of file diff --git a/src/main/java/com/ftm/server/application/dto/command/GeneralUserCreationCommand.java b/src/main/java/com/ftm/server/application/dto/command/GeneralUserCreationCommand.java index 08fe4d2..fd39ecf 100644 --- a/src/main/java/com/ftm/server/application/dto/command/GeneralUserCreationCommand.java +++ b/src/main/java/com/ftm/server/application/dto/command/GeneralUserCreationCommand.java @@ -2,6 +2,7 @@ import com.ftm.server.domain.enums.AgeGroup; import com.ftm.server.domain.enums.HashTag; +import java.util.List; import lombok.Data; @Data @@ -10,10 +11,14 @@ public class GeneralUserCreationCommand { private final String password; private final String nickName; private final AgeGroup ageGroup; - private final HashTag[] hashTags; + private final List hashtags; public static GeneralUserCreationCommand of( - String email, String password, String nickName, AgeGroup ageGroup, HashTag[] hashTag) { - return new GeneralUserCreationCommand(email, password, nickName, ageGroup, hashTag); + String email, + String password, + String nickName, + AgeGroup ageGroup, + List hashtag) { + return new GeneralUserCreationCommand(email, password, nickName, ageGroup, hashtag); } } diff --git a/src/main/java/com/ftm/server/application/dto/command/SocialUserCreationCommand.java b/src/main/java/com/ftm/server/application/dto/command/SocialUserCreationCommand.java new file mode 100644 index 0000000..5e72c4c --- /dev/null +++ b/src/main/java/com/ftm/server/application/dto/command/SocialUserCreationCommand.java @@ -0,0 +1,25 @@ +package com.ftm.server.application.dto.command; + +import com.ftm.server.domain.enums.AgeGroup; +import com.ftm.server.domain.enums.HashTag; +import com.ftm.server.domain.enums.SocialProvider; +import java.util.List; +import lombok.Data; + +@Data +public class SocialUserCreationCommand { + private final SocialProvider provider; + private final String socialId; + private final String nickname; + private final AgeGroup age; + private final List hashtags; + + public static SocialUserCreationCommand of( + SocialProvider provider, + String socialId, + String nickname, + AgeGroup age, + List hashTags) { + return new SocialUserCreationCommand(provider, socialId, nickname, age, hashTags); + } +} diff --git a/src/main/java/com/ftm/server/application/dto/command/SocialUserSignupCommand.java b/src/main/java/com/ftm/server/application/dto/command/SocialUserSignupCommand.java new file mode 100644 index 0000000..6159315 --- /dev/null +++ b/src/main/java/com/ftm/server/application/dto/command/SocialUserSignupCommand.java @@ -0,0 +1,23 @@ +package com.ftm.server.application.dto.command; + +import com.ftm.server.domain.enums.AgeGroup; +import com.ftm.server.domain.enums.HashTag; +import com.ftm.server.domain.enums.SocialProvider; +import com.ftm.server.domain.vo.PendingSocialUserVo; +import com.ftm.server.web.dto.request.SocialUserSignupRequest; +import java.util.List; +import lombok.Data; + +@Data +public class SocialUserSignupCommand { + private final SocialProvider socialProvider; + private final String socialId; + private final AgeGroup age; + private final List hashtags; + + public static SocialUserSignupCommand from( + SocialUserSignupRequest request, PendingSocialUserVo vo) { + return new SocialUserSignupCommand( + vo.getSocialProvider(), vo.getSocialId(), request.getAge(), request.getHashtags()); + } +} diff --git a/src/main/java/com/ftm/server/application/dto/query/FindBySocialValueQuery.java b/src/main/java/com/ftm/server/application/dto/query/FindBySocialValueQuery.java new file mode 100644 index 0000000..40852be --- /dev/null +++ b/src/main/java/com/ftm/server/application/dto/query/FindBySocialValueQuery.java @@ -0,0 +1,15 @@ +package com.ftm.server.application.dto.query; + +import com.ftm.server.domain.enums.SocialProvider; +import lombok.Data; + +@Data +public class FindBySocialValueQuery { + + private final SocialProvider socialProvider; + private final String socialId; + + public static FindBySocialValueQuery of(SocialProvider socialProvider, String socialId) { + return new FindBySocialValueQuery(socialProvider, socialId); + } +} diff --git a/src/main/java/com/ftm/server/application/port/repository/UserRepository.java b/src/main/java/com/ftm/server/application/port/repository/UserRepository.java index 66db833..700ebc3 100644 --- a/src/main/java/com/ftm/server/application/port/repository/UserRepository.java +++ b/src/main/java/com/ftm/server/application/port/repository/UserRepository.java @@ -12,4 +12,6 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); Optional findBySocialProviderAndSocialId(SocialProvider socialProvider, String socialId); + + Boolean existsBySocialIdAndSocialProvider(String socialId, SocialProvider socialProvider); } diff --git a/src/main/java/com/ftm/server/application/service/UserImageService.java b/src/main/java/com/ftm/server/application/service/UserImageService.java index 1454adb..b265208 100644 --- a/src/main/java/com/ftm/server/application/service/UserImageService.java +++ b/src/main/java/com/ftm/server/application/service/UserImageService.java @@ -21,8 +21,9 @@ public UserImage queryUserImageByUserId(FindByUserIdQuery query) { .orElseThrow(() -> new CustomException(ErrorResponseCode.USER_IMAGE_NOT_FOUND)); } - public void saveUserDefaultImage(User user) { + public UserImage saveUserDefaultImage(User user) { UserImage userImage = UserImage.createUserImage(user); userImageRepository.save(userImage); + return userImage; } } diff --git a/src/main/java/com/ftm/server/application/service/UserService.java b/src/main/java/com/ftm/server/application/service/UserService.java index 3756fb3..efdf6e2 100644 --- a/src/main/java/com/ftm/server/application/service/UserService.java +++ b/src/main/java/com/ftm/server/application/service/UserService.java @@ -1,8 +1,10 @@ package com.ftm.server.application.service; import com.ftm.server.application.dto.command.GeneralUserCreationCommand; +import com.ftm.server.application.dto.command.SocialUserCreationCommand; import com.ftm.server.application.dto.query.FindByEmailQuery; import com.ftm.server.application.dto.query.FindByIdQuery; +import com.ftm.server.application.dto.query.FindBySocialValueQuery; import com.ftm.server.application.dto.query.FindSocialUserQuery; import com.ftm.server.application.port.repository.UserRepository; import com.ftm.server.common.exception.CustomException; @@ -41,6 +43,17 @@ public User createGeneralUser(GeneralUserCreationCommand command) { return user; } + public User createSocialUser(SocialUserCreationCommand command) { + User user = User.createSocailUser(command); + userRepository.save(user); + return user; + } + + public Boolean userCheckBySocialValue(FindBySocialValueQuery query) { + return userRepository.existsBySocialIdAndSocialProvider( + query.getSocialId(), query.getSocialProvider()); + } + public Boolean userCheckByEmail(FindByEmailQuery query) { return userRepository.existsByEmail(query.getEmail()); } diff --git a/src/main/java/com/ftm/server/application/usecase/user/EmailAuthenticationUseCase.java b/src/main/java/com/ftm/server/application/usecase/user/EmailAuthenticationUseCase.java index 8479b06..027a4c9 100644 --- a/src/main/java/com/ftm/server/application/usecase/user/EmailAuthenticationUseCase.java +++ b/src/main/java/com/ftm/server/application/usecase/user/EmailAuthenticationUseCase.java @@ -14,9 +14,11 @@ import java.time.LocalDateTime; import java.util.Optional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @UseCase @RequiredArgsConstructor +@Slf4j public class EmailAuthenticationUseCase { private final MailSenderPort mailSenderPort; diff --git a/src/main/java/com/ftm/server/application/usecase/user/GeneralUserSignupUseCase.java b/src/main/java/com/ftm/server/application/usecase/user/GeneralUserSignupUseCase.java index f94d03d..6f348a8 100644 --- a/src/main/java/com/ftm/server/application/usecase/user/GeneralUserSignupUseCase.java +++ b/src/main/java/com/ftm/server/application/usecase/user/GeneralUserSignupUseCase.java @@ -14,7 +14,6 @@ import com.ftm.server.common.utils.RandomNickNameCreator; import com.ftm.server.domain.entity.EmailVerificationLogs; import com.ftm.server.domain.entity.User; -import com.ftm.server.domain.enums.HashTag; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -45,7 +44,6 @@ public void execute(GeneralUserSignupCommand command) { } String nickname = RandomNickNameCreator.generateNickname(); // random 닉네임 생성 - int size = command.getHashtags().size(); GeneralUserCreationCommand convertedCommand = GeneralUserCreationCommand.of( @@ -53,7 +51,7 @@ public void execute(GeneralUserSignupCommand command) { authenticationPort.passwordEncode(command.getPassword()), nickname, command.getAge(), - command.getHashtags().toArray(new HashTag[size])); + command.getHashtags()); User user = userService.createGeneralUser(convertedCommand); userImageService.saveUserDefaultImage(user); diff --git a/src/main/java/com/ftm/server/application/usecase/user/SocialUserSignupUseCase.java b/src/main/java/com/ftm/server/application/usecase/user/SocialUserSignupUseCase.java new file mode 100644 index 0000000..c5fe568 --- /dev/null +++ b/src/main/java/com/ftm/server/application/usecase/user/SocialUserSignupUseCase.java @@ -0,0 +1,48 @@ +package com.ftm.server.application.usecase.user; + +import com.ftm.server.application.dto.command.SocialUserCreationCommand; +import com.ftm.server.application.dto.command.SocialUserSignupCommand; +import com.ftm.server.application.dto.query.FindBySocialValueQuery; +import com.ftm.server.application.service.UserImageService; +import com.ftm.server.application.service.UserService; +import com.ftm.server.common.annotation.UseCase; +import com.ftm.server.common.exception.CustomException; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import com.ftm.server.common.utils.RandomNickNameCreator; +import com.ftm.server.domain.entity.User; +import com.ftm.server.domain.entity.UserImage; +import com.ftm.server.domain.vo.SocialUserSignupSummaryVo; +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +public class SocialUserSignupUseCase { + + private final UserService userService; + private final UserImageService userImageService; + + public SocialUserSignupSummaryVo execute(SocialUserSignupCommand command) { + + // 이미 가입된 회원인지 한번 더 확인 + if (userService.userCheckBySocialValue( + FindBySocialValueQuery.of(command.getSocialProvider(), command.getSocialId()))) { + throw new CustomException(ErrorResponseCode.USER_ALREADY_EXISTS); + } + + String nickname = RandomNickNameCreator.generateNickname(); + + SocialUserCreationCommand command1 = + SocialUserCreationCommand.of( + command.getSocialProvider(), + command.getSocialId(), + nickname, + command.getAge(), + command.getHashtags()); + + User user = userService.createSocialUser(command1); + + UserImage userImage = userImageService.saveUserDefaultImage(user); + + return SocialUserSignupSummaryVo.of(user, userImage.getObjectKey()); + } +} diff --git a/src/main/java/com/ftm/server/common/consts/PropertiesHolder.java b/src/main/java/com/ftm/server/common/consts/PropertiesHolder.java new file mode 100644 index 0000000..bc83094 --- /dev/null +++ b/src/main/java/com/ftm/server/common/consts/PropertiesHolder.java @@ -0,0 +1,19 @@ +package com.ftm.server.common.consts; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class PropertiesHolder { + + @Value("${cdn.path.root}") + private String cdnPathValue; + + public static String CDN_PATH; + + @PostConstruct + public void init() { + CDN_PATH = cdnPathValue; + } +} diff --git a/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java b/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java index 6fe11b4..a976a1e 100644 --- a/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java +++ b/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java @@ -9,6 +9,8 @@ public enum ErrorResponseCode { // 400번 INVALID_REQUEST_ARGUMENT( HttpStatus.BAD_REQUEST, "E400_001", "클라이언트 요청값의 일부가 잘못된 형식이거나, 필수 데이터가 누락되었습니다."), + INVALID_SEESION_FOR_SOCIAL_USER_SIGNUP( + HttpStatus.BAD_REQUEST, "E400_002", "소셜 회원가입을 위한 session 값이 잘못됨"), // 401번 NOT_AUTHENTICATED(HttpStatus.UNAUTHORIZED, "E401_001", "인증되지 않은 사용자입니다."), diff --git a/src/main/java/com/ftm/server/domain/entity/User.java b/src/main/java/com/ftm/server/domain/entity/User.java index 8d83d7d..63e8ef9 100644 --- a/src/main/java/com/ftm/server/domain/entity/User.java +++ b/src/main/java/com/ftm/server/domain/entity/User.java @@ -1,6 +1,7 @@ package com.ftm.server.domain.entity; import com.ftm.server.application.dto.command.GeneralUserCreationCommand; +import com.ftm.server.application.dto.command.SocialUserCreationCommand; import com.ftm.server.domain.enums.AgeGroup; import com.ftm.server.domain.enums.HashTag; import com.ftm.server.domain.enums.SocialProvider; @@ -107,12 +108,38 @@ private User( } public static User createGeneralUser(GeneralUserCreationCommand command) { + + HashTag[] hashTags = null; + + if (command.getHashtags() != null && command.getHashtags().isEmpty()) { + hashTags = command.getHashtags().toArray(new HashTag[0]); + } return User.builder() .email(command.getEmail()) .password(command.getPassword()) .nickname(command.getNickName()) .ageGroup(command.getAgeGroup()) - .favoriteHashtags(command.getHashTags()) + .favoriteHashtags(hashTags) + .groomingScore(0) + .isDeleted(false) + .role(UserRole.USER) + .build(); + } + + public static User createSocailUser(SocialUserCreationCommand command) { + + HashTag[] hashTags = null; + + if (command.getHashtags() != null && command.getHashtags().isEmpty()) { + hashTags = command.getHashtags().toArray(new HashTag[0]); + } + + return User.builder() + .socialProvider(command.getProvider()) + .socialId(command.getSocialId()) + .nickname(command.getNickname()) + .ageGroup(command.getAge()) + .favoriteHashtags(hashTags) .groomingScore(0) .isDeleted(false) .role(UserRole.USER) diff --git a/src/main/java/com/ftm/server/domain/vo/SocialUserSignupSummaryVo.java b/src/main/java/com/ftm/server/domain/vo/SocialUserSignupSummaryVo.java new file mode 100644 index 0000000..dd85351 --- /dev/null +++ b/src/main/java/com/ftm/server/domain/vo/SocialUserSignupSummaryVo.java @@ -0,0 +1,34 @@ +package com.ftm.server.domain.vo; + +import com.ftm.server.common.consts.PropertiesHolder; +import com.ftm.server.domain.entity.User; +import com.ftm.server.domain.enums.SocialProvider; +import lombok.Data; + +@Data +public class SocialUserSignupSummaryVo { + private final User user; + private final Long id; + private final String nickname; + private final SocialProvider socialProvider; + private final String profileImageUrl; + private final String mildLevelName; + private final String spicyLevelName; + + public static SocialUserSignupSummaryVo of(User user, String userImage) { + String mildLevelName = null; + String spicyLevelName = null; + if (user.getGroomingLevel() != null) { + mildLevelName = user.getGroomingLevel().getMildLevelName(); + spicyLevelName = user.getGroomingLevel().getMildLevelName(); + } + return new SocialUserSignupSummaryVo( + user, + user.getId(), + user.getNickname(), + user.getSocialProvider(), + PropertiesHolder.CDN_PATH + "/" + userImage, + mildLevelName, + spicyLevelName); + } +} diff --git a/src/main/java/com/ftm/server/domain/vo/UserSummaryVo.java b/src/main/java/com/ftm/server/domain/vo/UserSummaryVo.java index 600d4ab..be07b64 100644 --- a/src/main/java/com/ftm/server/domain/vo/UserSummaryVo.java +++ b/src/main/java/com/ftm/server/domain/vo/UserSummaryVo.java @@ -1,5 +1,6 @@ package com.ftm.server.domain.vo; +import com.ftm.server.common.consts.PropertiesHolder; import com.ftm.server.domain.entity.GroomingLevel; import com.ftm.server.domain.entity.User; import com.ftm.server.domain.entity.UserImage; @@ -17,7 +18,11 @@ public class UserSummaryVo { private UserSummaryVo(User user, UserImage userImage, GroomingLevel groomingLevel) { this.id = user.getId(); this.nickname = user.getNickname(); - this.profileImageUrl = userImage.getObjectKey(); // TODO: 추후 CDN 주소 + getObjectKey() 로 변경해야함 + this.profileImageUrl = + PropertiesHolder.CDN_PATH + + "/" + + userImage + .getObjectKey(); // TODO: 추후 CDN 주소 + getObjectKey() 로 변경해야함 -> 변경완료 this.mildLevelName = groomingLevel != null ? groomingLevel.getMildLevelName() : null; this.spicyLevelName = groomingLevel != null ? groomingLevel.getSpicyLevelName() : null; } diff --git a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java index b213ad3..48c2e88 100644 --- a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java +++ b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java @@ -55,7 +55,8 @@ public class SecurityConfig { "/api/users/email/authentication", "/api/users/email/authentication/code", "/api/auth/login/**", - "/api/users" + "/api/users", + "/api/users/social" }; private static final String[] ANONYMOUS_MATCHERS = {"/docs/**"}; diff --git a/src/main/java/com/ftm/server/web/controller/user/SocialUserSignupController.java b/src/main/java/com/ftm/server/web/controller/user/SocialUserSignupController.java new file mode 100644 index 0000000..b240873 --- /dev/null +++ b/src/main/java/com/ftm/server/web/controller/user/SocialUserSignupController.java @@ -0,0 +1,70 @@ +package com.ftm.server.web.controller.user; + +import static com.ftm.server.common.consts.StaticConsts.PENDING_SOCIAL_USER_SESSION_KEY; + +import com.ftm.server.application.dto.command.SocialUserSignupCommand; +import com.ftm.server.application.usecase.user.SocialUserSignupUseCase; +import com.ftm.server.common.exception.CustomException; +import com.ftm.server.common.response.ApiResponse; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import com.ftm.server.common.response.enums.SuccessResponseCode; +import com.ftm.server.domain.entity.User; +import com.ftm.server.domain.vo.PendingSocialUserVo; +import com.ftm.server.domain.vo.SocialUserSignupSummaryVo; +import com.ftm.server.infrastructure.security.AuthenticationService; +import com.ftm.server.web.dto.request.SocialUserSignupRequest; +import com.ftm.server.web.dto.response.SocialUserSignupResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class SocialUserSignupController { + + private final SocialUserSignupUseCase socialUserSignupUseCase; + + private final AuthenticationService authenticationService; + + @PostMapping("api/users/social") + public ResponseEntity> socialUserSignup( + @RequestBody @Valid SocialUserSignupRequest request, + HttpServletRequest req, + HttpServletResponse res) { + + HttpSession session = req.getSession(false); // session 조회 + + if (session == null) { // 잘못된 session 값일 경우 예외 반환 + throw new CustomException(ErrorResponseCode.INVALID_SEESION_FOR_SOCIAL_USER_SIGNUP); + } + + // 세션에서 저장된값 가져오기 + PendingSocialUserVo pendingSocialUser = + (PendingSocialUserVo) session.getAttribute(PENDING_SOCIAL_USER_SESSION_KEY); + + SocialUserSignupCommand command = SocialUserSignupCommand.from(request, pendingSocialUser); + + SocialUserSignupSummaryVo summary = socialUserSignupUseCase.execute(command); + + User user = summary.getUser(); + + session.invalidate(); + + // 인증 정보 저장 + Authentication auth = authenticationService.createAuthenticationFromSocial(user); + authenticationService.saveAuthenticatedSession(auth, req, res); + + SocialUserSignupResponse response = SocialUserSignupResponse.from(summary); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success(SuccessResponseCode.CREATED, response)); + } +} diff --git a/src/main/java/com/ftm/server/web/dto/request/GeneralUserSignupRequest.java b/src/main/java/com/ftm/server/web/dto/request/GeneralUserSignupRequest.java index 3120fd5..d6a4359 100644 --- a/src/main/java/com/ftm/server/web/dto/request/GeneralUserSignupRequest.java +++ b/src/main/java/com/ftm/server/web/dto/request/GeneralUserSignupRequest.java @@ -2,7 +2,6 @@ import com.ftm.server.domain.enums.AgeGroup; import com.ftm.server.domain.enums.HashTag; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import java.util.List; @@ -16,7 +15,10 @@ public class GeneralUserSignupRequest { message = "이메일 형식이 올바르지 않습니다.") private final String email; - @NotBlank private final String password; + @Pattern( + regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+{}\\[\\]:;<>,.?~\\\\\\-=/]).{8,}$", + message = "비밀번호 형식이 조건에 부합하지 않습니다.") + private final String password; @NotNull private final AgeGroup age; diff --git a/src/main/java/com/ftm/server/web/dto/request/SocialUserSignupRequest.java b/src/main/java/com/ftm/server/web/dto/request/SocialUserSignupRequest.java new file mode 100644 index 0000000..70e6510 --- /dev/null +++ b/src/main/java/com/ftm/server/web/dto/request/SocialUserSignupRequest.java @@ -0,0 +1,14 @@ +package com.ftm.server.web.dto.request; + +import com.ftm.server.domain.enums.AgeGroup; +import com.ftm.server.domain.enums.HashTag; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.Data; + +@Data +public class SocialUserSignupRequest { + @NotNull private final AgeGroup age; + + private final List hashtags; +} diff --git a/src/main/java/com/ftm/server/web/dto/response/SocialUserSignupResponse.java b/src/main/java/com/ftm/server/web/dto/response/SocialUserSignupResponse.java new file mode 100644 index 0000000..f9faee6 --- /dev/null +++ b/src/main/java/com/ftm/server/web/dto/response/SocialUserSignupResponse.java @@ -0,0 +1,32 @@ +package com.ftm.server.web.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ftm.server.domain.enums.SocialProvider; +import com.ftm.server.domain.vo.SocialUserSignupSummaryVo; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +public class SocialUserSignupResponse { + + private final Long id; + private final String nickname; + private final SocialProvider socialProvider; + private final String profileImageUrl; + private final String mildLevelName; + private final String spicyLevelName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm", shape = JsonFormat.Shape.STRING) + private final LocalDateTime loginTime; + + public static SocialUserSignupResponse from(SocialUserSignupSummaryVo vo) { + return new SocialUserSignupResponse( + vo.getId(), + vo.getNickname(), + vo.getSocialProvider(), + vo.getProfileImageUrl(), + vo.getMildLevelName(), + vo.getSpicyLevelName(), + LocalDateTime.now()); + } +} diff --git a/src/test/java/com/ftm/server/auth/UserLoginTest.java b/src/test/java/com/ftm/server/auth/UserLoginTest.java index f622a4b..186c78b 100644 --- a/src/test/java/com/ftm/server/auth/UserLoginTest.java +++ b/src/test/java/com/ftm/server/auth/UserLoginTest.java @@ -31,6 +31,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.snippet.Attributes; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; @@ -60,11 +61,17 @@ public class UserLoginTest extends BaseTest { fieldWithPath("data.mildLevelName") .type(STRING) .optional() - .description("순한맛 그루밍 레벨 이름"), + .description("순한맛 그루밍 레벨 이름") + .attributes( + new Attributes.Attribute( + "nullable", "그루밍 테스트를 아직 진행하지 않은 경우 null")), fieldWithPath("data.spicyLevelName") .type(STRING) .optional() - .description("매운맛 그루밍 레벨 이름"), + .description("매운맛 그루밍 레벨 이름") + .attributes( + new Attributes.Attribute( + "nullable", "그루밍 테스트를 아직 진행하지 않은 경우 null")), fieldWithPath("data.loginTime").type(STRING).description("로그인 시간")); private ResultActions getResultActions(UserLoginRequest request) throws Exception { diff --git a/src/test/java/com/ftm/server/user/GeneralUserSignupTest.java b/src/test/java/com/ftm/server/user/GeneralUserSignupTest.java index 036933e..db8051d 100644 --- a/src/test/java/com/ftm/server/user/GeneralUserSignupTest.java +++ b/src/test/java/com/ftm/server/user/GeneralUserSignupTest.java @@ -28,6 +28,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.snippet.Attributes; import org.springframework.test.web.servlet.ResultActions; public class GeneralUserSignupTest extends BaseTest { @@ -41,13 +42,16 @@ public class GeneralUserSignupTest extends BaseTest { fieldWithPath("email").type(JsonFieldType.STRING).description("회원가입 email"), fieldWithPath("password") .type(JsonFieldType.STRING) - .description("회원가입 password"), + .description("회원가입 password") + .attributes( + new Attributes.Attribute("constraint", "영문+숫자+특수문자 포함 8자 이상.")), fieldWithPath("age") .type(JsonFieldType.STRING) .description("연령대. 사용자 정보 옵션 조회 api에서 반환받은 value 값을 전달해 주세요."), fieldWithPath("hashtags") .type(JsonFieldType.ARRAY) - .description("관심 해시태그. 사용자 정보 옵션 조회 api에서 반환받은 value 값을 전달해 주세요.")); + .description("관심 해시태그. 사용자 정보 옵션 조회 api에서 반환받은 value 값을 전달해 주세요.") + .optional()); private final List responseFieldDescriptors = List.of( @@ -99,7 +103,7 @@ private RestDocumentationResultHandler getDocument(Integer identifier) { GeneralUserSignupRequest request = new GeneralUserSignupRequest( - email, "123456", AgeGroup.FIFTIES, List.of(HashTag.PERFUME)); + email, "qwer1234!", AgeGroup.FIFTIES, List.of(HashTag.PERFUME)); // when ResultActions resultActions = getResultActions(request); @@ -117,7 +121,7 @@ private RestDocumentationResultHandler getDocument(Integer identifier) { // given GeneralUserSignupRequest request = new GeneralUserSignupRequest( - "test@gmail.com", "123456", AgeGroup.FIFTIES, List.of(HashTag.PERFUME)); + "test@gmail.com", "qwer1234!", AgeGroup.FIFTIES, List.of(HashTag.PERFUME)); // when ResultActions resultActions = getResultActions(request); @@ -137,14 +141,15 @@ private RestDocumentationResultHandler getDocument(Integer identifier) { void 일반회원가입_실패2() throws Exception { // given String email = "test@gmail.com"; - HashTag[] hashTags = {HashTag.PERFUME}; + List hashTags = List.of(HashTag.PERFUME); GeneralUserCreationCommand command = - new GeneralUserCreationCommand(email, "123456", "닉넴", AgeGroup.FIFTIES, hashTags); + new GeneralUserCreationCommand( + email, "qwer1234!", "닉넴", AgeGroup.FIFTIES, hashTags); userService.createGeneralUser(command); GeneralUserSignupRequest request = new GeneralUserSignupRequest( - "test@gmail.com", "123456", AgeGroup.FIFTIES, List.of(HashTag.PERFUME)); + "test@gmail.com", "qwer1234!", AgeGroup.FIFTIES, List.of(HashTag.PERFUME)); // when ResultActions resultActions = getResultActions(request); diff --git a/src/test/java/com/ftm/server/user/SocialUserSignupTest.java b/src/test/java/com/ftm/server/user/SocialUserSignupTest.java new file mode 100644 index 0000000..ae039b8 --- /dev/null +++ b/src/test/java/com/ftm/server/user/SocialUserSignupTest.java @@ -0,0 +1,203 @@ +package com.ftm.server.user; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static com.ftm.server.common.consts.StaticConsts.PENDING_SOCIAL_USER_SESSION_KEY; +import static org.springframework.restdocs.headers.HeaderDocumentation.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.ftm.server.BaseTest; +import com.ftm.server.application.dto.command.SocialUserCreationCommand; +import com.ftm.server.application.service.UserService; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import com.ftm.server.domain.enums.AgeGroup; +import com.ftm.server.domain.enums.HashTag; +import com.ftm.server.domain.enums.SocialProvider; +import com.ftm.server.domain.vo.PendingSocialUserVo; +import com.ftm.server.web.dto.request.SocialUserSignupRequest; +import jakarta.servlet.http.Cookie; +import jakarta.transaction.Transactional; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.snippet.Attributes; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; + +public class SocialUserSignupTest extends BaseTest { + + @Autowired private UserService userService; + + private final List requestFieldDescriptors = + List.of( + fieldWithPath("age") + .type(JsonFieldType.STRING) + .description("연령대. 사용자 정보 옵션 조회 api에서 반환받은 value 값을 전달해 주세요."), + fieldWithPath("hashtags") + .type(JsonFieldType.ARRAY) + .description("관심 해시태그. 사용자 정보 옵션 조회 api에서 반환받은 value 값을 전달해 주세요.") + .optional()); + + private final List responseFieldDescriptors = + List.of( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("응답 상태"), + fieldWithPath("code").type(JsonFieldType.STRING).description("상태 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("메시지"), + fieldWithPath("data").type(JsonFieldType.OBJECT).optional().description("data"), + fieldWithPath("data.id") + .type(JsonFieldType.NUMBER) + .description("생성된 회원의 고유 id"), + fieldWithPath("data.nickname").type(JsonFieldType.STRING).description("회원 닉네임"), + fieldWithPath("data.socialProvider").type(STRING).description("소셜 제공자"), + fieldWithPath("data.profileImageUrl") + .type(JsonFieldType.STRING) + .description("프로필 이미지 url"), + fieldWithPath("data.mildLevelName") + .type(JsonFieldType.STRING) + .description("그루밍 레벨 순한맛 이름") + .optional() + .attributes( + new Attributes.Attribute( + "nullable", "그루밍 테스트를 아직 진행하지 않은 경우 null")), + fieldWithPath("data.spicyLevelName") + .type(JsonFieldType.STRING) + .description("그루밍 레벨 매운맛 이름") + .optional() + .attributes( + new Attributes.Attribute( + "nullable", "그루밍 테스트를 아직 진행하지 않은 경우 null")), + fieldWithPath("data.loginTime").type(STRING).description("로그인 시간")); + + private ResultActions getResultActions(SocialUserSignupRequest request, MockHttpSession session) + throws Exception { + return mockMvc.perform( // api 실행 + RestDocumentationRequestBuilders.post("/api/users/social") + .contentType(MediaType.APPLICATION_JSON) // request body content type + .session(session) + .cookie(new Cookie("SESSION", session.getId())) + .content(mapper.writeValueAsString(request))); + } + + // 문서화 반환 함수 + private RestDocumentationResultHandler getDocument(Integer identifier) { + return document( + "socialUserSignUp/" + identifier, + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint(), getModifiedHeader()), + responseFields(responseFieldDescriptors), + requestFields(requestFieldDescriptors), + responseHeaders( + headerWithName("Set-Cookie") + .description("세션 ID를 담고 있는 쿠키 (SESSION), 만료 시간: 1시간") + .optional()), + resource( + ResourceSnippetParameters.builder() + .tag("회원") + .summary("소셜 회원가입 api") + .description("소셜 회원가입 api입니다.") + .responseFields(responseFieldDescriptors) + .requestFields(requestFieldDescriptors) + .build())); + } + + @Test + @Transactional + void 소셜회원가입_성공() throws Exception { + // given + + // session 수동 생성 + PendingSocialUserVo pendingSocialUserVo = + PendingSocialUserVo.from(SocialProvider.KAKAO, "12345"); + MockHttpSession session = new MockHttpSession(); + session.setAttribute(PENDING_SOCIAL_USER_SESSION_KEY, pendingSocialUserVo); + + SocialUserSignupRequest request = + new SocialUserSignupRequest(AgeGroup.FIFTIES, List.of(HashTag.PERFUME)); + + // when + ResultActions resultActions = getResultActions(request, session); + MvcResult result = resultActions.andReturn(); + result.getResponse().addHeader("Set-Cookie", "SESSION=session-id; Path=/; HttpOnly"); + + // then + resultActions.andExpect(status().isCreated()); + + // documentation + resultActions.andDo(getDocument(1)); + } + + @Test + @Transactional + void 소셜회원가입_실패1() throws Exception { + // given + SocialUserSignupRequest request = + new SocialUserSignupRequest(AgeGroup.FIFTIES, List.of(HashTag.PERFUME)); + MockHttpSession fakeSession = new MockHttpSession(); + fakeSession.invalidate(); + + // when + ResultActions resultActions = getResultActions(request, fakeSession); + + // then + resultActions + .andExpect( + status().is( + ErrorResponseCode.INVALID_SEESION_FOR_SOCIAL_USER_SIGNUP + .getHttpStatus() + .value())) + .andExpect( + jsonPath("code") + .value( + ErrorResponseCode.INVALID_SEESION_FOR_SOCIAL_USER_SIGNUP + .getCode())); + + // documentation + resultActions.andDo(getDocument(2)); + } + + @Test + @Transactional + void 소셜회원가입_실패2() throws Exception { + // given + SocialUserCreationCommand command = + SocialUserCreationCommand.of( + SocialProvider.KAKAO, + "12345", + "닉네임", + AgeGroup.FIFTIES, + List.of(HashTag.PERFUME)); + userService.createSocialUser(command); + + PendingSocialUserVo pendingSocialUserVo = + PendingSocialUserVo.from(SocialProvider.KAKAO, "12345"); + MockHttpSession session = new MockHttpSession(); + session.setAttribute(PENDING_SOCIAL_USER_SESSION_KEY, pendingSocialUserVo); + + SocialUserSignupRequest request = + new SocialUserSignupRequest(AgeGroup.FIFTIES, List.of(HashTag.PERFUME)); + + // when + ResultActions resultActions = getResultActions(request, session); + + // then + resultActions + .andExpect( + status().is(ErrorResponseCode.USER_ALREADY_EXISTS.getHttpStatus().value())) + .andExpect(jsonPath("code").value(ErrorResponseCode.USER_ALREADY_EXISTS.getCode())); + + // documentation + resultActions.andDo(getDocument(3)); + } +}